README

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.