README

marianoguerra/wax/ast does not have a README file

#
Attributes

type Attributes = Array[Attribute]

An Array is a collection of values that supports random access and can grow in size.

#
Body

A braced block of instructions together with the span of its braces.

#
FieldType

A type paired with its mutability. Used for both globals and struct fields, which is why it is generic in the thing being made mutable.

#
HeapType

A heap type: what a reference can point at.

The No* cases are the bottom types of each hierarchy (nofunc is a subtype of every function type and has no values), and None_ is the bottom of the internal hierarchy. Type names a concrete defined type; Exact names one and forbids subtypes, which is the custom-descriptors proposal's &!t.

#
Module

A module: a list of spanned fields.

There is no wrapper node -- a Wax file IS its field list.

#
RecType

A recursion group: types that may refer to each other.

A single type t = ... is a one-member group, so there is no separate non-recursive form.

#
RefType

A reference type: a heap type plus whether null is allowed.

#
StorageType

What a struct field or array element stores.

#
ValType

The Wax instantiations of the shared type family.

Wax refers to types by NAME, so every one of these is at Idx = Ident. Naming them here keeps [Ident] out of the ~90 AST constructors, and means the binary form's instantiation (at Idx = Int, for the code generator) will sit beside these rather than replace them.

Note Limits is deliberately absent: the Wax AST stores memory and table bounds inline as (UInt64, UInt64?)?, exactly as the reference does, and the Limits record in wasm_types belongs to the binary form.

#
BinOp

Binary operators.

Signedness is part of the operator rather than inferred, because WebAssembly has genuinely different instructions for signed and unsigned division, remainder, shift and comparison. It is optional on the operators where floats also apply (/, <, >, <=, >=): None means the spelling did not commit, and the typer resolves it from the operand type.

#
CastType

pub(all) enum CastType {
Value(
ValType
[Ident])
Func(nullable~ : Bool, sign~ : FuncType)
Signed(typ~ : NumType, signage~ :
Signage
, strict~ : Bool)
} derive(Eq,
Debug
)

What an as cast targets.

#
Catch

pub(all) enum Catch {
Catch(Ident, Ident)
CatchRef(Ident, Ident)
CatchAll(Ident)
CatchAllRef(Ident)
} derive(Eq,
Debug
)

A try_table catch clause.

The Ref variants deliver the exception reference itself alongside the payload, so a handler can rethrow.

#
DataElem

One element of a data segment's contents.

Holds no instructions: every value is a literal. In a run the element type is stated once and the values are RAW literal strings, packed little-endian -- raw for the same round-tripping reason as numeric literals.

#
DataMode

pub(all) enum DataMode[Info] {
Passive
Active(Ident, Instr[Info])
} derive(Eq,
Debug
)

Whether a data segment is copied in at instantiation or only on demand.

#
ElemMode

pub(all) enum ElemMode[Info] {
EPassive
EActive(Ident, Instr[Info])
} derive(Eq,
Debug
)

#
FuncType

A function signature.

Parameters may be anonymous (fn(i32)) or named (fn(x: i32)), hence the optional name; results never are.

#
FuncType::to_json

fn FuncType::to_json(self : FuncType) -> Json

#
Hint

pub(all) struct Hint[T] {
value : T
loc :
Location

} derive(Eq,
Debug
)

Advisory metadata.code.* metadata attached to an instruction.

