// Lowering instructions.
//
// Ported from the instruction cases of wax/src/lib-conversion/to_wasm.ml.
//
// One wax instruction becomes a SEQUENCE of wasm instructions, because wax is an
// expression language and wasm is a stack machine: `a + b` emits `a`, then `b`,
// then the add. So every case here appends, and the order it appends in is the
// evaluation order the checker validated.

///|
/// Lower one instruction, appending to `out`.
pub fn Lowering::instr(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  i : @ast.Instr[@typing_env.InferredAnnotation],
) -> Unit raise LowerError {
  let span_start = out.length()
  // The children lower through this same function, so each pushes its span
  // before this node pushes its own. Where the LAST of them ended is where
  // this node's own emission began -- which is the one thing a plain extent
  // cannot say, and the thing the text form needs.
  //
  // A node that emits nothing of its own -- a tuple, say, which is just its
  // elements -- has that boundary at the end. It is not a folded instruction,
  // and the printer must not make one of it by taking the last element as a
  // head that consumes the rest.
  let child_spans = self.spans.length()
  self.instr_span(out, i)
  if out.length() > span_start {
    let mut head_start = span_start
    for k in child_spans.. head_start {
        head_start = self.spans[k].end
      }
    }
    self.spans.push({
      start: span_start,
      head: head_start,
      end: out.length(),
      loc: i.info.1,
    })
  }
}

///|
/// Lower into an array of its own, keeping the spans it produced.
///
/// Some nodes have to see what they emitted before deciding what to emit --
/// a tail call replaces the call it ends with, a hinted instruction wraps it
/// -- and that needs a scratch array. The spans come back numbered from zero,
/// so the caller moves them to where the instructions land.
fn Lowering::detached(
  self : Lowering,
  build : (Array[@wasm_bin.Instruction]) -> Unit raise LowerError,
) -> (Array[@wasm_bin.Instruction], Array[@wasm_bin.Span]) raise LowerError {
  let outer = self.spans
  self.spans = []
  let scratch : Array[@wasm_bin.Instruction] = []
  build(scratch)
  let spans = self.spans
  self.spans = outer
  (scratch, spans)
}

///|
/// Record spans lowered elsewhere, at the offset their instructions landed at.
fn Lowering::rebase(
  self : Lowering,
  spans : Array[@wasm_bin.Span],
  base : Int,
) -> Unit {
  for sp in spans {
    self.spans.push({
      ..sp,
      start: sp.start + base,
      head: sp.head + base,
      end: sp.end + base,
    })
  }
}

///|
/// One character as the bytes that spell it.
fn utf8_of_char(c : Char) -> Bytes {
  Bytes::from_array(@unicode.utf8_bytes(c.to_string()).map(b => b.to_byte()))
}

///|
/// Read a name, recording the one-instruction span it takes.
fn Lowering::read_span(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  name : @ast.Ident,
  loc : @basic.Location,
) -> Unit raise LowerError {
  let at = out.length()
  out.push(self.read(name, loc))
  self.spans.push({ start: at, head: at, end: at + 1, loc })
}

///|
/// Lower one instruction, without recording where it landed.
fn Lowering::instr_span(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  i : @ast.Instr[@typing_env.InferredAnnotation],
) -> Unit raise LowerError {
  // A branch hint attaches to the instruction it annotates, and the format
  // records it by BYTE OFFSET in a custom section -- so it has to be carried
  // on the instruction until the encoder knows where that instruction landed.
  // The wrapper is how it rides along.
  guard i.hints.is_empty() else {
    // Lowered into an array of its own so the last instruction can be picked
    // out, which means its spans are numbered from zero and have to be moved
    // to where the instructions actually land. Left unmoved they point into
    // whatever else is in this body, and the printer folds by them.
    let base = out.length()
    let (inner, inner_spans) = self.detached(scratch => self.hinted(scratch, i))
    guard inner.length() >= 1 else { return }
    // The hint belongs to the LAST instruction emitted, which is the one the
    // node is: everything before it computed the operands.
    for k in 0..<(inner.length() - 1) {
      out.push(inner[k])
    }
    out.push(Hinted(self.lower_hints(i.hints), inner[inner.length() - 1]))
    self.rebase(inner_spans, base)
    return
  }
  self.hinted(out, i)
}

///|
/// The hints an instruction carries, in the form the encoder writes.
fn Lowering::lower_hints(
  self : Lowering,
  h : @ast.Hints,
) -> @wasm_bin.InstrHints {
  {
    branch: h.branch.map(x => x.value),
    freq: h.freq.map(x => x.value),
    // A call-target hint names FUNCTIONS, and the section stores indices, so
    // it is the one hint that needs the index space resolved. A name that does
    // not resolve drops that entry rather than the whole hint: the section is
    // advisory, and a partial list is still true.
    targets: h.targets.map(x => {
      let out : Array[(Int, Int)] = []
      for entry in x.value {
        if self.indices.funcs.get(entry.0.name) is Some(f) {
          out.push((f, entry.1))
        }
      }
      out
    }),
  }
}

