marianoguerra/wax/check/env does not have a README file

    InferredAnnotation

    The same annotation before resolution, with the inference cells intact.

    Resolution collapses distinctions the cells still hold: a flexible numeric literal that has not picked a width, an unknown or unreachable type, an inline anonymous composite type. The editor reads this form because a hover wants to say number rather than the i32 it would default to; the code generator reads the resolved one because an opcode has to pick.

    ControlFrame

    One enclosing control frame: the label it can be branched to by, and the types it delivers.

    The label is kept as its Ident rather than a string so a branch can be linked back to the labelled construct for go-to-definition.

    Declaration

    type Declaration

    One declaration of a name: what kind of thing it declared, where, and under what assumption.

    kind is the declaring table's kind -- "function", "global", "memory" -- and not the name, which is the key this is stored under. It is here because a clash reports what the name is already taken BY, and the two declarations need not be of the same kind: memories and tables share one name space.

    HoverTarget

    What a resolved reference summarises, for a hover on a name that is not itself an expression.

    Kept as data rather than a rendered string: nothing is formatted until a hover actually asks, so a plain check pays one boxing per reference and no printing at all.

    ModuleContext

    pub struct ModuleContext {
    diagnostics :
    Context

    warn_unused : Bool
    simplify : Bool
    suggest : Bool
    faithful : Bool
    type_context : TypeContext
    types : Tbl[(
    RefIndex
    ,
    SubType
    )]
    functions : Tbl[(
    Id
    , String, Bool)?]
    globals : Tbl[(Bool,
    InferredValType
    ?)]
    import_globals : Tbl[(Bool,
    InferredValType
    ?)]
    tags : Tbl[
    FuncType
    ]
    memories : Tbl[(Int,
    AddressType
    )]
    datas : Tbl[Unit]
    tables : Tbl[(
    AddressType
    ,
    RefType
    [
    Ident
    ])]
    elems : Tbl[
    RefType
    [
    Ident
    ]]
    assigned_globals : Map[String, Unit]
    cast_traps_reported : Map[(Int, Int), Unit]
    canonical_type_references : Array[(Origin,
    Id
    )]
    origin :
    Ref
    [Origin]
    structs_by_fields : Map[String,
    Ident
    ?]
    not_expression_reported : Map[(Int, Int), Unit]
    locals : Map[String, (
    InferredValType
    ?,
    Location
    )]
    initialized_locals : Map[String, Unit]
    deferred_uninit : Array[Array[
    Ident
    ]]
    unresolved_label :
    Ref
    [Bool]
    read_locals : Array[Int]
    local_decls : Array[
    Ident
    ]
    used_labels : Array[Int]
    deferred_lints : Array[() -> Unit]
    label_decls : Array[
    Ident
    ]
    assigned_locals : Map[String, Unit]
    control_types : Array[ControlFrame]
    return_types : Array[
    Cell
    [
    InferredType
    ]]
    cond :
    Ref
    [
    T
    ]
    cond_env :
    Env

    resolve_links : Array[Reference]?
    pun_spans : Array[
    Location
    ]?
    member_completions : Array[(
    Location
    ,
    MemberReceiver
    )]?
    }

    Everything in scope while checking one configuration of one module.

    It is a large record because a wasm module has a lot of index spaces, and most of the fields are one of them. What is worth reading carefully is which fields are per-FUNCTION -- those are reset on entry to each -- and which are shared refs, because a shared ref is how a fact discovered deep inside a block reaches the function level.

    ModuleContext::current_deferral

    The innermost open collector, if any.

    ModuleContext::enter_function

    fn ModuleContext::enter_function(self : ModuleContext, return_types~ : Array[
    Cell
    [
    InferredType
    ]], label_decls? : Array[
    Ident
    ], assigned_locals? : Map[String, Unit]) -> Unit

    Clear the per-function state on entry to a function.

    The fields this touches are exactly the ones whose comments say "reset per function" -- gathering them here is what makes that claim checkable rather than a set of promises spread through the record.

    ModuleContext::merge_initialized

    fn ModuleContext::merge_initialized(self : ModuleContext, extra : Map[String, Unit]) -> Unit

    Add to the set, without disturbing what is already in it.

    ModuleContext::new

    Build the context for checking one configuration of one module.

    Every table is created here rather than by the caller, because they all have to share the same cond and origin refs -- that sharing is what lets a branch assumption set in one place be seen by a lookup in another, and what lets a reference be attributed without the origin being threaded through every call. A constructor is the only place that can guarantee it.

    ModuleContext::pop_deferral

    fn ModuleContext::pop_deferral(self : ModuleContext) -> Unit

    Close the innermost collector.

    ModuleContext::push_deferral

    Open a collector for uninitialized reads that are to be deferred rather than reported, and return it.

    ModuleContext::restore_initialized

    fn ModuleContext::restore_initialized(self : ModuleContext, saved : Map[String, Unit]) -> Unit

    Put back a snapshot, discarding whatever has been initialized since.

    ModuleContext::snapshot_initialized

    fn ModuleContext::snapshot_initialized(self : ModuleContext) -> Map[String, Unit]

    A copy of which locals currently hold a value.

    ModuleContext::with_frame

    fn[A] ModuleContext::with_frame(self : ModuleContext, frame : ControlFrame, f : () -> A) -> A

    Run f inside a control frame, restoring the enclosing one afterwards.

    The reference builds a fresh context with { ctx with control_types = ... }, which restores by construction. Save and restore is the same thing, and is what keeps a frame from outliving the block that opened it.

    ModuleContext::with_import_globals

    fn[A] ModuleContext::with_import_globals(self : ModuleContext, f : () -> A) -> A

    Run f with only the IMPORTED globals in scope.

    A table initializer runs before the module's own globals exist, so it can only name one that came from outside. The reference says this by handing the initializer a context whose global table IS the import table; swapping the field is the same act.

    ModuleContext::with_initialized_snapshot

    fn[A] ModuleContext::with_initialized_snapshot(self : ModuleContext, f : () -> A) -> A

    Run f with a snapshot of which locals are initialized, restoring it afterwards.

    An assignment inside a block must not escape it: the block may not run. But within a straight-line sequence the set only grows, which is what lets a trailing operand checked out of emission order be reconciled later.

    Namespace

    pub struct Namespace {
    cond :
    Ref
    [
    T
    ]
    entries : Map[String, Array[Declaration]]
    links : Array[Reference]?
    }

    The shared name space: the current assumption, the declarations, and where references are recorded.

    The assumption is a Ref shared by every name space and table of one module's checking, and updated as the passes descend into #[if] and #[else] branches -- so a lookup anywhere sees the branch it is in without the branch being threaded through every call.

    Namespace::exists

    Whether name is already taken here, reporting it if so.

    The question and the report are one operation because every caller asks in order to skip a second registration, and a caller that skipped silently would drop the declaration without saying why.

    Origin

    pub(all) enum Origin {
    Root
    FromFunction(String)
    FromType(String)
    Ignored
    } derive(Eq,
    Debug
    )

    Where a name resolution is being made FROM, for the reachability analysis behind the unused-field lint.

    The distinction that motivates it: two functions that only call each other reference one another, yet neither ever runs. So the lint cannot ask merely whether a declaration is referenced -- it has to ask whether anything that can actually run references it, and that needs to know where each reference came from.

    Reference

    A resolved name or label reference: where it was used, where it was defined, and what it resolves to.

    There is more than one definition only under conditional compilation, where a name may be declared in several mutually exclusive branches.

    Tbl

    pub struct Tbl[A] {
    kind : String
    names : Namespace
    entries : Map[String, Array[(
    T
    , A)]]
    used : Map[String, Array[Origin]]
    current :
    Ref
    [Origin]
    hover : (A) -> HoverTarget?
    }

    A table of values keyed by name, each entry carrying the assumption it was declared under.

    Tbl::add

    fn[A] Tbl::add(self : Tbl[A], diagnostics :
    Context
    , name : String, loc :
    Location
    , value : A) -> Unit

    Declare a name under the current assumption, reporting a clash.

    Entries accumulate rather than replace, and the LAST is the most recent -- which is the one resolve, override and remove all mean by "the current declaration".

    Tbl::copy_entries_into

    fn[A] Tbl::copy_entries_into(self : Tbl[A], dst : Tbl[A]) -> Unit

    Copy this table's ENTRIES into another, without re-declaring anything.

    A snapshot, not a second declaration: nothing is registered in the name space, so no clash is reported and the entries keep their own conditions. Re-adding them instead would say "already bound" for every name that is visible under more than one configuration -- and there is nothing wrong with those; that is what conditional compilation is for.

    The record of what has been REFERENCED is shared, not copied. A snapshot is a second view of the same declarations, so a name resolved through it is a name that was used -- and the unused lint asks the table the name was declared in, which is this one.

    Tbl::exists

    fn[A] Tbl::exists(self : Tbl[A], diagnostics :
    Context
    , name : String, loc :
    Location
    ) -> Bool

    Whether this name is already bound in the shared name space, reporting it.

    Tbl::find_no_mark

    fn[A] Tbl::find_no_mark(self : Tbl[A], name : String) -> A?

    Look up a name WITHOUT counting it as a reference.

    For the checker's own internal lookups -- a function resolving its own declared type while being checked -- which are not references anyone wrote. Counting them would keep every declaration alive and silence the unused-declaration lint entirely.

    Tbl::iter_entries

    fn[A] Tbl::iter_entries(self : Tbl[A]) -> Array[(String, A)]

    Every declaration in this table, name and value, in no particular order.

    Tbl::iter_references

    fn[A] Tbl::iter_references(self : Tbl[A]) -> Array[(String, Array[Origin])]

    Every name referenced through this table, with its origins.

    Tbl::mark_reference

    fn[A] Tbl::mark_reference(self : Tbl[A], name : String, origin : Origin) -> Unit

    Record a reference to name from origin, for a use that names no declaration syntactically.

    A string literal builds the canonical mut i8 array without writing a type name, yet every source definition that deduplicated onto that array really is used by it.

    Tbl::names

    fn[A] Tbl::names(self : Tbl[A]) -> Array[String]

    Every name declared in this table, for the "did you mean" suggestions.

    All of them, not only the ones visible under the current assumption: a name declared in another #[if] branch is still a plausible thing the author meant to write, and suggesting it is more useful than staying silent.

    Tbl::new

    fn[A] Tbl::new(kind : String, names : Namespace, current :
    Ref
    [Origin], hover? : (A) -> HoverTarget?) -> Tbl[A]

    Tbl::override_

    fn[A] Tbl::override_(self : Tbl[A], name : String, value : A) -> Unit

    Replace the value of the most recent declaration of name.

    For the two-step registration a recursion group needs: its members are declared with placeholder indices so that they can refer to each other, then given their real ones once the group is interned. The name space is untouched -- the declaration is the same one, only its value has settled.

    Tbl::referrers

    fn[A] Tbl::referrers(self : Tbl[A], name : String) -> Array[Origin]

    Where this name was referenced from, if anywhere.

    Tbl::remove

    fn[A] Tbl::remove(self : Tbl[A], name : String) -> Unit

    Drop the most recent declaration of name, keeping any older one.

    The other half of the two-step registration: a group that fails to resolve takes its placeholder names back out, so they do not resolve to a type that was never interned.

    The NAME SPACE keeps its entry, deliberately: the name was still written here, so a second declaration of it is still a duplicate and still says so.

    Tbl::resolve

    fn[A] Tbl::resolve(self : Tbl[A], name : String, use_ :
    Location
    ) -> A?

    Look up a name for a use, marking it referenced and linking it to its definitions.

    Only ever called for a REFERENCE; a declaration goes through add. That is what lets the mark be trusted by the unused-declaration lint.

    Tbl::visible

    fn[A] Tbl::visible(self : Tbl[A], name : String) -> Array[A]

    Every declaration of this name whose assumption can hold together with the current one.

    Not the ones that IMPLY it: a declaration guarded by a weaker condition is still in scope here. Only a declaration whose assumption is inconsistent with the current one is invisible.

    TypeContext

    The type-resolution context: the names in scope, the store they resolve into, and the proposals that are enabled.

    TypeContext::invalidate

    fn TypeContext::invalidate(self : TypeContext) -> Unit

    Note that the type space changed, so the next query recomputes.

    TypeContext::subtyping_info

    Subtyping info for the current type space.

    blank_comments

    fn blank_comments(s : String) -> String

    The source with every comment blanked to spaces, newlines kept.

    The source-scanning suggestions look for a delimiter -- a :, a |, a keyword -- to decide where a rewrite goes, and one inside a comment is not syntax. Blanking rather than deleting is what keeps every byte offset and the line structure intact, so a span found in the blanked text addresses the same bytes in the real one.

    Wax has // line comments and /* */ block comments, and the block comments NEST -- so this counts depth rather than scanning for the first close.

    expression_type_opt

    The single inference cell an instruction leaves on the stack, or None if it leaves none or several.

    The error-free counterpart of the checker's own expression_type, which reports "an expression is expected here" and yields an error cell. A lint or a suggestion reads the cell when there is exactly one and stays silent otherwise, rather than emitting the diagnostic the checker already owns.

    record_members

    Record a struct field access, for member completion: the field's span -- possibly partial, since the cursor may be mid-word -- and what it is on.

    record_pun

    Record a punned struct-literal field's span -- the bare x standing for x: x.

    Such a span is both a field name and a variable use, so a rename has to expand it (x becomes x: new) rather than replace it, and the editor needs to know which spans those are.

    record_reference

    fn record_reference(sink : Array[Reference]?, use_ :
    Location
    , definitions : Array[
    Location
    ], hover? : HoverTarget?) -> Unit

    Record a use and the definitions it binds to.

    Synthesized definitions are dropped, and so is the self-reference a name's own declaration makes when it looks itself up: go-to-definition on a definition has nowhere useful to go.

    ref_exn_valtype

    The exn reference type: what an exception object is, and what the instructions that carry one around take.

    source_slice

    fn source_slice(source : String?, loc :
    Location
    ) -> String?

    The source text a span covers, or None when the source is unavailable or the span does not fit it.

    standalone_valtype

    The concrete value type a cell stands for on its own, or None when it has none yet.

    A still-flexible literal takes its default width -- an integer or a bare number becomes i32, a large one i64, a float f64. Null and UnknownRef concretize to the nullable and non-nullable bottom reference respectively: the latter because that is the type null! produced before UnknownRef existed.

    Pure and context-free, unlike the checker's own path, because the only reference it can produce is the built-in bottom.