// Entering a function.
//
// Ported from `functions` in wax/src/lib-wax/typing.ml.
//
// Everything a body needs in scope before its first instruction is checked: the
// declared type resolved, the parameters bound as locals, the result types as
// cells the `return`s and the fall-through will be checked against.
//
// The one subtlety is what happens when the signature did NOT resolve. Its
// failure was already reported at registration, so re-resolving it here would
// say the same thing twice -- but the body still has to be checked, and it
// needs types to check against. So the source signature is resolved again with
// diagnostics MUTED, and whatever fails again becomes `Error` poison: a poison
// local, an `Error` result cell. The body's own mistakes still surface; the
// ones that follow from the bad signature do not.
///|
/// Run `f` on a fresh empty stack, reporting whatever is left on it, and
/// restore the enclosing stack.
///
/// The stack a function body runs on starts empty and must end empty. Same for
/// a block: `Operands::with_empty` is what keeps a block's operands from
/// leaking in or out, and this adds the report.
fn[A] with_empty_stack(
ops : Operands,
diagnostics : @diagnostic.Context,
location : @basic.Location,
f : () -> A,
) -> A {
let (result, inner) = ops.with_empty(f)
report_leftovers(inner, diagnostics, location, render_stack)
result
}
///|
/// The function type a definition declares, or `None` if it did not resolve.
///
/// Looked up WITHOUT marking the function name used: a definition site is not a
/// reference to itself, so the unused-declaration lint can still flag a
/// function nothing calls. The type name it yields IS a reference, and is
/// marked as one.
fn declared_functype(
ctx : @typing_env.ModuleContext,
name : @ast.Ident,
) -> @ast.FuncType? {
// A poison entry -- `Some(None)`, a signature that failed at registration --
// yields `None` here, and the body is checked against the muted re-resolution
// instead.
guard ctx.functions.find_no_mark(name.name) is Some(Some((_, tname, _))) else {
return None
}
guard find(ctx.types, ctx.diagnostics, { name: tname, loc: name.loc })
is Some((_, def)) else {
return None
}
match def.typ {
Func(ft) => Some(ft)
_ => {
expected_func_type(ctx.diagnostics, name.loc)
None
}
}
}
///|
/// A `#[start]` function must take nothing and return nothing.
///
/// There is nowhere for an argument to come from and nowhere for a result to
/// go: the start function is called by the host on instantiation.
fn check_start_signature(
ctx : @typing_env.ModuleContext,
name : @ast.Ident,
attributes : Array[@ast.Attribute],
ft : @ast.FuncType,
) -> Unit {
let has_start = attributes.iter().any(a => a.attr_name == "start")
if has_start && !(ft.params.is_empty() && ft.results.is_empty()) {
start_function_signature(ctx.diagnostics, name.loc)
}
}
///|
/// Establish a function's scope: its origin, parameters, and result types.
///
/// Returns the result cells, which are both the function's `return` targets and
/// the types its fall-through is checked against -- the caller opens the
/// outermost control frame with them.
///
/// `label_decls` and `assigned_locals` are collected by the syntactic lints,
/// which are a later stage; they are parameters rather than gathered here so
/// that when those land the caller fills them without this changing.
pub fn enter_function_scope(
ctx : @typing_env.ModuleContext,
name : @ast.Ident,
sign : @ast.FuncType?,
attributes : Array[@ast.Attribute],
label_decls? : Array[@ast.Ident] = [],
assigned_locals? : Map[String, Unit] = Map([]),
) -> Array[@infer.Cell[@infer.InferredType]] {
// Attribute everything this definition resolves -- its declared type as much
// as its body -- to the function itself, so nothing a dead function names
// looks externally referenced.
ctx.origin.val = FromFunction(name.name)
let declared = declared_functype(ctx, name)
// Where the muting happens. The reference builds a whole shadow context for
// this; here the resolvers take their diagnostics sink as an argument, so
// choosing the sink is the entire difference.
let sink = match declared {
Some(_) => ctx.diagnostics
None => @diagnostic.collector(parent=Some(ctx.diagnostics))
}
if declared is Some(ft) {
check_start_signature(ctx, name, attributes, ft)
}
// The results of a type that resolved always resolve; only the muted path can
// fail, and there a failure recovers as `Error` poison.
let results = match declared {
Some(ft) => ft.results
None =>
match sign {
Some(s) => s.results
None => []
}
}
let return_types = results.map(t => {
match internalize(ctx.type_context, sink, t) {
Some(c) => c
None => @infer.Cell::make(@infer.InferredType::Error)
}
})
ctx.enter_function(return_types~, label_decls~, assigned_locals~)
// Parameters come from the SOURCE signature, not the resolved type: only the
// source has their names. A parameter whose type does not resolve still binds
// its name, as a poison local read as `Error`, so the body's uses of it do
// not cascade.
if sign is Some(s) {
for p in s.params {
if p.desc.0 is Some(pname) {
let ty = internalize_valtype(ctx.type_context, sink, p.desc.1)
ctx.locals[pname.name] = (ty, pname.loc)
// Parameters always hold a value.
ctx.initialized_locals[pname.name] = ()
}
}
}
return_types
}
///|
/// Leave a function's scope.
///
/// References made after this are module-level again -- a global initializer, a
/// segment -- and so are roots.
pub fn leave_function_scope(ctx : @typing_env.ModuleContext) -> Unit {
ctx.origin.val = Root
}