///|
/// Lower one instruction, appending to `out`.
fn Lowering::hinted(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  i : @ast.Instr[@typing_env.InferredAnnotation],
) -> Unit raise LowerError {
  let loc = i.info.1
  match i.desc {
    // A hole emits NOTHING. `_` means "the value already on the stack", put
    // there by an earlier statement -- so the instruction that consumes it
    // simply finds it, and there is nothing for the hole itself to push. The
    // checker has already validated that the value is there and has the right
    // type; that is the whole content of the hole machinery.
    Hole => ()
    Nop => out.push(Nop)
    Unreachable => out.push(Unreachable)
    // The literal's TEXT rides along with its value. `0x4` and `4` denote the
    // same i32 and are two different programs to read, and no reading of a
    // float's bits gives back `0x1.4p+3`.
    Int(s) =>
      match node_valtype(i) {
        Some(I32) => out.push(Spelled(s, I32Const(parse_i32(s, loc))))
        Some(I64) => out.push(Spelled(s, I64Const(parse_i64(s, loc))))
        Some(F32) => out.push(Spelled(s, F32Const(parse_f32(s, loc))))
        Some(F64) =>
          out.push(Spelled(s, F64Const(parse_float_bits(s, true, loc))))
        _ => raise Unresolved("integer literal type", loc)
      }
    Float(s) =>
      match node_valtype(i) {
        Some(F32) => out.push(Spelled(s, F32Const(parse_f32(s, loc))))
        Some(F64) =>
          out.push(Spelled(s, F64Const(parse_float_bits(s, true, loc))))
        _ => raise Unresolved("float literal type", loc)
      }
    // A character literal is an i32 code point, and remembers being written as
    // a character: 65 says nothing about having been `'A'`.
    Char(c) => out.push(FromChar(utf8_of_char(c), I32Const(c.to_int())))
    Get(name) => out.push(self.read(name, loc))
    Set(name, op, value) => {
      // `x op= e` is `x = x op e`: the read comes first, then the operand, then
      // the operator, then the write. The compound form is kept in the typed
      // AST so it round-trips; here it is spelled out.
      match op {
        None => self.instr(out, value)
        Some(binop) => {
          // `x op= e` spells out a tree the source did not write, so the
          // lowering records it: the read and the operand are the operator's
          // children, and the operator is a node of its own. Without that the
          // read has no span and the printer folds it into whatever follows.
          let at = out.length()
          self.read_span(out, name, loc)
          self.instr(out, value)
          guard node_valtype(value) is Some(ty) else {
            raise Unresolved("compound operand type", loc)
          }
          guard binop_instruction(binop.desc, ty) is Some(instruction) else {
            raise NotLowered("compound operator at this type", loc)
          }
          let op_at = out.length()
          out.push(instruction)
          self.spans.push({ start: at, head: op_at, end: op_at + 1, loc })
        }
      }
      out.push(self.write(name, loc))
    }
    Tee(name, value) => {
      self.instr(out, value)
      guard self.locals.get(name.name) is Some(k) else {
        raise Unresolved("tee target", loc)
      }
      out.push(LocalTee(k))
    }
    BinOpI(op, a, b) => {
      self.instr(out, a)
      self.instr(out, b)
      // The OPERANDS' type, not the result's: a comparison yields i32 whatever
      // it compared, so reading the result would pick `i32.lt_s` for a pair of
      // f64s.
      guard node_valtype(a) is Some(ty) else {
        raise Unresolved("operand type", loc)
      }
      // Reference equality is its own instruction, and there is no `ref.ne`:
      // `a != b` on references is the negation of `a == b`.
      if ty is Ref(_) {
        match op.desc {
          Eq => {
            out.push(RefEqInstr)
            return
          }
          Ne => {
            out.push(RefEqInstr)
            out.push(I32Eqz)
            return
          }
          _ => ()
        }
      }
      guard binop_instruction(op.desc, ty) is Some(instruction) else {
        raise NotLowered("operator at this type", loc)
      }
      out.push(instruction)
    }
    UnOpI(op, a) =>
      match op.desc {
        // `+x` is the identity; it exists to be written, not emitted.
        Pos => self.instr(out, a)
        // `-x` is `0 - x` for an integer and a real instruction for a float.
        Neg =>
          match node_valtype(i) {
            // A negated LITERAL folds into the constant, exactly as the code
            // generator folds it -- `-40` is one instruction, not a subtraction
            // from zero. Only a bare literal folds; `--x` is a real
            // subtraction, and the checker has already told them apart.
            Some(I32) =>
              if a.desc is Int(s) {
                // The MINUS is part of the spelling: `-0` and `0` are the same
                // i32 and two different things to read.
                out.push(
                  Spelled("-" + s, I32Const(-parse_i64(s, loc).to_int())),
                )
              } else {
                out.push(I32Const(0))
                self.instr(out, a)
                out.push(I32Sub)
              }
            Some(I64) =>
              if a.desc is Int(s) {
                out.push(Spelled("-" + s, I64Const(-parse_i64(s, loc))))
              } else {
                out.push(I64Const(0))
                self.instr(out, a)
                out.push(I64Sub)
              }
            // A negated float LITERAL folds too, and it has to: `-nan:0x..`
            // is a bit pattern, and reaching it by negating the positive one
            // flips a bit the source named explicitly. Every float negation
            // fits, so unlike the integers there is nothing to check.
            Some(F32) =>
              if a.desc is (Int(_) | Float(_)) {
                let t = literal_text(i, loc)
                out.push(Spelled(t, F32Const(parse_f32(t, loc))))
              } else {
                self.instr(out, a)
                out.push(F32Neg)
              }
            Some(F64) =>
              if a.desc is (Int(_) | Float(_)) {
                let t = literal_text(i, loc)
                out.push(Spelled(t, F64Const(parse_float_bits(t, true, loc))))
              } else {
                self.instr(out, a)
                out.push(F64Neg)
              }
            _ => raise Unresolved("negation type", loc)
          }
        // `!x` is `x == 0` on an integer and `ref.is_null` on a reference --
        // the same question, asked of two different kinds of nothing.
        Not => {
          self.instr(out, a)
          // In UNREACHABLE code there is no operand type, and `i32.eqz` stands
          // in: nothing runs, and the width is all that would have differed.
          match node_valtype(a) {
            Some(I64) => out.push(I64Eqz)
            Some(Ref(_)) => out.push(RefIsNull)
            Some(I32) | None => out.push(I32Eqz)
            _ => raise NotLowered("logical not at this type", loc)
          }
        }
      }
    Sequence(l) =>
      for s in l {
        self.instr(out, s)
      }
    Return(operand) => {
      if operand is Some(e) {
        self.instr(out, e)
      }
      out.push(Return)
    }
    Br(label, operand) => {
      if operand is Some(e) {
        self.instr(out, e)
      }
      out.push(Br(self.depth(label, loc)))
    }
    BrIf(label, operand) => {
      self.instr(out, operand)
      out.push(BrIf(self.depth(label, loc)))
    }
    // A declaration with NO initializer emits nothing at all: it reserves the
    // slot, and the slot is already reserved by the local collection pass.
    // Storing into it would take a value nobody pushed.
    Let(bindings, None) =>
      // Nothing is emitted, but the names still come into scope: a later read
      // of the binding has to find it.
      for b in bindings {
        if b.0 is Some(name) {
          self.bind_local(name, loc)
        }
      }
    Let(bindings, Some(init)) => {
      // The initializer is lowered BEFORE the names come into scope, so `let x
      // = x + 1` reads the outer `x` -- which is what the checker scoped it to.
      self.instr(out, init)
      for b in bindings {
        if b.0 is Some(name) {
          self.bind_local(name, loc)
        }
      }
      // Bound right to left: the values are on the stack with the LAST on top,
      // and each `local.set` takes the top one.
      //
      // SEVERAL bindings take a span each, because each is a statement: they
      // take turns off the same stack rather than nesting over one another,
      // and without the spans the printer reads the run as one tree and writes
      // `(local.set $q (local.set $r (call ...)))`.
      //
      // ONE binding is the opposite: `let x = e;` is `(local.set $x e)`, the
      // set folding over the initialiser, so it stays the node's own emission
      // and takes no span. Spanning it too was measured at 1395 exact to 1087.
      let many = bindings.length() > 1
      for k = bindings.length() - 1; k >= 0; k = k - 1 {
        let at = out.length()
        match bindings[k].0 {
          // An anonymous binding is a drop: `_ = e` computes and discards.
          None => out.push(Drop)
          Some(name) => {
            guard self.locals.get(name.name) is Some(slot) else {
              raise Unresolved("local binding", loc)
            }
            out.push(LocalSet(slot))
          }
        }
        if many {
          self.spans.push({ start: at, head: at, end: at + 1, loc })
        }
      }
    }
    Block(label~, typ~, block~) =>
      out.push(Block(self.block_type(typ, loc), self.body(label, block.desc)))
    Loop(label~, typ~, block~) =>
      out.push(Loop(self.block_type(typ, loc), self.body(label, block.desc)))
    If(label~, typ~, cond~, if_block~, else_block~) => {
      self.instr(out, cond)
      // An `if`/`else` is ONE block in the label space -- a `br` inside either
      // branch counts it once, and it carries one name -- so the label frame
      // is opened once around both bodies rather than once per body.
      self.open_label(label)
      let then_ = self.instrs(if_block.desc)
      // Lowered even when there is none, so that an `if` always records TWO
      // nested span lists. The printer pairs them by count, and an `if` that
      // sometimes records one and sometimes two shifts every list after it.
      let else_ = match else_block {
        Some(b) => self.instrs(b.desc)
        None => self.instrs([])
      }
      let _ = self.labels.pop()
      out.push(If(self.block_type(typ, loc), then_, else_))
    }
    // A tail call lowers exactly like the call it is, and then the trailing
    // CALL becomes its `return_call` form. Reusing the whole call dispatch is
    // the point: `become f(x)` and `become tab[i](x)` are the same two
    // questions the plain forms answer, and answering them twice is how the
    // two spellings drift apart.
    TailCall(callee, args) => {
      // Lowered into an array of its own so the trailing call can be replaced,
      // which numbers its spans from zero -- so they are moved to where the
      // instructions land rather than left behind as a nested body that
      // belongs to nothing.
      let base = out.length()
      let (inner, inner_spans) = self.detached(scratch => {
        self.instr(scratch, { ..i, desc: Call(callee, args) })
      })
      guard inner.length() >= 1 else { return }
      let last = inner[inner.length() - 1]
      let tail : @wasm_bin.Instruction? = match last {
        Call(f) => Some(ReturnCall(f))
        CallIndirect(t, tab) => Some(ReturnCallIndirect(t, tab))
        CallRef(t) => Some(ReturnCallRef(t))
        // An INTRINSIC is not a call and has no tail form: it is evaluated and
        // its result returned, which is what the source asked for anyway.
        _ => None
      }
      for k in 0..<(inner.length() - 1) {
        out.push(inner[k])
      }
      match tail {
        Some(t) => out.push(t)
        None => {
          out.push(last)
          out.push(Return)
        }
      }
      self.rebase(inner_spans, base)
    }
    Call(callee, args) => {
      // The special forms come FIRST and emit their own operands, because they
      // do not all take them in written order and some of the arguments are
      // immediates rather than values. Only the ordinary call falls through to
      // the plain left-to-right walk below.
      //
      // A memory access: the receiver names the memory, and the labelled
      // immediates among the arguments are the memarg.
      if callee.desc is StructGet(recv, meth) && recv.desc is Get(memname) {
        if self.indices.memories.get(memname.name) is Some(mem) {
          let (align, offset) = self.memarg(meth, args, loc)
          let stored = if args.length() >= 2 {
            node_valtype(args[1])
          } else {
            None
          }
          if store_instruction(meth.name, mem, align, offset, stored)
            is Some(st) {
            for a in args {
              if !(a.desc is Labelled(_, _)) {
                self.instr(out, a)
              }
            }
            out.push(st)
            return
          }
          if load_instruction(meth.name, mem, align, offset, None) is Some(ld) {
            for a in args {
              if !(a.desc is Labelled(_, _)) {
                self.instr(out, a)
              }
            }
            out.push(ld)
            return
          }
        }
      }
      // The qualified-path intrinsics, which have no receiver: the `v128::`
      // constants and bitselect, `atomic::fence`, and the wide arithmetic.
      if callee.desc is Path(ns, name) {
        if self.path_intrinsic(out, ns, name, args, loc) {
          return
        }
      }
      // `tab[i](args)` is ONE instruction. A table read followed by a call
      // through the reference is what the syntax says, but `call_indirect`
      // does both, and the cast wax writes to name the signature is exactly
      // the type immediate it takes. The operands go args-then-index, which is
      // the reverse of how they are written.
      if self.indirect_call(out, callee, args, loc) {
        return
      }
      // The ATOMIC accesses: `m.atomic_load32(p)`, `m.atomic_rmw_add32(p, v)`.
      // The method name carries the width -- which is also the natural
      // alignment, and an atomic's alignment is not merely a hint -- but not
      // the operand type, which comes from the VALUE the access is given.
      if callee.desc is StructGet(recv, meth) && recv.desc is Get(memname) {
        if self.indices.memories.get(memname.name) is Some(mem) {
          if @atomics.of_method_name(meth.name) is Some(family) {
            let (align, offset) = self.memarg_natural(
              log2_exact(@atomics.family_bytes(family).to_int64(), loc),
              args,
              loc,
            )
            let operands = args.filter(a => !(a.desc is Labelled(_, _)))
            let op = atomic_op(family, operands)
            for a in operands {
              self.instr(out, a)
            }
            out.push(Atomic(@atomics.opcode(op), mem, align, offset))
            return
          }
        }
      }
      // The SIMD memory accesses: `m.loadv128(p)`, `m.load8_lane(p, v, lane:
      // 3)`. Their widths -- and so their natural alignments -- come from the
      // registry rather than from the name, and a lane access takes a
      // mandatory `lane:` immediate alongside the memarg.
      if callee.desc is StructGet(recv, meth) && recv.desc is Get(memname) {
        if self.indices.memories.get(memname.name) is Some(mem) {
          if @simd.mem_method(meth.name) is Some(op) {
            let (align, offset) = self.memarg_natural(
              log2_exact(op.nat_align.to_int64(), loc),
              args,
              loc,
            )
            let lane = if op.lane { labelled_lane(args, loc) } else { 0 }
            for a in args {
              if !(a.desc is Labelled(_, _)) {
                self.instr(out, a)
              }
            }
            out.push((op.build)(mem, align, offset, lane))
            return
          }
        }
      }
      // A SIMD vector operation, written as a method on the first operand:
      // `v.add_i32x4(w)`, `v.extract_lane_s_i8x16(lane)`. The lane shape is in
      // the NAME, so the registry knows the whole instruction; what varies is
      // how many leading arguments are lane immediates rather than operands.
      if callee.desc is StructGet(recv, meth) {
        if @simd.classify(meth.name) is Some(op) {
          let nimm = match op.imm {
            NoImm => 0
            Lane(_) => 1
            Shuffle => 16
          }
          let lanes = []
          for k in 0.. {
              self.instr(out, recv)
              for a in args {
                self.instr(out, a)
              }
              out.push(ArrayFill(at))
              return
            }
            // `dst.copy(i, src, j, n)` names TWO array types: the destination
            // is the receiver and the source is the second argument, and both
            // are immediates. The source array is still emitted as an operand.
            "copy" if args.length() == 4 => {
              guard node_type_index(args[1]) is Some(st) else {
                raise Unresolved("source array type", loc)
              }
              self.instr(out, recv)
              for a in args {
                self.instr(out, a)
              }
              out.push(ArrayCopy(at, st))
              return
            }
            // `arr.init(seg, dst, src, n)`: the segment name is an immediate,
            // and which of the two instructions this is depends on whether the
            // name is an element segment or a data one.
            "init" if args.length() == 4 && args[0].desc is Get(seg) => {
              self.instr(out, recv)
              for a in args[1:] {
                self.instr(out, a)
              }
              if self.indices.elems.get(seg.name) is Some(e) {
                out.push(ArrayInitElem(at, e))
              } else if self.indices.datas.get(seg.name) is Some(d) {
                out.push(ArrayInitData(at, d))
              } else {
                raise Unresolved("array init segment", loc)
              }
              return
            }
            _ => ()
          }
        }
      }
      // Dropping a SEGMENT. The receiver names one, not a value, so nothing
      // is emitted for it -- the segment index is the whole instruction.
      if callee.desc is StructGet(recv, meth) && meth.name == "drop" {
        if recv.desc is Get(seg) && !self.locals.contains(seg.name) {
          if self.indices.elems.get(seg.name) is Some(e) {
            out.push(ElemDrop(e))
            return
          }
          if self.indices.datas.get(seg.name) is Some(d) {
            out.push(DataDrop(d))
            return
          }
        }
      }
      // Memory and table management: the same five names on both, told apart
      // by which space the receiver is in -- `size` on a memory counts pages
      // and on a table counts elements.
      if callee.desc is StructGet(recv, meth) && recv.desc is Get(rname) {
        if self.mgmt_call(out, rname, meth, args, loc) {
          return
        }
      }
      for a in args {
        self.instr(out, a)
      }
      match callee.desc {
        // A DIRECT call: the callee names a function, and the index is enough.
        // Resolved from where the CALL stands: two branches of a conditional
        // may declare one name, and the site says which is meant.
        Get(name) if self.resolve(self.indices.funcs, name.name, name.loc)
          is Some(_) &&
          !self.locals.contains(name.name) => {
          guard self.resolve(self.indices.funcs, name.name, name.loc) is Some(f) else {
            raise Unresolved("call target", loc)
          }
          out.push(Call(f))
        }
        // Anything else that produced a function REFERENCE is called through
        // it. The type index comes from the reference's own type, which is
        // what makes `call_ref` typed at all.
        _ =>
          if node_type_index(callee) is Some(t) {
            self.instr(out, callee)
            out.push(CallRef(t))
          } else {
            // Named precisely, because these are different jobs wearing one
            // syntax and the burn-down needs to tell them apart.
            raise NotLowered(
              match callee.desc {
                StructGet(_, meth) => "method '" + meth.name + "'"
                Path(ns, meth) =>
                  "intrinsic '" + ns.name + "::" + meth.name + "'"
                _ => "indirect call"
              },
              loc,
            )
          }
      }
    }
    BrOnNull(label, operand) => {
      self.instr(out, operand)
      out.push(BrOnNull(self.depth(label, loc)))
    }
    BrOnNonNull(label, operand) => {
      self.instr(out, operand)
      out.push(BrOnNonNull(self.depth(label, loc)))
    }
    NonNull(e) => {
      self.instr(out, e)
      out.push(RefAsNonNull)
    }
    Test(e, target) => {
      self.instr(out, e)
      out.push(RefTest(self.reftype_index(target, loc)))
    }
    BrOnCast(label, target, operand) => {
      self.instr(out, operand)
      // Both types are immediates: the SOURCE the value is known to have, and
      // the TARGET being tested for. The source comes from the operand's own
      // annotation, since the syntax writes only the target.
      let to = self.reftype_index(target, loc)
      out.push(
        BrOnCast(self.depth(label, loc), br_on_cast_source(operand, to), to),
      )
    }
    BrOnCastFail(label, target, operand) => {
      self.instr(out, operand)
      let to = self.reftype_index(target, loc)
      out.push(
        BrOnCastFail(self.depth(label, loc), br_on_cast_source(operand, to), to),
      )
    }
    TryTable(label~, typ~, catches~, block~) => {
      let handlers : Array[@wasm_bin.CatchHandler] = []
      for c in catches {
        handlers.push(self.catch_handler(c, loc))
      }
      let bt = self.block_type(typ, loc)
      out.push(TryTable(bt, handlers, self.body(label, block.desc)))
    }
    // The DEPRECATED legacy handler. Unlike `try_table`, whose handlers are
    // immediates naming labels to branch to, these handlers are inline bodies
    // that run in place -- so each is lowered inside the try's own label frame,
    // which is the one frame the whole construct occupies.
    Try(label~, typ~, block~, catches~, catch_all~) => {
      let bt = self.block_type(typ, loc)
      let body = self.body(label, block.desc)
      let handlers : Array[(Int, Array[@wasm_bin.Instruction])] = []
      for c in catches {
        guard self.indices.tags.get(c.0.name) is Some(t) else {
          raise Unresolved("tag", loc)
        }
        handlers.push((t, self.body(label, c.1.desc)))
      }
      let all = match catch_all {
        Some(b) => Some(self.body(label, b.desc))
        None => None
      }
      out.push(LegacyTry(bt, body, handlers, all))
    }
    // The structured form, which the checker validated against a lowering to
    // `try_table` plus a block ladder -- so it is emitted from that same
    // lowering. `join` is the try's OWN label when it has one, because a `br`
    // to the try has to leave the join block carrying the value; only when
    // there is no such label does the lowering invent one.
    TryCatch(label~, typ~, block~, arms~) => {
      let inner : Array[@ast.Instr[@typing_env.InferredAnnotation]] = []
      for x in block.desc {
        inner.push(x)
      }
      for a in arms {
        for x in a.arm_body.desc {
          inner.push(x)
        }
      }
      let taken = self.enclosing_labels()
      if label is Some(l) {
        taken.push(l.name)
      }
      for name in labels_within(inner) {
        taken.push(name)
      }
      let arm_labels : Array[@ast.Ident] = []
      for k in 0.. l
        None => { name: fresh_name(taken, "join", 0), loc }
      }
      self.instr(
        out,
        @ast.lower_trycatch(i.info, join~, arm_labels~, typ~, block~, arms~),
      )
    }
    BrTable(labels, operand) => {
      self.instr(out, operand)
      // The written list is the cases and the DEFAULT last, which is how the
      // syntax reads and how the format stores it.
      guard labels.length() >= 1 else {
        raise Unresolved("br_table targets", loc)
      }
      let targets : Array[Int] = []
      for k in 0..<(labels.length() - 1) {
        targets.push(self.depth(labels[k], loc))
      }
      out.push(BrTable(targets, self.depth(labels[labels.length() - 1], loc)))
    }
    ContNew(ct, f) => {
      self.instr(out, f)
      out.push(ContNew(self.type_index_of(ct, loc)))
    }
    ContBind(src, dst, args) => {
      for a in args {
        self.instr(out, a)
      }
      out.push(
        ContBind(self.type_index_of(src, loc), self.type_index_of(dst, loc)),
      )
    }
    Suspend(tag, args) => {
      for a in args {
        self.instr(out, a)
      }
      out.push(Suspend(self.tag_of(tag, loc)))
    }
    Resume(ct, handlers, args) => {
      for a in args {
        self.instr(out, a)
      }
      out.push(
        Resume(self.type_index_of(ct, loc), self.on_clauses(handlers, loc)),
      )
    }
    ResumeThrow(ct, tag, handlers, args) => {
      for a in args {
        self.instr(out, a)
      }
      out.push(
        ResumeThrow(
          self.type_index_of(ct, loc),
          self.tag_of(tag, loc),
          self.on_clauses(handlers, loc),
        ),
      )
    }
    ResumeThrowRef(ct, handlers, args) => {
      for a in args {
        self.instr(out, a)
      }
      out.push(
        ResumeThrowRef(
          self.type_index_of(ct, loc),
          self.on_clauses(handlers, loc),
        ),
      )
    }
    Switch(ct, tag, args) => {
      for a in args {
        self.instr(out, a)
      }
      out.push(Switch(self.type_index_of(ct, loc), self.tag_of(tag, loc)))
    }
    // An `on` clause is the surface spelling that writes a resume's handlers
    // OUTSIDE the call. They belong to the resume, so they are handed down to
    // it rather than emitted here -- there is no instruction for the clause
    // itself.
    On(inner, handlers) => {
      let saved = self.pending_handlers
      self.pending_handlers = handlers
      self.instr(out, inner)
      self.pending_handlers = saved
    }
    Throw(tag, args) => {
      for a in args {
        self.instr(out, a)
      }
      guard self.indices.tags.get(tag.name) is Some(t) else {
        raise Unresolved("tag", loc)
      }
      out.push(Throw(t))
    }
    ThrowRef(e) => {
      self.instr(out, e)
      out.push(ThrowRef)
    }
    // The three constructs the CHECKER validated against a lowering are emitted
    // from that same lowering: there is no second shape to keep in step, and
    // the code that runs is the code that was checked. The synthetic labels the
    // lowering invents are spelled `<..>`, so they never reach the name section.
    While(label~, cond~, step~, block~) => {
      // The lowering's synthetic labels are the ones the DECOMPILER would have
      // written, because they reach the name section: a label-less `while`
      // becomes `loop`, and only a collision with an enclosing label, a label
      // nested in the body, or the while's own gives it a number.
      let inner = [cond]
      if step is Some(x) {
        inner.push(x)
      }
      for x in block.desc {
        inner.push(x)
      }
      let taken = self.enclosing_labels()
      if label is Some(l) {
        taken.push(l.name)
      }
      for name in labels_within(inner) {
        taken.push(name)
      }
      let fresh : @ast.Ident = { name: fresh_name(taken, "loop", 1), loc }
      for
        x in @ast.lower_while(
          i.info,
          fresh_loop=fresh,
          label~,
          cond~,
          step~,
          block=block.desc,
        ) {
        self.instr(out, x)
      }
    }
    Dispatch(index~, cases~, default~, arms~) =>
      for x in @ast.lower_dispatch(i.info, index~, cases~, default~, arms~) {
        self.instr(out, x)
      }
    Match(scrutinee~, arms~, default~) => {
      // One `arm`/`arm_1`/... per arm, then `default` for the escape block,
      // each picked fresh so that neither an arm body's branch to an outer
      // label nor its own labelled block is captured.
      let inner : Array[@ast.Instr[@typing_env.InferredAnnotation]] = []
      for x in default.desc {
        inner.push(x)
      }
      for a in arms {
        for x in a.1.desc {
          inner.push(x)
        }
      }
      let taken = self.enclosing_labels()
      for name in labels_within(inner) {
        taken.push(name)
      }
      let labels : Array[@ast.Ident] = []
      for k in 0.. {
      // A cast to a CONTINUATION type is a compile-time ascription, not a
      // `ref.cast`: continuations carry no RTT, so there is nothing to test at
      // run time and the instruction does not exist. The checker accepted this
      // one only as a provable no-op, so the operand alone is the whole of it.
      if target is Value(Ref(r)) &&
        @typing.is_cont_heaptype(self.ctx.type_context, r.typ) {
        self.instr(out, operand)
        return
      }
      // A narrow load and a following sign cast are ONE instruction: wax splits
      // the width and the signedness across two constructs, and the binary has
      // no separate sign-extension to emit afterwards.
      if target is Signed(typ~, signage~, ..) &&
        self.fused_load(out, operand, typ, signage, loc) {
        return
      }
      if target is Signed(typ~, signage~, ..) &&
        self.fused_atomic_load(out, operand, typ, signage, loc) {
        return
      }
      // A PACKED field or element reads the same way: the width is written on
      // the declaration and the sign on the cast, and the binary has one
      // instruction for the pair.
      if target is Signed(typ~, signage~, ..) &&
        self.fused_packed_read(out, operand, typ, signage, loc) {
        return
      }
      // A NULL cast to a reference type is simply that type's null. Emitting
      // a bottom null and then a `ref.cast` would not even validate: the bottom
      // of the `any` hierarchy is not in the `func` one, so the cast has no
      // common ground to stand on. A non-null target still needs the cast --
      // that is the trap the source is asking for.
      if operand.desc is Null && target is Value(Ref(r)) {
        let want = self.reftype_index(r, loc)
        out.push(RefNull(want.typ))
        if !want.nullable {
          out.push(RefCast(want))
        }
        return
      }
      // `(x as i32) as i64_s` where `x` is an i64 is ONE instruction. There is
      // no wax spelling for `i64.extend32_s`, so the decompiler renders it as
      // the wrap-then-widen pair; re-fuse it. Only a SIGNED widening of a
      // genuinely wrapped i64 is that instruction -- an unsigned widen, or an
      // i32 source where the inner cast is the identity, is something else.
      if target is Signed(typ=I64, signage=Signed, ..) &&
        operand.desc is Cast(inner, Value(I32)) &&
        node_valtype(inner) is Some(I64) {
        self.instr(out, inner)
        out.push(I64Extend32S)
        return
      }
      self.instr(out, operand)
      // In UNREACHABLE code the operand has no type, and so the cast has no
      // instruction: there is nothing on the stack to convert. Validation
      // accepts whatever follows, which is exactly why nothing has to be
      // invented here.
      guard node_valtype(operand) is Some(operand_type) else { return }
      let from = cast_prologue(out, operand_type, target)
      self.cast_instruction(out, from, target, i, loc)
    }
    Null =>
      match node_valtype(i) {
        Some(Ref(r)) => out.push(RefNull(r.typ))
        // A bare `null` with no type is the bottom reference; the checker gives
        // it one whenever the context pins one, so reaching here means it did
        // not.
        _ => raise Unresolved("null type", loc)
      }
    Select(cond, a, b) => {
      self.instr(out, a)
      self.instr(out, b)
      self.instr(out, cond)
      // A `select` over a REFERENCE needs its type written out: the untyped
      // form only works where the value type is unambiguous, which a reference
      // never is.
      // In UNREACHABLE code there is no type, and the untyped form is right
      // there too: nothing runs, so nothing needs telling apart.
      match node_valtype(i) {
        Some(Ref(_) as ty) => out.push(SelectTyped([ty]))
        Some(_) | None => out.push(Select)
      }
    }
    Str(_, bytes) => {
      // A string literal is an ARRAY of its bytes, built one constant at a
      // time. There is no shortcut through a data segment: `array.new_data`
      // takes a segment the source named, and a literal names none.
      let (t, f) = self.array_element_of(i, loc)
      // An `i16` array holds code UNITS, so the bytes are decoded first; an
      // `i8` array holds the bytes as they stand.
      let units = if f.typ is Packed(I16) {
        utf16_units(bytes, loc)
      } else {
        let out : Array[Int] = []
        for k in 0.. {
      self.instr(out, off)
      self.instr(out, len)
      let (t, _) = self.array_element_of(i, loc)
      // An ELEMENT segment means `array.new_elem`; anything else is a data
      // segment. The two spaces are separate, so the name decides.
      match self.indices.elems.get(seg.name) {
        Some(e) => out.push(ArrayNewElem(t, e))
        None =>
          match self.indices.datas.get(seg.name) {
            Some(d) => out.push(ArrayNewData(t, d))
            None => raise Unresolved("array segment", loc)
          }
      }
    }
    Struct(_, fields) => {
      let (t, declared) = self.struct_fields_of(i, loc)
      guard fields.length() == declared.length() else {
        raise Unresolved("struct field count", loc)
      }
      // The checker rebuilt the fields in DECLARED order, which is the order
      // `struct.new` takes them in -- so they are emitted as they stand.
      //
      // A field written with no value is the shorthand `{ x }`, which means
      // the name in scope. It reads that name here rather than being an
      // omission: `struct.new` takes every field, and there is no hole in it.
      for f in fields {
        match f.1 {
          Some(v) => self.instr(out, v)
          // The shorthand still READS a name, so it is a node like any other
          // and gets a span like any other -- without one the printer cannot
          // tell it apart from an instruction the construction emitted itself.
          None => self.read_span(out, f.0, loc)
        }
      }
      out.push(StructNew(t))
    }
    // The custom-descriptors constructions. The DESCRIPTOR is pushed last,
    // above the field values, because that is the order the instruction reads
    // them -- and the type is the construction's own (exact) result type
    // rather than anything the descriptor names.
    StructDesc(descriptor, fields) => {
      let (t, declared) = self.struct_fields_of(i, loc)
      guard fields.length() == declared.length() else {
        raise Unresolved("struct field count", loc)
      }
      for f in fields {
        match f.1 {
          Some(v) => self.instr(out, v)
          // The shorthand still READS a name, so it is a node like any other
          // and gets a span like any other -- without one the printer cannot
          // tell it apart from an instruction the construction emitted itself.
          None => self.read_span(out, f.0, loc)
        }
      }
      self.instr(out, descriptor)
      out.push(StructNewDesc(t))
    }
    StructDefaultDesc(descriptor) => {
      let (t, _) = self.struct_fields_of(i, loc)
      self.instr(out, descriptor)
      out.push(StructNewDefaultDesc(t))
    }
    // `v as ?descriptor(d)` tests a value against a DESCRIPTOR rather than a
    // type immediate -- so the descriptor is an operand, pushed last, and the
    // immediate is the cast's own (exact) result type. The branching forms are
    // the same test with the residual routed to a label.
    CastDesc(value, _, descriptor) => {
      self.instr(out, value)
      self.instr(out, descriptor)
      guard node_valtype(i) is Some(Ref(r)) else {
        raise Unresolved("descriptor cast type", loc)
      }
      out.push(RefCastDescEq(r))
    }
    BrOnCastDescEq(label, nullable, value, descriptor) => {
      self.instr(out, value)
      self.instr(out, descriptor)
      let to = self.descriptor_target(i, nullable, loc)
      out.push(
        BrOnCastDescEq(self.depth(label, loc), br_on_cast_source(value, to), to),
      )
    }
    BrOnCastDescEqFail(label, nullable, value, descriptor) => {
      self.instr(out, value)
      self.instr(out, descriptor)
      let to = self.descriptor_target(i, nullable, loc)
      out.push(
        BrOnCastDescEqFail(
          self.depth(label, loc),
          br_on_cast_source(value, to),
          to,
        ),
      )
    }
    // A struct's descriptor, read back off it. The type immediate is the
    // RECEIVER's, not the descriptor's: it says which struct is being asked.
    GetDescriptor(e) => {
      self.instr(out, e)
      guard node_type_index(e) is Some(t) else {
        raise Unresolved("descriptor receiver type", loc)
      }
      out.push(RefGetDesc(t))
    }
    StructDefault(_) => {
      let (t, _) = self.struct_fields_of(i, loc)
      out.push(StructNewDefault(t))
    }
    StructGet(recv, field) => {
      self.instr(out, recv)
      let (t, declared) = self.struct_fields_of(recv, loc)
      guard self.field_index(t, declared, field.name) is Some((k, f)) else {
        raise Unresolved("struct field", loc)
      }
      // A packed field read is unsigned unless a sign cast follows and fuses;
      // this is the bare form, so it is the unsigned one.
      out.push(if is_packed(f) { StructGetU(t, k) } else { StructGet(t, k) })
    }
    StructSet(recv, field, value) => {
      self.instr(out, recv)
      self.instr(out, value)
      let (t, declared) = self.struct_fields_of(recv, loc)
      guard self.field_index(t, declared, field.name) is Some((k, _)) else {
        raise Unresolved("struct field", loc)
      }
      out.push(StructSet(t, k))
    }
    Array(_, init, size) => {
      self.instr(out, init)
      self.instr(out, size)
      let (t, _) = self.array_element_of(i, loc)
      out.push(ArrayNew(t))
    }
    ArrayDefault(_, size) => {
      self.instr(out, size)
      let (t, _) = self.array_element_of(i, loc)
      out.push(ArrayNewDefault(t))
    }
    ArrayFixed(_, elems) => {
      for e in elems {
        self.instr(out, e)
      }
      let (t, _) = self.array_element_of(i, loc)
      out.push(ArrayNewFixed(t, elems.length()))
    }
    ArrayGet(recv, index) if self.table_of(recv) is Some(_) => {
      guard self.table_of(recv) is Some(t) else { return }
      self.instr(out, index)
      out.push(TableGet(t))
    }
    ArraySet(recv, index, value) if self.table_of(recv) is Some(_) => {
      guard self.table_of(recv) is Some(t) else { return }
      self.instr(out, index)
      self.instr(out, value)
      out.push(TableSet(t))
    }
    ArrayGet(recv, index) => {
      self.instr(out, recv)
      self.instr(out, index)
      let (t, f) = self.array_element_of(recv, loc)
      out.push(if is_packed(f) { ArrayGetU(t) } else { ArrayGet(t) })
    }
    ArraySet(recv, index, value) => {
      self.instr(out, recv)
      self.instr(out, index)
      self.instr(out, value)
      let (t, _) = self.array_element_of(recv, loc)
      out.push(ArraySet(t))
    }
    // An unresolved `#[if(..)]` has nowhere to go in the binary format, and
    // the reference refuses it there too -- the fix is `-D`. The TEXT format
    // writes it, and then BOTH branches are lowered side by side and told
    // apart afterwards by where each was written. That is not a body anyone
    // could run, which is why it is only ever built for the text form.
    IfAnnotation(cond~, then_body~, else_body~) => {
      guard self.keep_conditionals else {
        raise Unemittable("conditional annotations", loc)
      }
      self.body_conditionals.push({
        cond: wat_cond(cond),
        loc,
        then_: then_body.info,
        else_: else_body.map(b => b.info),
      })
      for st in then_body.desc {
        self.instr(out, st)
      }
      if else_body is Some(b) {
        for st in b.desc {
          self.instr(out, st)
        }
      }
    }
    _ => raise NotLowered(construct_name(i.desc), loc)
  }
}

///|
/// Lower a block body under a new label scope.
fn Lowering::body(
  self : Lowering,
  label : @ast.Ident?,
  instrs : Array[@ast.Instr[@typing_env.InferredAnnotation]],
) -> Array[@wasm_bin.Instruction] raise LowerError {
  self.open_label(label)
  let out = self.instrs(instrs)
  let _ = self.labels.pop()
  out
}

///|
/// Bring a `let` binding's name into scope, at the slot it was assigned.
fn Lowering::bind_local(
  self : Lowering,
  name : @ast.Ident,
  loc : @basic.Location,
) -> Unit raise LowerError {
  guard self.binding_slots.get(name.loc.start.cnum) is Some(slot) else {
    raise Unresolved("local binding", loc)
  }
  self.locals[name.name] = slot
}

///|
/// Lower a run of instructions into a fresh array, opening no label of its own.
fn Lowering::instrs(
  self : Lowering,
  instrs : Array[@ast.Instr[@typing_env.InferredAnnotation]],
) -> Array[@wasm_bin.Instruction] raise LowerError {
  // A nested body is its OWN array with its own indices, so it collects its
  // own spans; they are handed to the nested list as the body completes.
  let outer = self.spans
  self.spans = []
  let out : Array[@wasm_bin.Instruction] = []
  for s in instrs {
    self.instr(out, s)
  }
  self.nested_spans.push(self.spans)
  self.spans = outer
  out
}

///|
/// Enter a block, recording its label's name.
///
/// Labels are numbered for the name section in the order the blocks OPEN, which
/// is not the order a `br` counts them in -- depth is relative to where the
/// branch stands, while a name belongs to the block itself.
fn Lowering::open_label(self : Lowering, label : @ast.Ident?) -> Unit {
  self.labels.push(label.map(l => l.name))
  if label is Some(l) {
    note_name(self.label_names, self.label_counter, l.name)
  }
  self.label_counter = self.label_counter + 1
}

///|
/// A block's signature, as the format spells it.
fn Lowering::block_type(
  self : Lowering,
  typ : @ast.FuncType,
  loc : @basic.Location,
) -> @wasm_bin.BlockType raise LowerError {
  if typ.params.is_empty() {
    match typ.results {
      [] => Empty
      [r] =>
        match r {
          I32 => Value(I32)
          I64 => Value(I64)
          F32 => Value(F32)
          F64 => Value(F64)
          V128 => Value(V128)
          // A block type IS a value type, and a reference is one -- its type
          // names just have to become indices first.
          Ref(rt) => Value(Ref(self.reftype_index(rt, loc)))
        }
      // Anything else needs a type INDEX: the inline forms only reach one
      // result and no parameters.
      _ => TypeIndex(self.block_functype_index(typ, loc))
    }
  } else {
    TypeIndex(self.block_functype_index(typ, loc))
  }
}

///|
/// Read a name: a local, a global, or a function reference.
///
/// The order is the language's -- a local shadows everything -- and it is the
/// same order the checker resolved it in, which is what makes the two agree.
fn Lowering::read(
  self : Lowering,
  name : @ast.Ident,
  loc : @basic.Location,
) -> @wasm_bin.Instruction raise LowerError {
  match self.locals.get(name.name) {
    Some(k) => LocalGet(k)
    None =>
      match self.indices.globals.get(name.name) {
        Some(g) => GlobalGet(g)
        None =>
          match self.indices.funcs.get(name.name) {
            Some(f) => {
              // Record which side of the declaration question this reference
              // falls on: one inside a body declares nothing.
              let into = if self.in_body {
                self.func_refs_in_body
              } else {
                self.func_refs_outside
              }
              if !into.contains(name.name) {
                into.push(name.name)
              }
              RefFunc(f)
            }
            None => raise Unresolved("name", loc)
          }
      }
  }
}

///|
/// Write a name: a local or a global.
fn Lowering::write(
  self : Lowering,
  name : @ast.Ident,
  loc : @basic.Location,
) -> @wasm_bin.Instruction raise LowerError {
  match self.locals.get(name.name) {
    Some(k) => LocalSet(k)
    None =>
      match self.indices.globals.get(name.name) {
        Some(g) => GlobalSet(g)
        None => raise Unresolved("assignment target", loc)
      }
  }
}

///|
/// Emit a narrow load fused with the sign cast that follows it.
///
/// `true` when it applied. The pair is one instruction, so recognising it here
/// is not an optimisation -- emitting the load and then the cast separately
/// would emit a zero-extending load and no sign extension at all.
fn Lowering::fused_load(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  operand : @ast.Instr[@typing_env.InferredAnnotation],
  typ : @ast.NumType,
  signage : @wasm_types.Signage,
  loc : @basic.Location,
) -> Bool raise LowerError {
  guard operand.desc is Call(callee, args) else { return false }
  guard callee.desc is StructGet(recv, meth) else { return false }
  guard recv.desc is Get(memname) else { return false }
  guard self.indices.memories.get(memname.name) is Some(mem) else {
    return false
  }
  let (align, offset) = self.memarg(meth, args, loc)
  guard load_instruction(meth.name, mem, align, offset, Some((typ, signage)))
    is Some(ld) else {
    return false
  }
  for a in args {
    if !(a.desc is Labelled(_, _)) {
      self.instr(out, a)
    }
  }
  out.push(ld)
  true
}

///|
/// A NARROW ATOMIC LOAD and its zero-extending cast, as one instruction.
///
/// The same fusion the plain loads get, for the same reason: wax writes the
/// width on the method and the extension on the cast, and the binary has a
/// single opcode for the pair. Only the UNSIGNED direction exists -- there is
/// no sign-extending atomic load -- so a signed cast falls through to the
/// generic path, which is right: it really is two operations.
fn Lowering::fused_atomic_load(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  operand : @ast.Instr[@typing_env.InferredAnnotation],
  typ : @ast.NumType,
  signage : @wasm_types.Signage,
  loc : @basic.Location,
) -> Bool raise LowerError {
  guard signage is Unsigned else { return false }
  guard operand.desc is Call(callee, args) else { return false }
  guard callee.desc is StructGet(recv, meth) else { return false }
  guard recv.desc is Get(memname) else { return false }
  guard self.indices.memories.get(memname.name) is Some(mem) else {
    return false
  }
  guard @atomics.of_method_name(meth.name) is Some(Load(width)) else {
    return false
  }
  let op : @atomics.Op = match (width, typ) {
    (W8, I32) => Load(I32, Some(N8))
    (W8, I64) => Load(I64, Some(N8))
    (W16, I32) => Load(I32, Some(N16))
    (W16, I64) => Load(I64, Some(N16))
    (W32, I64) => Load(I64, Some(N32))
    _ => return false
  }
  let (align, offset) = self.memarg_natural(
    log2_exact(@atomics.family_bytes(Load(width)).to_int64(), loc),
    args,
    loc,
  )
  for a in args {
    if !(a.desc is Labelled(_, _)) {
      self.instr(out, a)
    }
  }
  out.push(Atomic(@atomics.opcode(op), mem, align, offset))
  true
}

///|
/// A field's position in a struct's declared fields, with the field itself.
///
/// The position comes from the SOURCE declaration, because the store erases
/// field names -- wasm has none -- and the two lists are the same fields in the
/// same order.
fn Lowering::field_index(
  self : Lowering,
  t : Int,
  declared : Array[@wasm_types.MutType[@wasm_types.StorageType[@type_store.Id]]],
  name : String,
) -> (Int, @wasm_types.MutType[@wasm_types.StorageType[@type_store.Id]])? {
  guard self.source_types.get(t) is Some(sub) else { return None }
  guard sub.typ is Struct(fields) else { return None }
  for k, f in fields {
    if f.desc.0.name == name && k < declared.length() {
      return Some((k, declared[k]))
    }
  }
  None
}

///|
/// Re-fuse a table read and a call through it into `call_indirect`.
///
/// Three spellings reach the same instruction: the read cast to a named
/// function type, the read cast to an inline one, and the bare read when the
/// table's own element type already names a signature. What they share is that
/// the type is known WITHOUT running anything, which is the whole condition --
/// `call_indirect` carries its signature as an immediate.
fn Lowering::indirect_call(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  callee : @ast.Instr[@typing_env.InferredAnnotation],
  args : Array[@ast.Instr[@typing_env.InferredAnnotation]],
  loc : @basic.Location,
) -> Bool raise LowerError {
  let (read, signature) = match callee.desc {
    Cast(inner, Value(Ref({ typ: Type(ft), .. }))) =>
      (inner, Some(self.type_index_of(ft, loc)))
    // A bare read: the table's element type names the signature, so there is
    // nothing for a cast to add.
    ArrayGet(_, _) => (callee, None)
    _ => return false
  }
  guard read.desc is ArrayGet(recv, index) else { return false }
  guard self.table_of(recv) is Some(tab) else { return false }
  let type_idx = match signature {
    Some(t) => t
    None =>
      match node_type_index(read) {
        Some(t) => t
        None => return false
      }
  }
  for a in args {
    self.instr(out, a)
  }
  self.instr(out, index)
  out.push(CallIndirect(type_idx, tab))
  true
}

///|
/// The table a receiver names, if it names one.
///
/// `tab[i]` and `arr[i]` are one syntax; the receiver decides, exactly as it
/// does for the management methods.
fn Lowering::table_of(
  self : Lowering,
  recv : @ast.Instr[@typing_env.InferredAnnotation],
) -> Int? {
  guard recv.desc is Get(name) else { return None }
  // A LOCAL of the same name shadows the table, which is the order the checker
  // resolved it in too.
  guard !self.locals.contains(name.name) else { return None }
  self.indices.tables.get(name.name)
}

///|
/// Lower a qualified-path intrinsic, returning whether it applied.
///
/// These are the operations with no receiver to hang a method off. A `v128::`
/// constant is the odd one: its arguments are not operands at all but the lane
/// LITERALS of a single 16-byte immediate, so nothing is emitted for them.
fn Lowering::path_intrinsic(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  ns : @ast.Ident,
  name : @ast.Ident,
  args : Array[@ast.Instr[@typing_env.InferredAnnotation]],
  loc : @basic.Location,
) -> Bool raise LowerError {
  match (ns.name, name.name) {
    ("v128", part) => {
      let full = @simd.free_full(part)
      if @simd.const_shape_of_name(full) is Some(shape) {
        // The SHAPE is part of the spelling. Sixteen bytes are sixteen bytes
        // whichever shape wrote them, and the text says which one did.
        let texts : Array[String] = []
        for a in args {
          texts.push(literal_text(a, loc))
        }
        let spelling = StringBuilder::new()
        spelling.write_string(shape.to_str())
        for t in texts {
          spelling.write_string(" " + t)
        }
        out.push(
          Spelled(
            spelling.to_string(),
            V128Const(vector_bytes(shape, texts, loc)),
          ),
        )
        return true
      }
      guard @simd.classify(full) is Some(op) else { return false }
      for a in args {
        self.instr(out, a)
      }
      out.push((op.build)([]))
      true
    }
    ("atomic", "fence") => {
      out.push(AtomicFence)
      true
    }
    // The wide arithmetic: two 64-bit halves in, two out. The operands are
    // already on the stack in call order, so there is nothing to reorder.
    ("i64", wide) => {
      let instruction : @wasm_bin.Instruction = match wide {
        "add128" => I64Add128
        "sub128" => I64Sub128
        "mul_wide_s" => I64MulWideS
        "mul_wide_u" => I64MulWideU
        _ => return false
      }
      for a in args {
        self.instr(out, a)
      }
      out.push(instruction)
      true
    }
    _ => false
  }
}

///|
/// The 16 bytes of a vector, from its lanes as written.
fn vector_bytes(
  shape : @simd.Shape,
  texts : Array[String],
  loc : @basic.Location,
) -> Bytes raise LowerError {
  let lanes = @simd.const_arity(shape)
  guard texts.length() == lanes else { raise Unresolved("vector lanes", loc) }
  let width = 16 / lanes
  let out : Array[Byte] = Array::make(16, b'\x00')
  for k in 0..
        Float::from_double(parse_float_bits(text, false, loc))
        .reinterpret_as_int()
        .to_int64() &
        0xFFFFFFFFL
      F64x2 => parse_float_bits(text, true, loc).reinterpret_as_int64()
      _ => parse_i64(text, loc)
    }
    for b in 0..> (b * 8)).to_int() &
      0xFF).to_byte()
    }
  }
  Bytes::from_array(out)
}

