// Lowering a module's fields.
//
// Ported from `module_` in wax/src/lib-conversion/to_wasm.ml.
//
// The sections come out in the order the format requires, which is not the order
// a wax module is written in -- so this collects each kind as it walks and
// assembles at the end. The index spaces were already assigned, so a function
// written last can be called by one written first without a second pass.
///|
/// Lower a checked module to the binary form.
pub fn lower_module(
ctx : @typing_env.ModuleContext,
store : @type_store.TypeStore,
source : @ast.Module[@basic.Location],
typed : Array[
@basic.Annotated[
@ast.ModuleField[@typing_env.InferredAnnotation],
@basic.Location,
],
],
/// Whether an unresolved `#[if(..)]` may be lowered rather than refused.
/// Only the TEXT form may: the binary has nowhere to put one.
keep_conditionals? : Bool = false,
) -> @wasm_bin.Module raise LowerError {
// A module with an UNRESOLVED conditional cannot be emitted at all, and the
// check belongs here rather than at the first one reached: the reference
// rejects the module, not the field, and stumbling further in produces a
// downstream complaint about whatever the conditional left inconsistent.
for field in source {
ignore(field)
}
// A module with an unresolved conditional cannot be EMITTED at all, and the
// check belongs here rather than at the first one reached: the reference
// rejects the module, not the field, and stumbling further in produces a
// downstream complaint about whatever the conditional left inconsistent.
if !keep_conditionals {
for field in source {
if field.desc is Conditional(..) {
raise Unemittable("conditional annotations", field.info)
}
// A GUARDED attribute is a conditional too, and the reference refuses a
// module for one just the same: `#[start, if(debug)]` says the start is
// conditional, and a binary has one start or none.
for a in field_attributes(field.desc) {
if a.attr_guard is Some(_) {
raise Unemittable("conditional annotations", field.info)
}
}
}
}
// The conditional groups FIRST: the index spaces are read through them --
// which declaration of a name a reference means depends on which branch the
let indices = assign_indices(ctx, source)
// The layout FIRST: it installs the store-to-emitted translation that every
// type reference below is read through, including the ones inside the
// annotations the checker left.
let layout = type_layout(ctx, source, store)
let m = @wasm_bin.Module::empty()
// The conditional groups before anything resolves a name: which declaration
// of a name a reference means depends on which branch the reference stands
// in, and that is read off these.
note_conditionals(m, source)
let low : Lowering = {
ctx,
store,
indices,
layout,
locals: Map([]),
local_types: [],
entry_locs: Map([]),
conditionals: m.text.conditionals,
keep_conditionals,
body_conditionals: [],
pending_start: None,
spans: [],
nested_spans: [],
binding_slots: Map([]),
claimed: Map([]),
local_names: Map([]),
label_names: Map([]),
label_counter: 0,
pending_handlers: [],
func_refs_in_body: [],
func_refs_outside: [],
in_body: false,
labels: [],
source_types: source_type_index(ctx, layout),
functypes: functype_index(store, layout),
}
// Imports come from the SOURCE, because the checker resolves them into its
// tables and drops them from the typed output -- and they are exactly what
// cannot be skipped, since each one starts an index space. Emitted first, in
// written order, which is the order their indices were assigned in.
// Where each recorded field stood in the SOURCE, so the two walks below --
// imports first, then everything else -- can be put back into one order.
// The walks cannot be merged: an import starts an index space and has to be
// lowered before anything that refers to it.
let order_at : Array[@basic.Location] = []
// The declared features lead the fields, wherever the attributes stood.
for f in source {
if f.desc is ModuleAnnotation(attrs) {
for a in attrs {
if a.attr_name == "feature" &&
a.attr_value is Some({ desc: Str(_, b), .. }) {
m.text.field_order.push(FFeature(b))
order_at.push(f.info)
}
}
}
}
let mut failure : LowerError? = None
@typing.walk_fields(ctx, source, field => {
if failure is Some(_) {
return
}
try {
match field.desc {
Import(module_~, decl~) => {
low.import_(m, module_.desc, decl.desc, field.info)
m.text.field_order.push(FImport(m.imports.length() - 1))
order_at.push(field.info)
low.flush_start(m, order_at, field.info)
}
ImportGroup(module_~, decls~) => {
// The order is recorded AFTER the group is compacted, because
// compacting replaces the run of entries with one that stands for
// all of them -- so the indices to record only exist once it has run.
let before = m.imports.length()
let before_exports = m.exports.length()
for d in decls {
low.import_(m, module_.desc, d.desc, field.info)
}
low.compact_group(m, decls.length(), field.info)
for k in before.. ()
}
} catch {
e => failure = Some(e)
}
})
if failure is Some(e) {
raise e
}
for field in typed {
let before = m.text.field_order.length()
low.field(m, field)
for k in before.. (func))` beside the import that already
// spells that signature out.
for at, _ in layout.referenced {
m.text.named_types[at] = true
}
low.type_names(m)
low.module_name(m, source)
low.target_features(m, source)
m
}
///|
/// Write the `(start ..)` an import named, after the import itself.
///
/// An import is written before the start it names, and `import_` cannot push
/// the field entry itself: the import's own entry is pushed by its caller,
/// once the index it took is known.
fn Lowering::flush_start(
self : Lowering,
m : @wasm_bin.Module,
order_at : Array[@basic.Location],
loc : @basic.Location,
) -> Unit {
guard self.pending_start is Some((fi, a)) else { return }
self.pending_start = None
if a.attr_guard is Some(g) {
m.text.conditionals.push({
cond: wat_cond(g.desc),
loc: a.attr_span,
then_: a.attr_span,
else_: None,
})
m.text.field_order.push(FStart(fi))
order_at.push(a.attr_span)
return
}
m.text.field_order.push(FStart(fi))
order_at.push(loc)
}
///|
/// Whether an emitted function type has the shape this signature wrote.
///
/// Only the ARITY is compared: that is what tells two conditional branches'
/// declarations apart, and a deeper comparison would have to re-resolve every
/// written type through the layout to say anything more.
fn Lowering::functype_matches(
self : Lowering,
emitted : Int,
sign : @ast.FuncType,
) -> Bool {
guard self.layout.types.get(emitted) is Some(t) else { return true }
guard t.composite is Func(ft) else { return true }
ft.params.length() == sign.params.length() &&
ft.results.length() == sign.results.length()
}
///|
/// How many imports of this kind are already there, which is the index the
/// next one takes.
fn imports_so_far(m : @wasm_bin.Module, want : Int) -> Int {
let mut n = 0
for i in m.imports {
// A COMPACT group is one entry standing for a run of imports, so the count
// advances by what the entry stands for rather than by one.
match i.group {
None => if import_kind(i.desc) == want { n = n + 1 }
Some(Heterogeneous(items)) =>
for it in items {
if import_kind(it.1) == want {
n = n + 1
}
}
Some(Homogeneous(names)) =>
if import_kind(i.desc) == want {
n = n + names.length()
}
}
}
n
}
///|
/// Which index space an import takes a slot in.
fn import_kind(d : @wasm_bin.ImportDesc) -> Int {
match d {
Func(_, _) => 0
Table(_) => 1
Memory(_) => 2
Global(_) => 3
Tag(_) => 4
}
}
///|
/// The attributes a field carries, wherever it carries them.
fn field_attributes(
f : @ast.ModuleField[@basic.Location],
) -> Array[@ast.Attribute] {
match f {
Func(attributes~, ..)
| Global(attributes~, ..)
| Memory(attributes~, ..)
| Table(attributes~, ..)
| Tag(attributes~, ..)
| Elem(attributes~, ..)
| Data(attributes~, ..) => attributes
Import(decl~, ..) => decl.desc.attributes
ImportGroup(decls~, ..) => {
let out : Array[@ast.Attribute] = []
for d in decls {
for a in d.desc.attributes {
out.push(a)
}
}
out
}
_ => []
}
}
///|
/// Record the conditional groups the source wrote, with the extent of each
/// branch, so the text form can put its fields back under them.
fn note_conditionals(
m : @wasm_bin.Module,
fields : @ast.Module[@basic.Location],
) -> Unit {
for field in fields {
guard field.desc is Conditional(cond~, then_fields~, else_fields~) else {
continue
}
m.text.conditionals.push({
cond: wat_cond(cond),
loc: field.info,
then_: then_fields.info,
else_: else_fields.map(e => e.info),
})
note_conditionals(m, then_fields.desc)
if else_fields is Some(e) {
note_conditionals(m, e.desc)
}
}
}
///|
/// A condition as the WEBASSEMBLY TEXT format writes one.
///
/// Not the Wax spelling: wax says `all(a, b)` and the text format says
/// `(and a b)`. Rendered to a string because a condition never wraps -- it is
/// an annotation payload, and the layout has nothing to decide inside it.
fn wat_cond(c : @wasm_types.Cond) -> String {
match c {
Var(v) => "$" + v.desc
Str(s) => quoted_bytes(s.desc)
Version(a, b, c2) => "(\{a} \{b} \{c2})"
And(l) => "(and" + cond_list(l) + ")"
Or(l) => "(or" + cond_list(l) + ")"
Not(c2) => "(not " + wat_cond(c2) + ")"
Cmp(op, a, b) =>
"(" + wat_cmp(op) + " " + wat_cond(a) + " " + wat_cond(b) + ")"
}
}
///|
fn cond_list(l : Array[@wasm_types.Cond]) -> String {
let out = StringBuilder::new()
for c in l {
out.write_string(" " + wat_cond(c))
}
out.to_string()
}
///|
fn wat_cmp(op : @wasm_types.CmpOp) -> String {
match op {
Eq => "="
Ne => "<>"
Lt => "<"
Gt => ">"
Le => "<="
Ge => ">="
}
}
///|
/// A byte string as an annotation payload writes one. Printable ASCII stands
/// for itself; anything else takes a hex escape.
fn quoted_bytes(b : Bytes) -> String {
let digits = [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f",
]
let out = StringBuilder::new()
out.write_char('"')
for k in 0..= 0x20 && c < 0x7F && c != 0x22 && c != 0x5C {
out.write_char(Int::unsafe_to_char(c))
} else {
out.write_string("\\" + digits[c / 16] + digits[c % 16])
}
}
out.write_char('"')
out.to_string()
}
///|
/// Put the recorded fields back into source order.
///
/// The imports were lowered first because each starts an index space, so the
/// order they were RECORDED in is not the order they were written in. Sorting
/// by where each field started restores that, and the sort is stable so the
/// fields one source field produced keep their relative order.
fn reorder_fields(m : @wasm_bin.Module, at : Array[@basic.Location]) -> Unit {
guard at.length() == m.text.field_order.length() else { return }
let order = Array::makei(at.length(), k => k)
order.sort_by((a, b) => {
if at[a].start.cnum != at[b].start.cnum {
at[a].start.cnum - at[b].start.cnum
} else {
a - b
}
})
let sorted = order.map(k => m.text.field_order[k])
let locs = order.map(k => at[k])
m.text.field_order.clear()
for f in sorted {
m.text.field_order.push(f)
}
for l in locs {
m.text.field_locs.push(l)
}
}
///|
/// Lower one module field into the module being built.
fn Lowering::field(
self : Lowering,
m : @wasm_bin.Module,
field : @basic.Annotated[
@ast.ModuleField[@typing_env.InferredAnnotation],
@basic.Location,
],
) -> Unit raise LowerError {
let loc = field.info
match field.desc {
Func(name~, typ~, sign~, body~, attributes~) => {
// The function's TYPE index comes from the checker, which interned the
// signature when it declared the function -- so the type section and the
// function section agree by construction rather than by a second lookup.
guard self.ctx.functions.find_no_mark(name.name) is Some(Some(entry)) else {
raise Unresolved("function signature", loc)
}
// A function that NAMES its type means that declaration, not whichever
// like-shaped one interned first.
m.funcs.push(
match typ {
Some(n) => self.type_index_of(n, loc)
// No name: the signature is matched against the emitted section, the
// same way a block's is, because that is where the difference between
// two like-interned declarations survives.
None =>
match sign {
Some(s) => self.functype_of(s, loc)
None => emitted_type_index(entry.0.to_int_for_tests_only())
}
},
)
// The index this definition TAKES, not the one its name resolves to.
// Under conditional compilation two branches may declare one name, and
// the name table has a single answer for both -- so the second would go
// unnamed and the first would be named twice. A definition's index is
// its position after the imports, which is what it always was; asking
// the table for it was a longer way to the same number everywhere else.
let fi = imports_so_far(m, 0) + m.funcs.length() - 1
note_name(m.names.functions, fi, name.name)
note_typeuse(m, OwnerFunc(fi), typ, sign)
let code = self.function_body(entry.0, sign, body.0, body.1, loc)
m.codes.push({ ..code, priority: function_priority(attributes) })
m.text.field_order.push(FFunc(m.codes.length() - 1))
// The local names belong to the function that just closed over them.
if !self.local_names.is_empty() {
m.names.locals[fi] = self.local_names
}
if !self.label_names.is_empty() {
m.names.labels[fi] = self.label_names
}
self.export_of(m, attributes, Func(fi), name)
// `#[start]` names the function that runs at instantiation. A module has
// at most one, which the attribute pass has already checked.
for a in attributes {
if a.attr_name == "start" {
m.start = Some(fi)
// A GUARDED start stands where its attribute does, under its
// condition -- the same shape a guarded export takes, and for the
// same reason: the condition is on the start, not on the function.
if a.attr_guard is Some(g) {
self.entry_locs[m.text.field_order.length()] = a.attr_span
m.text.conditionals.push({
cond: wat_cond(g.desc),
loc: a.attr_span,
then_: a.attr_span,
else_: None,
})
}
m.text.field_order.push(FStart(fi))
}
}
}
Global(name~, mut_~, typ~, def~, attributes~) => {
// The WRITTEN type when there is one. A global's initialiser may have a
// narrower type than the global itself -- `let g: &?func = f;` holds a
// funcref, not the type of `f` -- and the declaration is what the module
// states.
let ty = match typ {
Some(t) => self.valtype_index(t, loc)
None =>
match node_valtype(def) {
Some(t) => t
None => raise Unresolved("global type", loc)
}
}
let (init, init_spans) = self.const_expr(def)
let gi = imports_so_far(m, 3) + m.globals.length()
note_name(m.names.globals, gi, name.name)
m.globals.push({ type_: { mut_, typ: ty }, init, init_spans })
m.text.field_order.push(FGlobal(m.globals.length() - 1))
self.export_of(m, attributes, Global(gi), name)
}
Memory(
name~,
address_type~,
limits~,
page_size_log2~,
shared~,
data~,
attributes~
) => {
// Unwritten limits are DERIVED from the inline data: the memory has to be
// big enough to hold what is put in it, and at least one page even when
// nothing is -- a zero-page memory is legal but is not what "size derived
// from the data" means, and instantiating against it traps.
let (mi, ma) = match limits {
Some(l) => l
None => (self.derived_pages(data, page_size_log2, loc), None)
}
m.memories.push({
limits: { mi, ma, address_type, page_size_log2, shared },
})
m.text.field_order.push(FMemory(m.memories.length() - 1))
let mem = self.memory_index(name, loc)
note_name(m.names.memories, mem, name.name)
// An inline data segment is an ACTIVE segment of this memory, written
// where the memory is rather than beside it.
for d in data {
let (offset, offset_spans) = self.const_expr(d.offset)
// An inline segment may still be NAMED, and the name belongs to the
// index it is about to take.
if d.data_name is Some(n) {
note_name(m.names.data, m.datas.length(), n.name)
}
// An inline segment is still a `(data ..)` FIELD in the text, written
// after the memory it was written inside.
m.text.field_order.push(FData(m.datas.length()))
m.datas.push({
mode: Active(mem, offset),
init: data_bytes(d.init, loc),
spelling: data_spelling(d.init),
offset_spans,
})
}
self.export_of(m, attributes, Memory(mem), name)
}
Table(name~, address_type~, reftype~, limits~, init~, attributes~) => {
let (mi, ma) = limits.unwrap_or((0UL, None))
let elem_type = self.reftype_index(reftype, loc)
let init_ = match init {
Some(e) => Some(self.const_expr(e))
None => None
}
let (init_body, init_spans) = match init_ {
Some((b, sp)) => (Some(b), sp)
None => (None, [])
}
note_name(m.names.tables, self.table_index(name, loc), name.name)
m.text.field_order.push(FTable(m.tables.length()))
m.tables.push({
type_: {
elem_type,
limits: { mi, ma, address_type, page_size_log2: None, shared: false },
},
init: init_body,
init_spans,
})
self.export_of(m, attributes, Table(self.table_index(name, loc)), name)
}
Tag(name~, typ~, sign~, attributes~) => {
note_name(m.names.tags, self.tag_index(name, loc), name.name)
note_typeuse(m, OwnerTag(self.tag_index(name, loc)), typ, sign)
note_param_names(m, OwnerTag(self.tag_index(name, loc)), sign)
// A tag that NAMES its type means that declaration, not whichever
// like-shaped one the signature search happens to reach first.
m.text.field_order.push(FTag(m.tags.length()))
m.tags.push({
type_idx: match typ {
Some(n) => self.type_index_of(n, loc)
None => self.tag_type_index(name, loc)
},
})
self.export_of(m, attributes, Tag(self.tag_index(name, loc)), name)
}
Elem(name~, reftype~, mode~, init~, ..) => {
let mut offset_spans : Array[@wasm_bin.Span] = []
let mode_ = match mode {
EPassive => @wasm_bin.ElemMode::Passive
EActive(tab, offset) => {
let (out, spans) = self.const_expr(offset)
offset_spans = spans
Active(self.table_index(tab, loc), out)
}
}
// Each element is its own constant expression, which is what lets a
// segment mix `ref.func` and `ref.null`.
let inits : Array[Array[@wasm_bin.Instruction]] = []
let init_spans : Array[Array[@wasm_bin.Span]] = []
for e in init {
let (out, spans) = self.const_expr(e)
inits.push(out)
init_spans.push(spans)
}
note_name(m.names.elem, m.elems.length(), name.name)
m.text.field_order.push(FElem(m.elems.length()))
m.elems.push({
mode: mode_,
type_: self.reftype_index(reftype, loc),
init: inits,
init_spans,
offset_spans,
})
}
Data(name~, mode~, init~, ..) => {
let mut offset_spans : Array[@wasm_bin.Span] = []
let mode_ = match mode {
Passive => @wasm_bin.DataMode::Passive
Active(mem, offset) => {
let (out, spans) = self.const_expr(offset)
offset_spans = spans
Active(self.memory_index(mem, loc), out)
}
}
// A data segment's name belongs to the index it is about to take, which
// is the length before the push -- the same reasoning the element
// segments use, and the reason both are recorded here rather than in the
// declaration pass: a segment has no declaration of its own.
if name is Some(n) {
note_name(m.names.data, m.datas.length(), n.name)
}
m.text.field_order.push(FData(m.datas.length()))
m.datas.push({
mode: mode_,
init: data_bytes(init, loc),
spelling: data_spelling(init),
offset_spans,
})
}
// Nothing to EMIT: the type section comes from the STORE, which interned
// every declaration as it was made, so a `type` field has already had its
// effect by the time the lowering runs. Its POSITION is still recorded,
// because the text format writes the field where the source put it and the
// section says nothing about that.
Type(decls) => {
let placed : Array[Int] = []
for d in decls {
let ti = self.type_index_of(d.desc.0, d.info)
placed.push(ti)
// A declared function type may NAME its parameters, and the type
// section has nowhere to put a name.
if d.desc.1.typ is Func(ft) {
note_param_names(m, OwnerType(ti), Some(ft))
}
}
m.text.field_order.push(FTypes(placed))
}
// A module annotation carries feature and name metadata the checker
// consumed, not code -- except for the declared features, which the text
// format keeps. They are collected before the walk, not here, because the
// reference writes them ALL first rather than where they stood.
ModuleAnnotation(_) => ()
_ => raise NotLowered(field_name(field.desc), loc)
}
}
///|
/// One function's locals and body.
///
/// Wasm numbers locals with the PARAMETERS first, then the declared ones in
/// order -- so the map is built by walking the same way, and a `local.get` needs
/// no arithmetic to find its slot.
fn Lowering::function_body(
self : Lowering,
type_id : @type_store.Id,
sign : @ast.FuncType?,
label : @ast.Ident?,
instrs : Array[@ast.Instr[@typing_env.InferredAnnotation]],
loc : @basic.Location,
) -> @wasm_bin.FunctionCode raise LowerError {
self.locals = Map([])
self.local_types = []
self.spans = []
// A FRESH array, not a cleared one: the code built for the previous function
// holds the array itself, so emptying it in place empties that function's
// record too -- and every function ended up with the last one's.
self.nested_spans = []
self.binding_slots = Map([])
self.claimed = Map([])
self.local_names = Map([])
self.label_names = Map([])
self.label_counter = 0
self.labels = []
self.body_conditionals = []
self.in_body = true
// A function that WROTE its parameters has them right there, and that is the
// only answer that is certainly this function's. The checker's table is
// keyed by name alone, so under conditional compilation -- where two
// branches may declare one name with different signatures -- the store can
// hand back the other branch's.
//
// Only a function that named a type instead of writing a signature has to
// ask the store, and then there are no names to bind anyway.
guard sign is Some(sign) else {
let sub = self.store.subtyping_info().get_subtype(type_id)
guard sub.typ is Func(ft) else { raise Unresolved("function type", loc) }
if ft.params.length() > 0 {
raise Unresolved("parameter names", loc)
}
return {
locals: [],
body: self.lower_body(label, instrs),
spans: self.spans,
nested_spans: self.nested_spans,
conditionals: self.body_conditionals,
priority: None,
}
}
for k, p in sign.params {
if p.desc.0 is Some(name) {
self.locals[name.name] = k
note_name(self.local_names, k, self.claim_name(name.name))
}
}
// Then the declared locals, numbered after the parameters in the order they
// are written -- which is how wasm numbers them, so no arithmetic is needed
// at any `local.get`.
self.collect_locals(instrs, sign.params.length())
let body = self.lower_body(label, instrs)
self.in_body = false
{
locals: self.local_types,
body,
spans: self.spans,
nested_spans: self.nested_spans,
conditionals: self.body_conditionals,
priority: None,
}
}
///|
/// Record the slot a binding took, refusing to take one twice.
///
/// `binding_slots` is keyed by where the name was written and is rebuilt per
/// function, so within one function two bindings sharing a key is not a case to
/// resolve -- it is a caller that built its identifiers without spans. Silently
/// overwriting the entry costs nothing here and produces a module whose locals
/// are all one slot, which is the failure this refuses to emit. See
/// `AmbiguousBinding`.
fn Lowering::claim_slot(
self : Lowering,
name : @ast.Ident,
slot : Int,
loc : @basic.Location,
) -> Unit raise LowerError {
let key = name.loc.start.cnum
guard !self.binding_slots.contains(key) else {
raise AmbiguousBinding(name.name, loc)
}
self.binding_slots[key] = slot
}
///|
/// Assign a slot to every `let` binding in a body, in source order.
fn Lowering::collect_locals(
self : Lowering,
instrs : Array[@ast.Instr[@typing_env.InferredAnnotation]],
start : Int,
) -> Unit raise LowerError {
let mut next = start
fn visit(
i : @ast.Instr[@typing_env.InferredAnnotation],
) -> Unit raise LowerError {
if i.desc is Let(bindings, init) {
// Numbered in the order the `local.set`s HAPPEN, which is right to left:
// the values are on the stack with the last on top, so the last binding
// is written first and takes the lower slot. Numbering them as written
// would name the same locals in the other order.
for j in 0.. Some(self.valtype_index(written, i.info.1))
None =>
match init {
Some(e) =>
if e.info.0.length() > k &&
@typing_env.standalone_valtype(e.info.0[k]) is Some(v) {
Some(lower_valtype(v.internal))
} else {
None
}
None => None
}
}
guard ty is Some(ty) else { raise Unresolved("local type", i.info.1) }
// The slot is recorded against the BINDING, not the name: a shadowing
// `let` must not be in scope for its own initializer, and only the
// lowering knows when that moment has passed.
self.claim_slot(name, next, i.info.1)
note_name(self.local_names, next, self.claim_name(name.name))
self.local_types.push(ty)
next = next + 1
}
}
// A `match` arm BINDS the value it matched, and the binding is written in
// the pattern rather than as a `let` -- so the generic walk below never
// sees it. Its slot has to be assigned where the arm's own lowering puts
// the store: before the arm's body, and after the previous arm's.
if i.desc is Match(arms~, default~, scrutinee~) {
// The arms' blocks NEST, the last one outermost, so the store for the
// last arm's binding is the first one reached -- and slots are numbered
// in the order the stores happen. Walking the arms forwards would number
// them the other way round and name every local wrong.
for k = arms.length() - 1; k >= 0; k = k - 1 {
if arms[k].0 is MatchCast(Some(bind), rt) {
let ty = self.valtype_index(Ref(rt), i.info.1)
self.claim_slot(bind, next, i.info.1)
note_name(self.local_names, next, self.claim_name(bind.name))
self.local_types.push(ty)
next = next + 1
}
}
// The scrutinee sits at the BOTTOM of the test chain, inside the
// innermost block, so anything it binds comes after every arm's binding.
visit(scrutinee)
for arm in arms {
for s in arm.1.desc {
visit(s)
}
}
for s in default.desc {
visit(s)
}
return
}
for sub in i.sub_instrs() {
visit(sub)
}
}
for s in instrs {
visit(s)
}
}
///|
/// The body's instructions, under the function's own label.
fn Lowering::lower_body(
self : Lowering,
label : @ast.Ident?,
instrs : Array[@ast.Instr[@typing_env.InferredAnnotation]],
) -> Array[@wasm_bin.Instruction] raise LowerError {
let body : Array[@wasm_bin.Instruction] = []
// The function's own frame is pushed for DEPTH but takes no label index: the
// name section numbers the blocks a body contains, and the body is not one of
// them.
self.labels.push(label.map(l => l.name))
for s in instrs {
self.instr(body, s)
}
body
}
///|
/// Lower a constant expression, keeping its spans to itself.
///
/// A const expression is its own body with its own indices, so its spans
/// cannot go in the enclosing list -- the function being lowered, or the last
/// one lowered before this field. They did, and an element segment's four
/// initialisers left four spans behind that the preceding function's operands
/// then matched against, folding two of them into each other.
fn Lowering::const_expr(
self : Lowering,
e : @ast.Instr[@typing_env.InferredAnnotation],
) -> (Array[@wasm_bin.Instruction], Array[@wasm_bin.Span]) raise LowerError {
let outer = self.spans
self.spans = []
let out : Array[@wasm_bin.Instruction] = []
self.instr(out, e)
let spans = self.spans
self.spans = outer
(out, spans)
}
///|
/// Record an `#[export]` on a field.
fn Lowering::export_of(
self : Lowering,
m : @wasm_bin.Module,
attributes : Array[@ast.Attribute],
desc : @wasm_bin.ExportDesc,
own : @ast.Ident,
) -> Unit raise LowerError {
for a in attributes {
guard a.attr_name == "export" else { continue }
let name = match a.attr_value {
Some(v) =>
match v.desc {
Str(_, b) => b
_ => raise NotLowered("computed export name", v.info)
}
// A bare `#[export]` exports under the entity's OWN name, which is the
// whole point of writing it bare.
None => utf8_of(own.name)
}
let at = m.exports.length()
m.exports.push({ name, desc })
// A GUARDED export is a field of its own under its condition. A clause on
// the thing exported has nowhere to put one.
if a.attr_guard is Some(g) {
m.text.standalone_exports[at] = true
self.entry_locs[m.text.field_order.length()] = a.attr_span
m.text.field_order.push(FExport(at))
m.text.conditionals.push({
cond: wat_cond(g.desc),
loc: a.attr_span,
then_: a.attr_span,
else_: None,
})
}
}
}
///|
/// A name as the UTF-8 bytes the format stores.
fn utf8_of(s : String) -> Bytes {
Bytes::from_array(@unicode.utf8_bytes(s).map(b => b.to_byte()))
}
///|
fn Lowering::func_index(
self : Lowering,
name : @ast.Ident,
loc : @basic.Location,
) -> Int raise LowerError {
match self.resolve(self.indices.funcs, name.name, loc) {
Some(i) => i
None => raise Unresolved("function index", loc)
}
}
///|
/// The declaration of `name` a reference at `loc` can see.
///
/// Conditional branches make one name several declarations, and which one is
/// meant is decided by where the reference stands.
fn Lowering::resolve(
self : Lowering,
space : Space,
name : String,
loc : @basic.Location,
) -> Int? {
space.resolve(name, guards_at(self.conditionals, loc), d => {
guards_at(self.conditionals, d)
})
}
///|
/// The conditional frames a source position stands inside, outermost first.
fn guards_at(
conds : Array[@wasm_bin.CondGroup],
loc : @basic.Location,
) -> Array[(Int, Bool)] {
fn covers(outer : @basic.Location, inner : @basic.Location) -> Bool {
inner.start.cnum >= outer.start.cnum && inner.end.cnum <= outer.end.cnum
}
let out : Array[(Int, Bool)] = []
for k, c in conds {
if covers(c.then_, loc) {
out.push((k, true))
} else if c.else_ is Some(e) && covers(e, loc) {
out.push((k, false))
}
}
out.sort_by((a, b) => conds[a.0].loc.start.cnum - conds[b.0].loc.start.cnum)
out
}
///|
/// One imported declaration.
///
/// The import section is where an index space STARTS, so an import that is not
/// emitted does not merely go missing -- it shifts every definition after it,
/// and a `call` compiled against the wax numbering then names the wrong
/// function. Nothing here is optional for that reason.
fn Lowering::import_(
self : Lowering,
m : @wasm_bin.Module,
module_ : Bytes,
decl : @ast.ImportDecl,
loc : @basic.Location,
) -> Unit raise LowerError {
// The import's own name in the other module: `#[import = "nm"]` overrides it,
// and otherwise it is the name this module knows it by.
let mut name = utf8_of(decl.id.name)
for a in decl.attributes {
if a.attr_name == "import" && a.attr_value is Some(v) && v.desc is Str(_, b) {
name = b
}
}
let desc : @wasm_bin.ImportDesc = match decl.kind {
Func(exact~, sign~, ..) => {
guard self.ctx.functions.find_no_mark(decl.id.name) is Some(Some(entry)) else {
raise Unresolved("imported function signature", loc)
}
let interned = emitted_type_index(entry.0.to_int_for_tests_only())
// The checker's table is keyed by NAME alone, so two branches of a
// conditional that import one name share an entry and one of them gets
// the other's signature. Only then is the written one preferred: taking
// it always changes which imports a compact group can share a descriptor
// with, and that is encoded.
let ti = match sign {
Some(fsig) if !self.functype_matches(interned, fsig) =>
self.functype_of(fsig, loc)
_ => interned
}
Func(ti, exact)
}
Global(mut_~, typ~) => Global({ mut_, typ: self.valtype_index(typ, loc) })
Memory(address_type~, limits~, page_size_log2~, shared~) => {
let (mi, ma) = limits.unwrap_or((0UL, None))
Memory({ limits: { mi, ma, address_type, page_size_log2, shared } })
}
Table(address_type~, reftype~, limits~) => {
let (mi, ma) = limits.unwrap_or((0UL, None))
Table({
elem_type: self.reftype_index(reftype, loc),
limits: { mi, ma, address_type, page_size_log2: None, shared: false },
})
}
// An imported tag that NAMES its type means that declaration, exactly as a
// defined one does.
Tag(typ~, ..) =>
Tag(
match typ {
Some(n) => self.type_index_of(n, loc)
None => self.tag_type_index(decl.id, loc)
},
)
}
// The index this import TAKES, which is its position among the imports of
// its own kind. Not what its name resolves to: two branches of a conditional
// may import one name, and the name table has a single answer for both.
let at = imports_so_far(m, import_kind(desc))
m.imports.push({ mod_name: module_, name, desc, group: None })
match decl.kind {
Func(typ~, sign~, ..) => {
note_name(m.names.functions, at, decl.id.name)
note_param_names(m, OwnerFunc(at), sign)
note_typeuse(m, OwnerFunc(at), typ, sign)
}
Global(..) => note_name(m.names.globals, at, decl.id.name)
Memory(..) => note_name(m.names.memories, at, decl.id.name)
Table(..) => note_name(m.names.tables, at, decl.id.name)
Tag(typ~, sign~) => {
note_name(m.names.tags, at, decl.id.name)
note_typeuse(m, OwnerTag(at), typ, sign)
note_param_names(m, OwnerTag(at), sign)
}
}
// An import may be re-exported, and then it is exported under the name THIS
// module gave it rather than the one it was imported as.
let export_desc : @wasm_bin.ExportDesc = match decl.kind {
Func(..) => Func(at)
Global(..) => Global(at)
Memory(..) => Memory(at)
Table(..) => Table(at)
Tag(..) => Tag(self.tag_index(decl.id, loc))
}
self.export_of(m, decl.attributes, export_desc, decl.id)
// An IMPORTED function can be the start function too: `#[start]` names the
// function that runs at instantiation, and nothing says it has to be one this
// module defines.
if decl.kind is Func(..) {
for a in decl.attributes {
if a.attr_name == "start" {
let fi = self.func_index(decl.id, loc)
m.start = Some(fi)
// The FIELD entry is pushed by the caller, after the import's own --
// an import is written before the start it names.
self.pending_start = Some((fi, a))
}
}
}
}
///|
/// A source value type with its type names resolved to indices.
fn Lowering::valtype_index(
self : Lowering,
v : @wasm_types.ValType[@ast.Ident],
loc : @basic.Location,
) -> @wasm_types.ValType[Int] raise LowerError {
match v {
I32 => I32
I64 => I64
F32 => F32
F64 => F64
V128 => V128
Ref(r) => Ref(self.reftype_index(r, loc))
}
}
///|
/// The type index a tag's signature interned to.
///
/// A tag names a function type, and the store interned it structurally -- so
/// the lookup is BY SHAPE, exactly as a block's is. A tag whose inline
/// signature matches nothing in the store is one the checker never interned,
/// and there is no index to name it by.
fn Lowering::tag_type_index(
self : Lowering,
name : @ast.Ident,
loc : @basic.Location,
) -> Int raise LowerError {
guard self.ctx.tags.find_no_mark(name.name) is Some(ft) else {
raise Unresolved("tag signature", loc)
}
let params : Array[@wasm_types.ValType[Int]] = []
for p in ft.params {
params.push(self.valtype_index(p.desc.1, loc))
}
let results : Array[@wasm_types.ValType[Int]] = []
for r in ft.results {
results.push(self.valtype_index(r, loc))
}
guard self.functypes.get((params, results)) is Some(i) else {
raise Unresolved("tag type", loc)
}
i
}
///|
fn Lowering::tag_index(
self : Lowering,
name : @ast.Ident,
loc : @basic.Location,
) -> Int raise LowerError {
match self.indices.tags.get(name.name) {
Some(i) => i
None => raise Unresolved("tag index", loc)
}
}
///|
fn Lowering::memory_index(
self : Lowering,
name : @ast.Ident,
loc : @basic.Location,
) -> Int raise LowerError {
match self.indices.memories.get(name.name) {
Some(i) => i
None => raise Unresolved("memory index", loc)
}
}
///|
fn Lowering::table_index(
self : Lowering,
name : @ast.Ident,
loc : @basic.Location,
) -> Int raise LowerError {
match self.indices.tables.get(name.name) {
Some(i) => i
None => raise Unresolved("table index", loc)
}
}
///|
/// A source reference type with its type names resolved to indices.
fn Lowering::reftype_index(
self : Lowering,
r : @wasm_types.RefType[@ast.Ident],
loc : @basic.Location,
) -> @wasm_types.RefType[Int] raise LowerError {
let typ = match r.typ {
Type(n) | Exact(n) => {
let i = self.type_index_of(n, loc)
if r.typ is Exact(_) {
@wasm_types.HeapType::Exact(i)
} else {
Type(i)
}
}
Func => Func
NoFunc => NoFunc
Exn => Exn
NoExn => NoExn
Cont => Cont
NoCont => NoCont
Extern => Extern
NoExtern => NoExtern
Any => Any
Eq => Eq
I31 => I31
Struct => Struct
Array => Array
None_ => None_
}
{ nullable: r.nullable, typ }
}
///|
/// A data segment's contents as the bytes the format stores.
fn data_bytes(
init : Array[@ast.DataElem],
loc : @basic.Location,
) -> Bytes raise LowerError {
let out : Array[Byte] = []
for e in init {
match e {
Str(b) =>
for k in 0.. {
let width = match ty {
Packed(I8) => 1
Packed(I16) => 2
Value(I32) | Value(F32) => 4
Value(I64) | Value(F64) => 8
_ => raise NotLowered("data run element type", loc)
}
for item in items {
let bits = match ty {
Value(F32) =>
parse_f32(item.desc, loc).reinterpret_as_int().to_int64() &
0xFFFFFFFFL
Value(F64) =>
parse_float_bits(item.desc, true, loc).reinterpret_as_int64()
_ => parse_i64(item.desc, loc)
}
for b in 0..> (b * 8)).to_int() & 0xFF).to_byte(),
)
}
}
}
// A vector run is the same, sixteen bytes at a time, and each element
// carries its own shape: `[v128: i32x4(..), f64x2(..)]` is one segment
// of two vectors written two different ways.
V128Run(vs) =>
for v in vs {
let shape = match v.desc.shape {
I8x16 => @simd.Shape::I8x16
I16x8 => @simd.Shape::I16x8
I32x4 => @simd.Shape::I32x4
I64x2 => @simd.Shape::I64x2
F32x4 => @simd.Shape::F32x4
F64x2 => @simd.Shape::F64x2
}
let bytes = vector_bytes(shape, v.desc.components, loc)
for k in 0.. Array[@wasm_bin.DataPiece] {
let out : Array[@wasm_bin.DataPiece] = []
for e in init {
match e {
Str(b) => out.push(PieceStr(b))
Run(ty, items) => {
let kw = match ty {
Packed(I8) => "i8"
Packed(I16) => "i16"
Value(I32) => "i32"
Value(I64) => "i64"
Value(F32) => "f32"
Value(F64) => "f64"
_ => "i8"
}
out.push(PieceRun(kw, items.map(i => i.desc)))
}
V128Run(vs) => {
let items : Array[String] = []
for v in vs {
items.push(
match v.desc.shape {
I8x16 => "i8x16"
I16x8 => "i16x8"
I32x4 => "i32x4"
I64x2 => "i64x2"
F32x4 => "f32x4"
F64x2 => "f64x2"
},
)
for c in v.desc.components {
items.push(c)
}
}
out.push(PieceRun("v128", items))
}
}
}
out
}
///|
/// Note which of the two type clauses a declaration wrote. Both are optional
/// and independent, and the index the binary keeps says nothing about either.
fn note_typeuse(
m : @wasm_bin.Module,
owner : @wasm_bin.ParamOwner,
typ : @ast.Ident?,
sign : @ast.FuncType?,
) -> Unit {
m.text.decl_typeuse[owner] = {
named: typ is Some(_),
spelled: sign is Some(_),
}
}
///|
/// Note the parameter names a declaration wrote, when it wrote any.
///
/// Only the declarations whose names the binary drops: a defined function's
/// parameters are locals and the name section carries them already.
fn note_param_names(
m : @wasm_bin.Module,
owner : @wasm_bin.ParamOwner,
sign : @ast.FuncType?,
) -> Unit {
guard sign is Some(ft) else { return }
let names : Map[Int, Bytes] = Map([])
for k, p in ft.params {
if p.desc.0 is Some(id) {
names[k] = utf8_of(id.name)
}
}
if !names.is_empty() {
m.text.decl_param_names[owner] = names
}
}
///|
/// A readable name for a module field, for the "not lowered" report.
fn field_name(f : @ast.ModuleField[@typing_env.InferredAnnotation]) -> String {
match f {
Func(..) => "function"
Global(..) => "global"
Type(_) => "type"
Tag(..) => "tag"
Memory(..) => "memory"
Table(..) => "table"
Data(..) => "data segment"
Elem(..) => "element segment"
Import(..) | ImportGroup(..) => "import"
ModuleAnnotation(_) => "module annotation"
Conditional(..) => "conditional"
}
}
///|
/// The declarative element segment that makes the module's funcrefs valid.
///
/// `ref.func` is only accepted on a function the module has DECLARED. A
/// reference in a global initializer or an element segment already declares
/// it; one inside a body declares nothing, so those are gathered into a single
/// segment emitted once at module level.
///
/// A module with `#[if]` fields gets none. A referenced function may itself be
/// conditionally defined, so the segment would name an index that is absent
/// under some configuration -- and a segment that is wrong under one
/// configuration is worse than none, since the conditional output is wax-only
/// text anyway and `-D` resolves it before this ever matters.
fn Lowering::declare_func_refs(
self : Lowering,
m : @wasm_bin.Module,
source : @ast.Module[@basic.Location],
) -> Unit {
for field in source {
if field.desc is Conditional(..) {
return
}
}
// LAST reference first, which matches the reference on more modules than
// first-seen does but not on all of them: its own order is a hash table's
// iteration order, which is neither. Reproducing that exactly would mean
// reimplementing OCaml's string hashing and bucket layout, and the segment's
// order is observable, so a handful of modules stay different for this
// reason alone.
//
// Re-measured after the text printer landed, since it reads this order too:
// first-seen costs 3 files their bytes and 3 more their text.
let init : Array[Array[@wasm_bin.Instruction]] = []
for k = self.func_refs_in_body.length() - 1; k >= 0; k = k - 1 {
let name = self.func_refs_in_body[k]
if self.func_refs_outside.contains(name) {
continue
}
guard self.indices.funcs.get(name) is Some(f) else { continue }
init.push([RefFunc(f)])
}
if !init.is_empty() {
m.elems.push({
mode: Declarative,
type_: { nullable: false, typ: Func },
init,
// One `ref.func` each, so each folds to itself.
init_spans: init.map(_ => {
[{ start: 0, head: 0, end: 1, loc: @basic.dummy_loc }]
}),
offset_spans: [],
})
}
}
///|
/// Claim a wasm-level name for this function, renaming on a collision.
///
/// Wasm gives a function one flat namespace, and wax lets a `let` shadow. The
/// second `x` therefore becomes `x_2`, the third `x_3`, and the count is kept
/// against the BASE so the numbering continues rather than restarting -- which
/// is what stops `x_2` itself from colliding with a written `x_2`.
fn Lowering::claim_name(self : Lowering, base : String) -> String {
match self.claimed.get(base) {
None => {
self.claimed[base] = 1
base
}
Some(n) => {
let mut k = n + 1
while self.claimed.contains(base + "_" + k.to_string()) {
k = k + 1
}
let name = base + "_" + k.to_string()
self.claimed[name] = 1
self.claimed[base] = k
name
}
}
}
///|
/// The minimum page count an inline data list forces.
///
/// The extent is the highest `offset + length` over the ACTIVE segments, and
/// the count is that divided by the page size, rounded up -- with a floor of
/// one page, which is what a memory whose size is "derived from its data"
/// means even when it has none.
fn Lowering::derived_pages(
self : Lowering,
data : Array[@ast.MemData[@typing_env.InferredAnnotation]],
page_size_log2 : Int?,
loc : @basic.Location,
) -> UInt64 raise LowerError {
ignore(self)
let page_size = 1UL << page_size_log2.unwrap_or(16)
let mut extent = 0UL
for d in data {
guard d.offset.desc is Int(s) else { continue }
let off = parse_i64(s, loc).reinterpret_as_uint64()
let len = data_bytes(d.init, loc).length().to_uint64()
let end = off + len
// Running past the end of the address space is not a small memory: the
// wrap would underestimate it, so treat it as maximal.
let end = if end < off { 0xFFFFFFFFFFFFFFFFUL } else { end }
if end > extent {
extent = end
}
}
let pages = extent / page_size +
(if extent % page_size == 0UL { 0UL } else { 1UL })
if pages < 1UL {
1UL
} else {
pages
}
}
///|
/// Fold the last `n` imports into one compact group entry.
///
/// `import "m" { .. }` is one block in the source and, under the
/// compact-import-section feature, one entry in the binary: the module name is
/// written once. Each item still claims its own index, so the entries are built
/// individually first and collapsed here -- which also means a module without
/// the feature simply skips this step and keeps them.
///
/// The homogeneous form shares a single descriptor and lists only names. It is
/// available exactly when every item's descriptor is EQUAL, so sharing loses
/// nothing -- not merely when they are the same kind, which would drop the
/// differences.
fn Lowering::compact_group(
self : Lowering,
m : @wasm_bin.Module,
n : Int,
loc : @basic.Location,
) -> Unit {
ignore(loc)
guard n >= 2 else { return }
guard self.ctx.type_context.features.is_enabled(CompactImportSection) else {
return
}
let start = m.imports.length() - n
guard start >= 0 else { return }
let items = m.imports[start:].to_owned()
let head = items[0]
let homogeneous = items.iter().all(i => i.desc == head.desc)
let group : @wasm_bin.ImportGroup = if homogeneous {
Homogeneous(items.map(i => i.name))
} else {
Heterogeneous(items.map(i => (i.name, i.desc)))
}
for _ in 0..