Written in Wax as an attribute prefixing the instruction (#[likely], #[freq = 16], #[targets(f: 0.73)]). These never affect behaviour: an engine may ignore them, and dropping one changes performance, never semantics.

A hint carries the span of the attribute it was written as, so a diagnostic about a malformed hint blames the hint rather than the instruction it decorates.

#
Hints

pub(all) struct Hints {
branch : Hint[Bool]?
freq : Hint[Int]?
targets : Hint[Array[(Ident, Int)]]?
} derive(Eq,
Debug
)

#
Hints::is_empty

fn Hints::is_empty(self : Hints) -> Bool

#
Ident

#alias(Label)
pub(all) struct Ident {
name : String
loc :
Location

} derive(Eq,
Debug
)

An identifier with its span.

This is the Idx the Wax AST instantiates the type family at. The reference spells it (string, location) annotated; a named struct is the same shape with readable field names.

#
Ident::to_json

fn Ident::to_json(self : Ident) -> Json

#
ImportDecl

pub(all) struct ImportDecl {
id : Ident
kind : ImportKind
attributes : Array[Attribute]
} derive(Eq,
Debug
)

A single imported entity.

It is imported under id unless a name-only #[import = "name"] attribute overrides it. attributes also carries e.g. #[export] to re-export it.

#
ImportKind

pub(all) enum ImportKind {
Func(typ~ : Ident?, sign~ : FuncType?, exact~ : Bool)
Global(mut_~ : Bool, typ~ :
ValType
[Ident])
Tag(typ~ : Ident?, sign~ : FuncType?)
Memory(address_type~ :
AddressType
, limits~ : (UInt64, UInt64?)?, page_size_log2~ : Int?, shared~ : Bool)
Table(address_type~ :
AddressType
, reftype~ :
RefType
[Ident], limits~ : (UInt64, UInt64?)?)
} derive(Eq,
Debug
)

What an import "module" { ... } entry brings in.

Imports have no body, so these carry only type-level information.

#
Instr

pub(all) struct Instr[Info] {
desc : InstrDesc[Info]
info : Info
hints : Hints
expected :
ValType
[Ident]?
} derive(Eq,
Debug
)

An instruction node.

hints and expected are fields rather than wrapper nodes so the pervasive matches on desc neither see them nor have to see through them.

#
Instr::iter_instr

fn[I] Instr::iter_instr(self : Instr[I], f : (Instr[I]) -> Unit) -> Unit

Apply f to this instruction and, recursively, to everything within it.

Unlike map_info, f sees the whole node, so it can look at the desc.

#
Instr::map_info

fn[A, B] Instr::map_info(self : Instr[A], f : (A) -> B) -> Instr[B]

Re-annotate every node, keeping the shape.

The type checker's move: the parser builds Instr[Location] and typing rebuilds the same tree carrying inferred types.

#
Instr::sub_instrs

fn[I] Instr::sub_instrs(self : Instr[I]) -> Array[Instr[I]]

The instructions immediately nested within this one, in no particular order.

#
Instr::to_json

fn[Info] Instr::to_json(self : Instr[Info]) -> Json

The instruction tree.

Deliberately shallow in places -- a node's own kind and its children, not every field -- because these snapshots exist to catch a wrong SHAPE, and a full dump would churn on every unrelated change.

#
InstrDesc

pub(all) enum InstrDesc[Info] {
Block(label~ : Ident?, typ~ : FuncType, block~ :
Annotated
[Array[Instr[Info]],
Location
])
Loop(label~ : Ident?, typ~ : FuncType, block~ :
Annotated
[Array[Instr[Info]],
Location
])
While(label~ : Ident?, cond~ : Instr[Info], step~ : Instr[Info]?, block~ :
Annotated
[Array[Instr[Info]],
Location
])
If(label~ : Ident?, typ~ : FuncType, cond~ : Instr[Info], if_block~ :
Annotated
[Array[Instr[Info]],
Location
], else_block~ :
Annotated
[Array[Instr[Info]],
Location
]?)
TryTable(label~ : Ident?, typ~ : FuncType, catches~ : Array[Catch], block~ :
Annotated
[Array[Instr[Info]],
Location
])
Try(label~ : Ident?, typ~ : FuncType, block~ :
Annotated
[Array[Instr[Info]],
Location
], catches~ : Array[(Ident,
Annotated
[Array[Instr[Info]],
Location
])], catch_all~ :
Annotated
[Array[Instr[Info]],
Location
]?)
TryCatch(label~ : Ident?, typ~ : FuncType, block~ :
Annotated
[Array[Instr[Info]],
Location
], arms~ : Array[TryCatchArm[Info]])
Unreachable
Nop
Hole
Null
Get(Ident)
Path(Ident, Ident)
Set(Ident,
Annotated
[BinOp,
Location
]?, Instr[Info])
Tee(Ident, Instr[Info])
Call(Instr[Info], Array[Instr[Info]])
TailCall(Instr[Info], Array[Instr[Info]])
Labelled(Ident, Instr[Info])
Char(Char)
Str(Ident?, Bytes)
Int(String)
Float(String)
Cast(Instr[Info], CastType)
CastDesc(Instr[Info], Bool, Instr[Info])
Test(Instr[Info],
RefType
[Ident])
NonNull(Instr[Info])
Struct(Ident?, Array[(Ident, Instr[Info]?)])
StructDefault(Ident?)
StructDesc(Instr[Info], Array[(Ident, Instr[Info]?)])
StructDefaultDesc(Instr[Info])
StructGet(Instr[Info], Ident)
GetDescriptor(Instr[Info])
StructSet(Instr[Info], Ident, Instr[Info])
Array(Ident?, Instr[Info], Instr[Info])
ArrayDefault(Ident?, Instr[Info])
ArrayFixed(Ident?, Array[Instr[Info]])
ArraySegment(Ident?, Ident, Instr[Info], Instr[Info])
ArrayGet(Instr[Info], Instr[Info])
ArraySet(Instr[Info], Instr[Info], Instr[Info])
BinOpI(
Annotated
[BinOp,
Location
], Instr[Info], Instr[Info])
UnOpI(
Annotated
[UnOp,
Location
], Instr[Info])
Let(Array[(Ident?,
ValType
[Ident]?)], Instr[Info]?)
Br(Ident, Instr[Info]?)
BrIf(Ident, Instr[Info])
BrTable(Array[Ident], Instr[Info])
Dispatch(index~ : Instr[Info], cases~ : Array[Ident], default~ : Ident, arms~ : Array[(Ident,
Annotated
[Array[Instr[Info]],
Location
])])
Match(scrutinee~ : Instr[Info], arms~ : Array[(MatchPattern,
Annotated
[Array[Instr[Info]],
Location
])], default~ :
Annotated
[Array[Instr[Info]],
Location
])
BrOnNull(Ident, Instr[Info])
BrOnNonNull(Ident, Instr[Info])
BrOnCast(Ident,
RefType
[Ident], Instr[Info])
BrOnCastFail(Ident,
RefType
[Ident], Instr[Info])
BrOnCastDescEq(Ident, Bool, Instr[Info], Instr[Info])
BrOnCastDescEqFail(Ident, Bool, Instr[Info], Instr[Info])
Throw(Ident, Array[Instr[Info]])
ThrowRef(Instr[Info])
ContNew(Ident, Instr[Info])
ContBind(Ident, Ident, Array[Instr[Info]])
Suspend(Ident, Array[Instr[Info]])
Resume(Ident, Array[OnClause], Array[Instr[Info]])
ResumeThrow(Ident, Ident, Array[OnClause], Array[Instr[Info]])
ResumeThrowRef(Ident, Array[OnClause], Array[Instr[Info]])
Switch(Ident, Ident, Array[Instr[Info]])
On(Instr[Info], Array[OnClause])
Return(Instr[Info]?)
Sequence(Array[Instr[Info]])
Select(Instr[Info], Instr[Info], Instr[Info])
IfAnnotation(cond~ :
Cond
, then_body~ :
Annotated
[Array[Instr[Info]],
Location
], else_body~ :
Annotated
[Array[Instr[Info]],
Location
]?)
} derive(Eq,
Debug
)

A statement or expression. Wax is expression-oriented, so there is no separate statement type.

#
InstrDesc::map_desc

fn[A, B] InstrDesc::map_desc(self : InstrDesc[A], instr~ : (Instr[A]) -> Instr[B], block~ : (Array[Instr[A]]) -> Array[Instr[B]]) -> InstrDesc[B]

Rebuild a desc, rewriting every nested operand and every nested body.

The one exhaustive match over InstrDesc. Changing the type breaks this and nothing else, which is the point.

#
InstrDesc::to_json

fn[Info] InstrDesc::to_json(self : InstrDesc[Info]) -> Json

#
MatchPattern

pub(all) enum MatchPattern {
MatchCast(Ident?,
RefType
[Ident])
MatchNull
} derive(Eq,
Debug
)

A match arm pattern: an optionally-bound reference-type test, or a null test.

#
MemData

pub(all) struct MemData[Info] {
data_name : Ident?
offset : Instr[Info]
init : Array[DataElem]
} derive(Eq,
Debug
)

A data segment attached to a memory definition.

#
ModuleField

pub(all) enum ModuleField[Info] {
Type(Array[
Annotated
[(Ident, SubType),
Location
]])
Func(name~ : Ident, typ~ : Ident?, sign~ : FuncType?, body~ : (Ident?, Array[Instr[Info]]), attributes~ : Array[Attribute])
Global(name~ : Ident, mut_~ : Bool, typ~ :
ValType
[Ident]?, def~ : Instr[Info], attributes~ : Array[Attribute])
Tag(name~ : Ident, typ~ : Ident?, sign~ : FuncType?, attributes~ : Array[Attribute])
Memory(name~ : Ident, address_type~ :
AddressType
, limits~ : (UInt64, UInt64?)?, page_size_log2~ : Int?, shared~ : Bool, data~ : Array[MemData[Info]], attributes~ : Array[Attribute])
Data(name~ : Ident?, mode~ : DataMode[Info], init~ : Array[DataElem], attributes~ : Array[Attribute])
Table(name~ : Ident, address_type~ :
AddressType
, reftype~ :
RefType
[Ident], limits~ : (UInt64, UInt64?)?, init~ : Instr[Info]?, attributes~ : Array[Attribute])
Elem(name~ : Ident, reftype~ :
RefType
[Ident], mode~ : ElemMode[Info], init~ : Array[Instr[Info]], attributes~ : Array[Attribute])
Import(module_~ :
Annotated
[Bytes,
Location
], decl~ :
Annotated
[ImportDecl,
Location
])
ImportGroup(module_~ :
Annotated
[Bytes,
Location
], decls~ : Array[
Annotated
[ImportDecl,
Location
]])
ModuleAnnotation(Array[Attribute])
Conditional(cond~ :
Cond
, then_fields~ :
Annotated
[Array[
Annotated
[ModuleField[Info],
Location
]],
Location
], else_fields~ :
Annotated
[Array[
Annotated
[ModuleField[Info],
Location
]],
Location
]?)
} derive(Eq,
Debug
)

A top-level definition.

#
ModuleField::to_json

fn[Info] ModuleField::to_json(self : ModuleField[Info]) -> Json

#
NumType

pub(all) enum NumType {
I32
I64
F32
F64
} derive(Eq,
Debug
)

The four numeric types, where a construct admits only those.

#
NumType::to_str

fn NumType::to_str(self : NumType) -> String

#
OnClause

pub(all) enum OnClause {
OnLabel(Ident, Ident)
OnSwitch(Ident)
} derive(Eq,
Debug
)

A stack-switching handler clause, e on [tag -> 'label].

#
SubType

pub(all) struct SubType {
typ : CompType
supertype : Ident?
final_ : Bool
descriptor : Ident?
describes : Ident?
} derive(Eq,
Debug
)

One member of a recursion group.

descriptor/describes are the custom-descriptors proposal's pairing; both are present because the relation is navigable in each direction.

#
TryCatchArm

One arm of a structured try ... catch.

#
UnOp

pub(all) enum UnOp {
Neg
Pos
Not
} derive(Eq,
Debug
)

Unary operators.

#
format_signed_type

fn format_signed_type(typ : NumType, signage :
Signage
, strict : Bool) -> String

Render a signed numeric cast type, e.g. i32_s or i64_u_strict.

#
heaptype_json

#
import_name

The name an imported entity is bound to in wasm.

The name-only #[import = "name"] override if there is one, else the Wax name.

BYTES, where the reference has a string: a wasm import name is a byte string, and this port keeps a string literal as the bytes the lexer decoded rather than re-encoding at every use. The Wax name is encoded here, which is lossless -- an identifier is UTF-8 by construction.

#
lower_dispatch

fn[I] lower_dispatch(block_info : I, index~ : Instr[I], cases~ : Array[Ident], default~ : Ident, arms~ : Array[(Ident,
Annotated
[Array[Instr[I]],
Location
])]) -> Array[Instr[I]]

Lower a dispatch to a br_table inside nested case blocks.

One void block per case, the br_table innermost, each case body just after its own block. Branching to case c exits c's block, runs c's body, and falls through into the enclosing cases -- so the arms are listed in fall-through order, which is the REVERSE of the block nesting. The last arm is therefore outermost and its body trails the whole structure, which is why this returns a list rather than one instruction.

#
lower_match

Lower a match to the nested type-test ladder.

The scrutinee is evaluated ONCE and threaded through a chain of br_on_cast (or br_on_null for a null arm) in the innermost block: each test, on success, branches out to its arm's block carrying the narrowed value; on failure it leaves the progressively narrowed value for the next test. The first arm is innermost, so arm i's body sits in arm i + 1's block and the last arm's body sits in the escape block. After every test fails the innermost block drops the value and branches to escape, past all the arm bodies, and the default follows the (void) escape block as trailing code.

labels supplies n + 1 fresh labels: one per arm in order, then escape.

#
lower_trycatch

fn[I] lower_trycatch(block_info : I, join~ : Ident, arm_labels~ : Array[Ident], typ~ : FuncType, block~ :
Annotated
[Array[Instr[I]],
Location
], arms~ : Array[TryCatchArm[I]]) -> Instr[I]

Lower a structured try/catch to try_table plus a block ladder.

One block per arm (the first innermost) inside the join block, the try_table innermost with one catch clause per arm, and each arm body as trailing code just after its block -- so an arm's completion falls into the next arm and the last arm's falls out of the join as the try's value. The body's normal completion escapes past all arms with the one implicit branch to join, carrying the value.

arm_labels supplies one fresh label per arm; join is the try's own label when it has one. Each arm block's result type is the arm's entry stack (arm_types, which the typer fills).

#
lower_while

fn[I] lower_while(block_info : I, fresh_loop~ : Ident, label~ : Ident?, cond~ : Instr[I], step~ : Instr[I]?, block~ : Array[Instr[I]]) -> Array[Instr[I]]

Lower a leading-test while C { B } to 'L: loop { if C { B; br 'L; } }.

A continue-expression step has to run at the end of EVERY iteration, including one reached by continue (a branch to the loop label). When the loop is labelled -- so a continue can target it -- the body is wrapped in a block carrying the user's label, and the back-edge uses fresh_loop: br 'L then exits that block, runs the step, and takes the back-edge. An unlabelled stepped loop cannot be continued, so the step is simply appended to the body.

#
module_to_json

The whole module.

#
no_hints

let no_hints : Hints

An instruction with no hints, which is what everything carries unless an attribute says otherwise.

#
no_loc_instr

A synthesized instruction: no source span, no hints.

#
splice_field_name

let splice_field_name : String

The field name marking a .. splice at the head of a struct definition, which inherits the supertype's fields.

.. is not a valid identifier, so it can never collide with a real field. The typer replaces it with the supertype's fields and the printer renders it back as ..; its field TYPE is a placeholder and is never inspected.

#
synthetic_loop_label

let synthetic_loop_label : String

The label of the loop a label-less while lowers to.

# is not a Wax identifier character, so this can never clash with a source label nor be the target of a user br. It only ever labels the lowering the type checker discards -- conversion to wasm picks a readable name instead, and that is what reaches emitted wat.

#
valtype_json