///|
/// The literal text of a lane argument. A negative lane arrives as a negation
/// applied to its magnitude, and the parsers below want the sign back.
fn literal_text(
  a : @ast.Instr[@typing_env.InferredAnnotation],
  loc : @basic.Location,
) -> String raise LowerError {
  match a.desc {
    Int(s) | Float(s) => s
    UnOpI(op, b) if op.desc is Neg => "-" + literal_text(b, loc)
    _ => raise Unresolved("lane literal", loc)
  }
}

///|
/// The atomic operation a family becomes, once the operand type is known.
///
/// The family says the WIDTH; what it cannot say is whether the access is an
/// i32 or an i64 one, because the same `atomic_store32` serves both -- storing
/// the low half of an i64 is a different opcode from storing an i32. The
/// answer is the type of the VALUE operand, and in unreachable code, where
/// there is no type to read, the i32 form is the default.
fn atomic_op(
  family : @atomics.Family,
  operands : Array[@ast.Instr[@typing_env.InferredAnnotation]],
) -> @atomics.Op {
  fn value_type() {
    if operands.length() >= 2 && node_valtype(operands[1]) is Some(I64) {
      @atomics.NumTy::I64
    } else {
      @atomics.NumTy::I32
    }
  }

  fn narrow(w : @atomics.Width, t : @atomics.NumTy) {
    match w {
      W8 => (t, Some(@atomics.Narrow::N8))
      W16 => (t, Some(@atomics.Narrow::N16))
      W32 =>
        match t {
          I64 => (@atomics.NumTy::I64, Some(@atomics.Narrow::N32))
          I32 => (@atomics.NumTy::I32, None)
        }
      W64 => (@atomics.NumTy::I64, None)
    }
  }

  match family {
    Notify => Notify
    Wait(t) => Wait(t)
    Load(W32) => Load(I32, None)
    Load(W64) => Load(I64, None)
    Load(W8) => Load(I32, Some(N8))
    Load(W16) => Load(I32, Some(N16))
    Store(w) => {
      let (t, n) = narrow(w, value_type())
      Store(t, n)
    }
    Rmw(op, w) => {
      let (t, n) = narrow(w, value_type())
      Rmw(op, t, n)
    }
  }
}

///|
/// The SOURCE type a branching cast emits, given the target it tests for.
///
/// Wax writes only the target; the binary wants both, and the typer recovers
/// the source from the operand. When the operand has no usable heap type of its
/// own -- a hole in dead code, or a literal `ref.null none` -- the TARGET
/// stands in, which is always a valid source against itself. Its nullability
/// still has to carry over, though: a nullable operand is not a subtype of a
/// non-null target, and the check the instruction carries would reject it.
fn br_on_cast_source(
  operand : @ast.Instr[@typing_env.InferredAnnotation],
  target : @wasm_types.RefType[Int],
) -> @wasm_types.RefType[Int] {
  match node_last_reftype(operand) {
    Some(r) => r
    None =>
      { ..target, nullable: target.nullable || node_last_nullable(operand) }
  }
}

///|
/// The integer literal a lane immediate is written as. Typing has already
/// rejected anything else, so a non-literal here is a lowering bug.
fn lane_immediate(
  a : @ast.Instr[@typing_env.InferredAnnotation],
  loc : @basic.Location,
) -> Int raise LowerError {
  let value = if a.desc is Labelled(_, v) { v } else { a }
  guard value.desc is Int(s) else { raise Unresolved("lane immediate", loc) }
  parse_i64(s, loc).to_int()
}

///|
/// The target reference type of a descriptor branch.
///
/// The node's own type is the RESIDUAL -- what is left when the test fails --
/// so the target is recovered from it and the nullability the branch wrote.
fn Lowering::descriptor_target(
  self : Lowering,
  i : @ast.Instr[@typing_env.InferredAnnotation],
  nullable : Bool,
  loc : @basic.Location,
) -> @wasm_types.RefType[Int] raise LowerError {
  ignore(self)
  guard node_last_reftype(i) is Some(r) else {
    raise Unresolved("descriptor branch type", loc)
  }
  { ..r, nullable, }
}

///|
/// The `tag:` immediate a stack-switching method carries.
fn labelled_tag(
  args : Array[@ast.Instr[@typing_env.InferredAnnotation]],
  loc : @basic.Location,
) -> @ast.Ident raise LowerError {
  for a in args {
    if a.desc is Labelled(l, v) && l.name == "tag" && v.desc is Get(t) {
      return t
    }
  }
  raise Unresolved("tag immediate", loc)
}

///|
/// The mandatory `lane:` immediate of a vector lane access.
fn labelled_lane(
  args : Array[@ast.Instr[@typing_env.InferredAnnotation]],
  loc : @basic.Location,
) -> Int raise LowerError {
  for a in args {
    if a.desc is Labelled(label, _) && label.name == "lane" {
      return lane_immediate(a, loc)
    }
  }
  raise Unresolved("lane immediate", loc)
}

///|
/// Whether a receiver is something `array.len` accepts: a concrete array type,
/// the abstract `array` heap type, or the bottom reference that is below it.
/// The bulk methods need a real element type and are checked separately.
fn receiver_is_array(recv : @ast.Instr[@typing_env.InferredAnnotation]) -> Bool {
  if node_type_index(recv) is Some(_) {
    return true
  }
  node_valtype(recv) is Some(Ref({ typ: Array | None_, .. }))
}

///|
/// Lower a management call, returning whether it applied.
///
/// The five names -- `size`, `grow`, `fill`, `copy`, `init` -- serve memories
/// and tables alike, so the receiver's SPACE picks the instruction. `init` and
/// `copy` name a second thing (a segment, or the other memory or table), and
/// that name is an immediate rather than a value: it is not emitted, it is
/// encoded.
fn Lowering::mgmt_call(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  recv : @ast.Ident,
  meth : @ast.Ident,
  args : Array[@ast.Instr[@typing_env.InferredAnnotation]],
  loc : @basic.Location,
) -> Bool raise LowerError {
  guard !self.locals.contains(recv.name) else { return false }
  let on_memory = self.indices.memories.get(recv.name)
  let on_table = self.indices.tables.get(recv.name)
  guard on_memory is Some(_) || on_table is Some(_) else { return false }
  // The leading segment or source name of `init`/`copy` is an immediate.
  let (immediate, values) = match meth.name {
    "init" =>
      if args.length() >= 1 && args[0].desc is Get(n) {
        (Some(n), args[1:].to_owned())
      } else {
        (None, args)
      }
    // A CROSS-space copy names the source first: `dst.copy(src, i, j, n)`.
    // It is told from the same-space `dst.copy(i, j, n)` by the first argument
    // naming a memory or table rather than being a value -- which is what
    // makes the two spellings one method.
    "copy" =>
      if args.length() >= 1 &&
        args[0].desc is Get(n) &&
        (
          self.indices.memories.get(n.name) is Some(_) ||
          self.indices.tables.get(n.name) is Some(_)
        ) &&
        !self.locals.contains(n.name) {
        (Some(n), args[1:].to_owned())
      } else {
        (None, args)
      }
    _ => (None, args)
  }
  for a in values {
    self.instr(out, a)
  }
  match (meth.name, on_memory, on_table) {
    ("size", Some(m), _) => out.push(MemorySize(m))
    ("grow", Some(m), _) => out.push(MemoryGrow(m))
    ("fill", Some(m), _) => out.push(MemoryFill(m))
    ("copy", Some(m), _) => {
      let src = match immediate {
        Some(n) =>
          match self.indices.memories.get(n.name) {
            Some(k) => k
            None => raise Unresolved("source memory", loc)
          }
        None => m
      }
      out.push(MemoryCopy(m, src))
    }
    ("init", Some(m), _) => {
      guard immediate is Some(n) else {
        raise Unresolved("data segment name", loc)
      }
      guard self.indices.datas.get(n.name) is Some(d) else {
        raise Unresolved("data segment", loc)
      }
      out.push(MemoryInit(m, d))
    }
    ("size", _, Some(t)) => out.push(TableSize(t))
    ("grow", _, Some(t)) => out.push(TableGrow(t))
    ("fill", _, Some(t)) => out.push(TableFill(t))
    ("copy", _, Some(t)) => {
      let src = match immediate {
        Some(n) =>
          match self.indices.tables.get(n.name) {
            Some(k) => k
            None => raise Unresolved("source table", loc)
          }
        None => t
      }
      out.push(TableCopy(t, src))
    }
    ("init", _, Some(t)) => {
      guard immediate is Some(n) else {
        raise Unresolved("element segment name", loc)
      }
      guard self.indices.elems.get(n.name) is Some(e) else {
        raise Unresolved("element segment", loc)
      }
      out.push(TableInit(t, e))
    }
    _ => return false
  }
  true
}

///|
/// The type index a block signature needs when no inline form fits.
///
/// A block with parameters, or with several results, cannot be written inline:
/// the format spells those as a reference into the type section. The signature
/// has to be there already -- the checker interned every block type it saw --
/// so this is a lookup, and its failure means the two disagree about what was
/// declared rather than that something is missing.
fn Lowering::functype_of(
  self : Lowering,
  typ : @ast.FuncType,
  loc : @basic.Location,
) -> Int raise LowerError {
  self.block_functype_index(typ, loc)
}

///|
fn Lowering::block_functype_index(
  self : Lowering,
  typ : @ast.FuncType,
  loc : @basic.Location,
) -> Int raise LowerError {
  let params : Array[@wasm_types.ValType[Int]] = []
  for p in typ.params {
    params.push(self.valtype_index(p.desc.1, loc))
  }
  let results : Array[@wasm_types.ValType[Int]] = []
  for r in typ.results {
    results.push(self.valtype_index(r, loc))
  }
  // The pre-built table first, then the section itself -- which may have to
  // grow: a block's signature is named by SHAPE, and nothing is obliged to
  // have interned that shape before the block asked for it.
  if self.functypes.get((params, results)) is Some(i) {
    return i
  }
  guard emitted_shape_index(params, results) is Some(i) else {
    raise Unresolved("block type", loc)
  }
  i
}

///|
/// The branch depth a label names.
fn Lowering::depth(
  self : Lowering,
  label : @ast.Ident,
  loc : @basic.Location,
) -> Int raise LowerError {
  match self.depth_of(label.name) {
    Some(d) => d
    None => raise Unresolved("branch target", loc)
  }
}

///|
/// One `try_table` catch clause.
///
/// The handler branches OUT of the try, so its label is resolved in the
/// enclosing scope -- which is where it is written, and where the checker
/// resolved it too.
fn Lowering::catch_handler(
  self : Lowering,
  c : @ast.Catch,
  loc : @basic.Location,
) -> @wasm_bin.CatchHandler raise LowerError {
  fn tag_of(t : @ast.Ident) -> Int raise LowerError {
    match self.indices.tags.get(t.name) {
      Some(i) => i
      None => raise Unresolved("catch tag", loc)
    }
  }

  match c {
    Catch(tag, label) => Catch(tag_of(tag), self.depth(label, loc))
    CatchRef(tag, label) => CatchRef(tag_of(tag), self.depth(label, loc))
    CatchAll(label) => CatchAll(self.depth(label, loc))
    CatchAllRef(label) => CatchAllRef(self.depth(label, loc))
  }
}

///|
/// A byte string as UTF-16 code units.
///
/// An `i16` array holds code units rather than bytes, so a literal destined for
/// one is decoded and re-encoded. An astral scalar becomes a surrogate PAIR,
/// which is why the count can differ from the character count.
fn utf16_units(b : Bytes, loc : @basic.Location) -> Array[Int] raise LowerError {
  guard @unicode.utf8_text(b) is Some(text) else {
    raise NotLowered("non-UTF-8 string in a code-unit array", loc)
  }
  let out : Array[Int] = []
  for k in 0.. Bool raise LowerError {
  // A packed read yields an i32 and nothing else: there is no `array.get` or
  // `struct.get` that produces an i64. So `as i64_u` is the fused read AND a
  // widening, and dropping the widening leaves an i32 where an i64 is wanted.
  fn widen(out : Array[@wasm_bin.Instruction]) -> Unit {
    if typ is I64 {
      out.push(if signage is Signed { I64ExtendI32S } else { I64ExtendI32U })
    }
  }

  guard typ is (I32 | I64) else { return false }
  match operand.desc {
    ArrayGet(recv, index) => {
      guard self.table_of(recv) is None else { return false }
      let (t, f) = self.array_element_of(recv, loc)
      guard is_packed(f) else { return false }
      self.instr(out, recv)
      self.instr(out, index)
      out.push(if signage is Signed { ArrayGetS(t) } else { ArrayGetU(t) })
      widen(out)
      true
    }
    StructGet(recv, field) => {
      let (t, declared) = self.struct_fields_of(recv, loc)
      guard self.field_index(t, declared, field.name) is Some((k, f)) else {
        return false
      }
      guard is_packed(f) else { return false }
      self.instr(out, recv)
      out.push(
        if signage is Signed {
          StructGetS(t, k)
        } else {
          StructGetU(t, k)
        },
      )
      widen(out)
      true
    }
    _ => false
  }
}

///|
/// A named type's index.
fn Lowering::type_index_of(
  self : Lowering,
  name : @ast.Ident,
  loc : @basic.Location,
) -> Int raise LowerError {
  // By NAME first. Two declarations of one shape share a store entry but not an
  // emitted index, and a reference that names one of them means that one.
  if self.layout.by_name.get(name.name) is Some(emitted) {
    return emitted
  }
  guard self.ctx.type_context.types.find_no_mark(name.name) is Some((idx, _)) else {
    raise Unresolved("type name", loc)
  }
  match idx {
    Def(id) => emitted_type_index(id.to_int_for_tests_only())
    Rec(_) => raise Unresolved("rec-group type reference", loc)
  }
}

///|
/// A tag's index.
fn Lowering::tag_of(
  self : Lowering,
  tag : @ast.Ident,
  loc : @basic.Location,
) -> Int raise LowerError {
  match self.indices.tags.get(tag.name) {
    Some(i) => i
    None => raise Unresolved("tag", loc)
  }
}

///|
/// A resume's handler clauses.
///
/// `on t -> 'l` sends a suspension to a label; `on t switch` lets the
/// suspending side switch directly instead. They are two ways of answering the
/// same question, so they share the clause list.
fn Lowering::on_clauses(
  self : Lowering,
  handlers : Array[@ast.OnClause],
  loc : @basic.Location,
) -> Array[@wasm_bin.OnClause] raise LowerError {
  let out : Array[@wasm_bin.OnClause] = []
  for h in handlers {
    match h {
      OnLabel(tag, label) =>
        out.push(OnLabel(self.tag_of(tag, loc), self.depth(label, loc)))
      OnSwitch(tag) => out.push(OnSwitch(self.tag_of(tag, loc)))
    }
  }
  out
}

///|
/// Whether a method name is one of the stack-switching operations.
fn cont_method_instruction(meth : String) -> Unit? {
  match meth {
    "resume" | "resume_throw" | "resume_throw_ref" | "switch" => Some(())
    _ => None
  }
}

///|
/// The instruction a stack-switching method becomes.
///
/// `switch` and `resume_throw` name a TAG, written as a labelled immediate and
/// as an invocation respectively -- neither is a value, so both are read out of
/// the arguments rather than emitted from them.
fn Lowering::cont_method(
  self : Lowering,
  meth : @ast.Ident,
  args : Array[@ast.Instr[@typing_env.InferredAnnotation]],
  ct : Int,
  handlers : Array[@ast.OnClause],
  loc : @basic.Location,
) -> @wasm_bin.Instruction raise LowerError {
  let on = self.on_clauses(handlers, loc)
  match meth.name {
    "resume" => Resume(ct, on)
    "resume_throw_ref" => ResumeThrowRef(ct, on)
    // Both remaining forms name a TAG, and the checker leaves it labelled on
    // the node for exactly this read -- `switch` because that is how it was
    // written, `resume_throw` because an invocation is not a shape a code
    // generator can read a name out of twice.
    "resume_throw" =>
      ResumeThrow(ct, self.tag_of(labelled_tag(args, loc), loc), on)
    _ => Switch(ct, self.tag_of(labelled_tag(args, loc), loc))
  }
}