Files
0d0e628b3a Compose wasi-threads with memory64: addr64 atomics, shared-mem64 modules (#49)
The threads support and the memory64 support each worked alone but
were kept apart by two gates: shared memory64 modules were rejected
at translation, and every 0xFE atomic was rejected on a memory64
memory. A wasm64-wasip1-threads toolchain now exists, so compose
them.

- atomicEA64 mirrors simdEA64's overflow-guarded address math; every
  atomic op body moves into an ea-keyed core, the wasm32 helpers
  become one-line wrappers through atomicEA, and each gets an _m64
  twin through atomicEA64 (loads/stores/RMW families, wait/notify).
  Contended fast paths and memory orders are moved, not rewritten.
- The 0xFE lowering decodes the memarg offset as u64 under memory64
  (the one place still reading u32), passes it as Const64, and
  selects the _m64 helper names; the inline atomic fast path becomes
  width-correct instead of truncating.
- wasi_thread_spawn under memory64 passes the start_arg as i64 (it is
  a linear-memory pointer): threadSpawn splits into a shared launch
  core with per-width entries, and the module wires a threadStart64
  field for the widened wasi_thread_start export.
- memoryGrow64 gains the no-relocation branch for shared memories a
  grow must never move the backing array out from under peer agents.
  A shared memory64 requires a declared maximum: the reservation is
  the mapping ceiling, and the memory64 hard cap is not a ceiling.
- WasiStubs gains the trivial Sched_yield64 LP64 twin (musl's spin
  paths import sched_yield; single-threaded modules never did).
- New shared+i64 fixtures mirror the wasm32 threads fixtures and run
  under the race detector; an atomicEA64 unit test covers beyond-4GiB
  addresses, overflow and misalignment; a regression test pins that
  unshared memory64 modules may use atomics. Generated output for
  wasm32 modules is byte-identical.

Verified end to end: a wasm64-wasip1-threads C program
(pthread_create x4 over a shared counter) transpiles and runs
correctly on goroutines, including under -race; llama.cpp built for
wasm64-wasip1-threads decodes correctly with ggml n_threads
parallelism.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-22 00:31:05 +09:00

2727 lines
88 KiB
Go

package lower
import (
"errors"
"fmt"
"math"
"strings"
"github.com/goccy/wasm2go/internal/ssa"
"github.com/goccy/wasm2go/internal/wasm"
)
// ssaFuncString re-exports ssa.FuncString without forcing test files
// to import the internal/ssa package.
func ssaFuncString(f interface{}) string {
if ff, ok := f.(*ssa.Func); ok {
return ssa.FuncString(ff)
}
return ""
}
// ErrSSAUnsupported is returned by LowerFunction (and the rest of the
// SSA pipeline) when a wasm function uses a feature the lowering /
// optimizer / emitter does not yet handle. Translate surfaces it as a
// hard build error; there is no longer a legacy direct-opcode fallback.
var ErrSSAUnsupported = errors.New("ssa lowering: unsupported wasm feature")
// LowerFunction decodes a single wasm function body and produces an
// ssa.Func.
//
// Anything not yet implemented returns ErrSSAUnsupported wrapped with
// the function index and the offending opcode; Translate fails the
// build with that message so the gap can be addressed in the SSA
// pipeline rather than worked around silently.
func LowerFunction(mod *wasm.Module, funcIdx uint32, name string, throwSet *ThrowSet) (resFn *ssa.Func, resErr error) {
// Recover from lowering bugs and surface them as ErrSSAUnsupported
// with a clear function reference. Without this, a single edge case
// anywhere in the lowering would crash the whole codegen run.
defer func() {
if r := recover(); r != nil {
resFn = nil
resErr = fmt.Errorf("%w: lower panic in fn%d (%s): %v", ErrSSAUnsupported, funcIdx, name, r)
}
}()
localIdx := funcIdx - mod.NumImportedFuncs
fn := mod.Functions[localIdx]
ft := mod.Types[fn.TypeIdx]
sig := ssa.FuncSig{
Params: toSSATypes(ft.Params),
Results: toSSATypes(ft.Results),
}
b := ssa.NewFuncBuilder(name, sig)
entry := b.NewBlock(ssa.BlockPlain)
b.SetEntry(entry)
b.SetCurrent(entry)
locals, localTypes, err := setupLocals(b, fn, ft)
if err != nil {
return nil, err
}
// A function containing an EH `try` uses the mutable-locals lowering
// mode: locals are NOT promoted to SSA but kept as mutable Go variables
// (retained OpLocalGet/OpLocalSet), so a Go panic/recover at a catch
// landing pad sees each local's throw-time value (wasm EH semantics).
// Branch-based EH keeps every handler reachable through real CFG edges,
// so locals flow through ordinary phis — the de-SSA "mutable locals"
// mode that panic/recover needed is gone.
mutableLocals := false
ls := &lowerState{
mem64: mod.Memory64(),
b: b,
mod: mod,
ft: ft,
locals: locals,
localTypes: localTypes,
incoming: map[*ssa.Block][]incomingEdge{},
mutableLocals: mutableLocals,
inlineInfo: analyzeModuleForInline(mod),
callerIdx: funcIdx,
throwSet: throwSet,
excSlots: maxTagArity(mod),
}
r := wasm.NewInstrReader(fn.Body)
if err := skipLocalDecls(r); err != nil {
return nil, err
}
// In mutable-locals mode, seed each parameter's mutable variable from
// its OpParam at entry (non-param locals zero-init via the emitted Go
// `var`, matching wasm's zeroed locals).
if mutableLocals {
for i := range ft.Params {
ls.b.NewValueAuxInt(ssa.OpLocalSet, ssa.TypeMem, int64(i), locals[i])
}
}
if err := ls.lowerBody(r); err != nil {
return nil, err
}
if mutableLocals {
b.Func().MutableLocals = true
b.Func().LocalTypes = localTypes
}
// Drop isolated (no-edge) blocks left behind by structured-control
// lowering — e.g. the unused post block of a loop that never falls
// through. Done before verification so they don't trip the
// reachability / terminator checks.
ssa.PruneDeadBlocks(b.Func())
// Structural verification: a malformed CFG (mismatched edges, bad
// phi arity, unreachable block, ...) is a lowering bug. Surface it
// as ErrSSAUnsupported so Translate aborts with a clear error
// instead of emitting wrong Go.
if err := ssa.Verify(b.Func()); err != nil {
return nil, fmt.Errorf("%w: verify fn%d: %w", ErrSSAUnsupported, funcIdx, err)
}
return b.Func(), nil
}
// lowerState carries the per-function bookkeeping needed during SSA
// construction.
type lowerState struct {
// throwSet answers "can a call to this function leave an exception
// pending?", so a check-and-branch is emitted only where one is
// possible. Nil means "assume every call can" (see ThrowSet).
throwSet *ThrowSet
// excSlots is the module's exception operand-slot count (the widest tag
// arity), matching the Module state array the emitter declares.
excSlots int
// excPropagate is the function's shared "return, leaving the exception
// pending for the caller" block, created on first use.
excPropagate *ssa.Block
b *ssa.FuncBuilder
mod *wasm.Module
ft wasm.FuncType
// mem64: the module's linear memory uses the memory64 proposal's
// 64-bit index space; addresses on the stack are i64 and
// memory.size/grow speak i64 pages.
mem64 bool
// locals[i] = current SSA value of local index i (params + decls).
// Mutated by local.set / local.tee and at block transitions.
locals []*ssa.Value
localTypes []ssa.Type
// stack is the wasm operand stack: a list of *ssa.Value most recent
// at end.
stack []*ssa.Value
// ctrl is the wasm control stack. Top of stack is the innermost
// frame.
ctrl []*ctrlFrame
// incoming records, per target block, the list of edges flowing in
// (from explicit branches AND fall-through). Materialised into phis
// when the target block is finalised.
incoming map[*ssa.Block][]incomingEdge
// unreachable is true after an unconditional br / return / trap
// terminates the current block. Subsequent instructions are
// skipped until end/else realigns the cursor with a reachable
// successor block.
unreachable bool
// unreachableDepth counts block/loop/if frames opened *inside* a
// dead (unreachable) region. Their matching `end`s are consumed
// without touching the live control stack.
unreachableDepth int
// mutableLocals selects the de-SSA local mode used for functions that
// contain an EH `try`: local.get/set become retained OpLocalGet/
// OpLocalSet (mutable Go vars) instead of SSA values, and locals are
// not threaded through block phis.
mutableLocals bool
// inlineInfo is the module-wide summary the wasm-level inliner
// consults (see inline.go); inlineFrames tracks in-progress inline
// expansions (`return` inside one branches to its frame instead of
// sealing a BlockRet); inlinedBytes is the per-caller growth budget
// spent so far.
inlineInfo *inlineAnalysis
inlineFrames []inlineFrame
inlinedBytes int
callerIdx uint32 // absolute funcIdx of the function being lowered
}
// ctrlFrame is one entry on the wasm control stack.
type ctrlFrame struct {
kind ctrlKind
// target: where `br N` whose depth resolves to this frame jumps.
// For block / if: the post-block (continuation after end).
// For loop: the header block (back-edge target).
target *ssa.Block
// fallthrough: where the END instruction transitions to when
// reached via fall-through (i.e. the bottom of the structured
// region). For block / if: the post-block. For loop: the post-
// loop block.
fallthrough_ *ssa.Block
// elseBlk: for if frames, the else branch entry block.
elseBlk *ssa.Block
// haveElse: for if frames, true once `else` has been seen so the
// matching `end` knows to use elseBlk's snapshot instead of the
// then-side one.
haveElse bool
// resultCount: arity of values the block produces at fall-through
// (result-arity, not param-arity).
resultCount int
// stackHeightAtEntry is the operand-stack height at the moment the
// frame opened, AFTER popping any block-type parameters. End / br
// truncate the stack back to this height + resultCount values.
stackHeightAtEntry int
// entryLocals is a snapshot of the per-local SSA values at the
// moment the frame opened. The else-branch of an if/else must
// start from this snapshot (not the then-branch's mutated state),
// and an if-without-else's implicit else edge carries it verbatim.
entryLocals []*ssa.Value
// (see eh.go for the branch-based EH model these support)
// tryRegion, for ctrlTry frames, is the in-progress EH region. Entry
// and Post are set when the frame opens; each catch / catch_all
// appends a handler. target / fallthrough_ both point at Post.
tryRegion *ssa.TryRegion
// excChain, for ctrlTry frames, is where the next catch clause's tag
// test goes: the dispatch block for the first clause, then the
// fall-through of each previous test. Nil once a catch_all has
// swallowed the chain (or the chain has been closed).
excChain *ssa.Block
// excTagVal / excSavedTag / excSavedVals are the dispatch's snapshot of
// the arriving exception: the tag the clause chain compares, and the
// tag+operands a `rethrow` naming this try reproduces.
excTagVal *ssa.Value
excSavedTag *ssa.Value
excSavedVals []*ssa.Value
// excDispatchLocals is the local set the dispatch block runs with, so
// every clause (and the out-of-line no-match edge) starts from it.
excDispatchLocals []*ssa.Value
// excHasCatchAll records that a catch_all clause terminated the chain.
excHasCatchAll bool
// excDead marks a dispatch no check-and-branch can reach (the protected
// body cannot raise), so its handlers lower as dead code.
excDead bool
// excDispatch, for ctrlTry frames, is the block a check-and-branch in
// the protected body targets when the flag is set: it clears the flag,
// reads the tag and runs the clause chain. See eh.go.
excDispatch *ssa.Block
// inHandler, for ctrlTry frames, is true once the first catch /
// catch_all has been seen: lowering is inside a handler, not the
// protected body. `rethrow l` labels must resolve to such a frame, and
// `delegate l` targeting one keeps searching outward (a try whose
// handler is already running cannot catch again).
inHandler bool
}
type ctrlKind uint8
const (
ctrlBlock ctrlKind = iota + 1
ctrlLoop
ctrlIf
ctrlTry
)
// branchArity returns the number of operand-stack values a branch (br /
// br_if / br_table) targeting this frame carries. For a block or if the
// branch goes to the post-block, so the arity is the result arity. For a
// loop the branch goes to the HEADER, which re-consumes the loop's
// PARAMETERS, so the arity is the loop's param count — currently always 0
// because decodeBlockResults only accepts inline value-type blocktypes
// (which have no params). When function-type-index blocktypes (which can
// carry params) are supported, store the loop's param count on the frame
// and return it here.
func (f *ctrlFrame) branchArity() int {
if f.kind == ctrlLoop {
return 0
}
return f.resultCount
}
// incomingEdge captures the locals + result-stack snapshot of a
// predecessor at the moment it branches into a target block.
type incomingEdge struct {
from *ssa.Block
locals []*ssa.Value
stack []*ssa.Value // size = target block's expected param/result arity
}
// lowerBody walks the wasm bytecode r and emits SSA into ls.b.
func (ls *lowerState) lowerBody(r *wasm.InstrReader) error {
return ls.lowerBodyUntil(r, -1)
}
// lowerBodyUntil is lowerBody parametrised for inline expansion:
// baseCtrl < 0 is the top-level function body (terminates when the
// function-level End fires); baseCtrl >= 0 lowers an inlined callee
// body, terminating as soon as an `end` pops the control stack back
// down to baseCtrl (the callee's implicit function-level end popping
// the inline frame pushed by lowerInlineCall).
func (ls *lowerState) lowerBodyUntil(r *wasm.InstrReader, baseCtrl int) error {
// depth tracks the open structured-control nesting. The function
// body itself is depth 0; the implicit function-level End terminates
// at depth 0.
for !r.EOF() {
op, err := r.ReadByte()
if err != nil {
return err
}
if ls.unreachable {
// Skip ops in the dead region until the structured
// boundary that re-establishes a reachable cursor. Nested
// block/loop/if opened *within* the dead region must be
// counted so their matching `end` is consumed here and
// not mistaken for the end of an enclosing live frame
// (which would pop the wrong ctrlFrame and corrupt the
// control stack).
switch op {
case wasm.OpBlock, wasm.OpLoop, wasm.OpIf, wasm.OpTry:
ls.unreachableDepth++
if err := r.SkipImmediates(op); err != nil {
return fmt.Errorf("%w: skipping unreachable block-open 0x%02x: %w", ErrSSAUnsupported, op, err)
}
case wasm.OpElse:
if ls.unreachableDepth > 0 {
// `else` of a dead-region if — stay dead.
break
}
if err := ls.handleElse(); err != nil {
return err
}
case wasm.OpCatch, wasm.OpCatchAll:
if ls.unreachableDepth > 0 {
// catch of a dead-region try — stay dead (skip the
// tag immediate for catch).
if err := r.SkipImmediates(op); err != nil {
return err
}
break
}
// A catch at the current try's depth re-establishes a
// reachable cursor: the body/prior-handler ended dead
// (e.g. threw), and this handler is a fresh landing pad.
if op == wasm.OpCatch {
if err := ls.handleCatch(r); err != nil {
return err
}
} else if err := ls.handleCatchAll(); err != nil {
return err
}
case wasm.OpDelegate:
// `delegate` closes its try like an `end` does: a
// dead-region-opened try just drops the depth count; a
// delegate at the live frame's depth must pop the open
// ctrlTry frame (handleDelegate re-establishes a
// reachable cursor at Post when it has incoming edges).
if ls.unreachableDepth > 0 {
ls.unreachableDepth--
if err := r.SkipImmediates(op); err != nil {
return err
}
break
}
if err := ls.handleDelegate(r); err != nil {
return err
}
case wasm.OpEnd:
if ls.unreachableDepth > 0 {
ls.unreachableDepth--
break
}
done, err := ls.handleEnd()
if err != nil {
return err
}
if done {
return nil
}
if baseCtrl >= 0 && len(ls.ctrl) == baseCtrl {
// Inline body finished: its function-level end
// popped the inline frame.
return nil
}
default:
if err := r.SkipImmediates(op); err != nil {
return fmt.Errorf("%w: skipping unreachable opcode 0x%02x: %w", ErrSSAUnsupported, op, err)
}
}
continue
}
// Function-level End must terminate the loop directly so the
// done signal isn't swallowed by handleOp.
if op == wasm.OpEnd {
done, err := ls.handleEnd()
if err != nil {
return err
}
if done {
return nil
}
if baseCtrl >= 0 && len(ls.ctrl) == baseCtrl {
// Inline body finished (see the unreachable-path twin
// above).
return nil
}
continue
}
if err := ls.handleOp(op, r); err != nil {
return err
}
}
return fmt.Errorf("%w: function body ended without End", ErrSSAUnsupported)
}
func (ls *lowerState) handleOp(op byte, r *wasm.InstrReader) error {
switch {
case op == wasm.OpEnd:
_, err := ls.handleEnd()
return err
case op == wasm.OpElse:
return ls.handleElse()
case op == wasm.OpIf:
return ls.handleIf(r)
case op == wasm.OpBlock:
return ls.handleBlock(r)
case op == wasm.OpLoop:
return ls.handleLoop(r)
case op == wasm.OpTry:
return ls.handleTry(r)
case op == wasm.OpCatch:
return ls.handleCatch(r)
case op == wasm.OpCatchAll:
return ls.handleCatchAll()
case op == wasm.OpBr:
return ls.handleBr(r)
case op == wasm.OpBrIf:
return ls.handleBrIf(r)
case op == wasm.OpBrTable:
return ls.handleBrTable(r)
case op == wasm.OpReturn:
return ls.handleReturn()
case op == wasm.OpThrow:
return ls.handleThrow(r)
case op == wasm.OpDelegate:
return ls.handleDelegate(r)
case op == wasm.OpRethrow:
return ls.handleRethrow(r)
case op == wasm.OpLocalGet:
idx, err := r.ReadU32()
if err != nil {
return err
}
if int(idx) >= len(ls.localTypes) {
return fmt.Errorf("local.get out of range: %d", idx)
}
if ls.mutableLocals {
// Read the mutable local variable (impure — not CSE'd).
ls.push(ls.b.NewValueAuxInt(ssa.OpLocalGet, ls.localTypes[idx], int64(idx)))
return nil
}
ls.push(ls.locals[idx])
return nil
case op == wasm.OpLocalSet:
idx, err := r.ReadU32()
if err != nil {
return err
}
if int(idx) >= len(ls.localTypes) {
return fmt.Errorf("local.set out of range: %d", idx)
}
v, err := ls.pop()
if err != nil {
return err
}
if ls.mutableLocals {
// Write the mutable local variable (side-effecting; ordered
// by block-value position).
ls.b.NewValueAuxInt(ssa.OpLocalSet, ssa.TypeMem, int64(idx), v)
return nil
}
ls.locals[idx] = v
return nil
case op == wasm.OpLocalTee:
idx, err := r.ReadU32()
if err != nil {
return err
}
if int(idx) >= len(ls.localTypes) {
return fmt.Errorf("local.tee out of range: %d", idx)
}
if len(ls.stack) == 0 {
return fmt.Errorf("local.tee on empty stack")
}
if ls.mutableLocals {
// tee = set + keep the value on the stack.
ls.b.NewValueAuxInt(ssa.OpLocalSet, ssa.TypeMem, int64(idx), ls.stack[len(ls.stack)-1])
return nil
}
ls.locals[idx] = ls.stack[len(ls.stack)-1]
return nil
case op == wasm.OpI32Const:
v, err := r.ReadS32()
if err != nil {
return err
}
ls.push(ls.b.Const32(v))
return nil
case op == wasm.OpI64Const:
v, err := r.ReadS64()
if err != nil {
return err
}
ls.push(ls.b.Const64(v))
return nil
case op == wasm.OpCall:
return ls.handleCall(r)
case op == wasm.OpCallIndirect:
return ls.handleCallIndirect(r)
case op == wasm.OpGlobalGet:
idx, err := r.ReadU32()
if err != nil {
return err
}
t := ls.globalType(idx)
if t == ssa.TypeInvalid {
return fmt.Errorf("%w: global.get for unsupported global type %d", ErrSSAUnsupported, idx)
}
ls.push(ls.b.NewValueAuxInt(ssa.OpGlobalGet, t, int64(idx)))
return nil
case op == wasm.OpGlobalSet:
idx, err := r.ReadU32()
if err != nil {
return err
}
v, err := ls.pop()
if err != nil {
return err
}
ls.b.NewValueAuxInt(ssa.OpGlobalSet, ssa.TypeMem, int64(idx), v)
return nil
case isMemoryOp(op):
return ls.handleMemoryOp(op, r)
case op == wasm.OpMemSize:
// memory.size and memory.grow each carry a 1-byte memory index
// immediate (must be 0 in MVP wasm).
if _, err := r.ReadByte(); err != nil {
return err
}
szType := ssa.TypeI32
if ls.mem64 {
szType = ssa.TypeI64
}
ls.push(ls.b.NewValue(ssa.OpMemSize, szType))
return nil
case op == wasm.OpMemGrow:
if _, err := r.ReadByte(); err != nil {
return err
}
delta, err := ls.pop()
if err != nil {
return err
}
growType := ssa.TypeI32
if ls.mem64 {
growType = ssa.TypeI64
}
ls.push(ls.b.NewValue(ssa.OpMemGrow, growType, delta))
return nil
case op == wasm.OpUnreachable:
// `unreachable` is a hard trap. Seal the current block as
// Unreachable; subsequent ops are dead until the next
// structured boundary.
cur := ls.b.Current()
cur.Kind = ssa.BlockUnreachable
ls.unreachable = true
return nil
case op == wasm.OpNop:
return nil
case op == wasm.OpDrop:
_, err := ls.pop()
return err
case op == wasm.OpSelect, op == wasm.OpSelectT:
return ls.handleSelect(op, r)
case op == wasm.OpF32Const:
v, err := r.ReadF32()
if err != nil {
return err
}
ls.push(ls.b.NewValueAuxInt(ssa.OpConstF32, ssa.TypeF32, int64(math.Float32bits(v))))
return nil
case op == wasm.OpF64Const:
v, err := r.ReadF64()
if err != nil {
return err
}
ls.push(ls.b.NewValueAuxInt(ssa.OpConstF64, ssa.TypeF64, int64(math.Float64bits(v))))
return nil
case isSimpleBinary(op):
rhs, err := ls.pop()
if err != nil {
return err
}
lhs, err := ls.pop()
if err != nil {
return err
}
ssaOp, ssaType := simpleBinaryOp(op)
// gt / ge wasm ops compile to the same SSA op as lt / le with
// swapped arguments. simpleBinaryOp signals this via the
// inverted mapping; we swap here.
if isGtGe(op) {
lhs, rhs = rhs, lhs
}
ls.push(ls.b.NewValue(ssaOp, ssaType, lhs, rhs))
return nil
case isHelperBinary(op):
rhs, err := ls.pop()
if err != nil {
return err
}
lhs, err := ls.pop()
if err != nil {
return err
}
spec := helperBinarySpec(op)
ls.push(ls.b.NewValueAux(ssa.OpHelperCall, spec.resType, spec.helper, lhs, rhs))
return nil
case op == wasm.OpI32Eqz || op == wasm.OpI64Eqz:
// eqz is a first-class comparison against zero, not a helper
// call: OpEq{32,64}(x, 0) with a TypeBool result. This lets a
// branch on the result fuse into a direct Go comparison
// (`x == 0`) via emitBoolCond instead of the historical
// `I32_eqz(x) != 0` helper round-trip — measured at ~10% of
// the SpiderMonkey interpreter's hot loop — and enrolls eqz
// in ConstProp/CSE like every other comparison. Value uses
// render as b2i32(x == 0), matching wasm's i32 0/1 result.
x, err := ls.pop()
if err != nil {
return err
}
if op == wasm.OpI32Eqz {
ls.push(ls.b.NewValue(ssa.OpEq32, ssa.TypeBool, x, ls.b.Const32(0)))
} else {
ls.push(ls.b.NewValue(ssa.OpEq64, ssa.TypeBool, x, ls.b.Const64(0)))
}
return nil
case isHelperUnary(op):
x, err := ls.pop()
if err != nil {
return err
}
spec := helperUnarySpec(op)
ls.push(ls.b.NewValueAux(ssa.OpHelperCall, spec.resType, spec.helper, x))
return nil
case op == wasm.OpPrefixFC:
return ls.handleFCOp(r)
case op == wasm.OpPrefixFD:
return ls.handleFDOp(r)
case op == wasm.OpPrefixFE:
return ls.handleFEOp(r)
}
return fmt.Errorf("%w: opcode 0x%02x not implemented", ErrSSAUnsupported, op)
}
// handleSelect implements the select / select-typed opcodes. Both pop
// (then-val, else-val, cond) — typed select additionally carries a 1-vec
// of value types we currently ignore (the polymorphic check is left to
// validation upstream).
func (ls *lowerState) handleSelect(op byte, r *wasm.InstrReader) error {
if op == wasm.OpSelectT {
n, err := r.ReadU32()
if err != nil {
return err
}
for i := uint32(0); i < n; i++ {
if _, err := r.ReadByte(); err != nil {
return err
}
}
}
cond, err := ls.pop()
if err != nil {
return err
}
elseVal, err := ls.pop()
if err != nil {
return err
}
thenVal, err := ls.pop()
if err != nil {
return err
}
// Lower as: thenBlk if cond != 0 else elseBlk; phi at join. This
// matches what an if/then/else with one result would produce, so the
// existing emitter handles it without further changes.
thenBlk := ls.b.NewBlock(ssa.BlockPlain)
elseBlk := ls.b.NewBlock(ssa.BlockPlain)
postBlk := ls.b.NewBlock(ssa.BlockPlain)
cur := ls.b.Current()
cur.Kind = ssa.BlockIf
cur.Control = cond
ssa.AddEdge(cur, thenBlk)
ssa.AddEdge(cur, elseBlk)
ssa.AddEdge(thenBlk, postBlk)
ssa.AddEdge(elseBlk, postBlk)
ls.b.SetCurrent(postBlk)
phi := ls.b.NewValue(ssa.OpPhi, thenVal.Type, thenVal, elseVal)
ls.push(phi)
return nil
}
// feAtomicSpec describes one threads-proposal atomic op: the module-aware
// helper it lowers to, its result type, and how many value operands it pops
// beyond the address (0 = load, 1 = store/RMW/notify, 2 = cmpxchg/wait).
type feAtomicSpec struct {
helper string
resType ssa.Type
extra int
// wait64's expected operand and cmpxchg64's operands are i64; everything
// else extra-operand-wise is inferred from resType except waits (i32
// result, typed operands) — recorded explicitly where it matters.
}
// feAtomics maps 0xFE sub-opcodes to their lowering. Sub-opcode layout is
// fixed by the threads proposal: 0x00 notify, 0x01/0x02 wait32/64,
// 0x03 fence, 0x10.. loads, 0x17.. stores, then RMW families in op order
// add/sub/and/or/xor/xchg/cmpxchg, each over
// {i32, i64, i32_8u, i32_16u, i64_8u, i64_16u, i64_32u}.
var feAtomics = map[uint32]feAtomicSpec{
0x00: {"atomicNotify", ssa.TypeI32, 1},
0x01: {"atomicWait32", ssa.TypeI32, 2},
0x02: {"atomicWait64", ssa.TypeI32, 2},
0x10: {"atomicLoad32", ssa.TypeI32, 0},
0x11: {"atomicLoad64", ssa.TypeI64, 0},
0x12: {"atomicLoad32_8u", ssa.TypeI32, 0},
0x13: {"atomicLoad32_16u", ssa.TypeI32, 0},
0x14: {"atomicLoad64_8u", ssa.TypeI64, 0},
0x15: {"atomicLoad64_16u", ssa.TypeI64, 0},
0x16: {"atomicLoad64_32u", ssa.TypeI64, 0},
0x17: {"atomicStore32", ssa.TypeI32, 1},
0x18: {"atomicStore64", ssa.TypeI32, 1},
0x19: {"atomicStore32_8", ssa.TypeI32, 1},
0x1a: {"atomicStore32_16", ssa.TypeI32, 1},
0x1b: {"atomicStore64_8", ssa.TypeI32, 1},
0x1c: {"atomicStore64_16", ssa.TypeI32, 1},
0x1d: {"atomicStore64_32", ssa.TypeI32, 1},
}
func init() {
// The seven RMW families share the size/type layout of the load family.
families := []struct {
base uint32
name string
}{
{0x1e, "Add"}, {0x25, "Sub"}, {0x2c, "And"}, {0x33, "Or"},
{0x3a, "Xor"}, {0x41, "Xchg"}, {0x48, "Cmpxchg"},
}
sizes := []struct {
suffix string
typ ssa.Type
}{
{"32", ssa.TypeI32}, {"64", ssa.TypeI64},
{"32_8u", ssa.TypeI32}, {"32_16u", ssa.TypeI32},
{"64_8u", ssa.TypeI64}, {"64_16u", ssa.TypeI64}, {"64_32u", ssa.TypeI64},
}
for _, f := range families {
for i, sz := range sizes {
extra := 1
if f.name == "Cmpxchg" {
extra = 2
}
feAtomics[f.base+uint32(i)] = feAtomicSpec{
"atomicRmw" + f.name + sz.suffix, sz.typ, extra,
}
}
}
}
// handleFEOp dispatches the wasm 0xFE threads-proposal opcodes. Every memory
// atomic lowers to an OpAtomicCall of a module-aware helper taking
// (m, addr, offset, operands...); alignment and bounds are checked in the
// helper (misalignment traps, as the proposal requires).
func (ls *lowerState) handleFEOp(r *wasm.InstrReader) error {
sub, err := r.ReadU32()
if err != nil {
return err
}
if sub == 0x03 { // atomic.fence: reserved byte, no operands
if _, err := r.ReadByte(); err != nil {
return err
}
ls.b.NewValueAux(ssa.OpAtomicCall, ssa.TypeI32, "atomicFence")
return nil
}
spec, ok := feAtomics[sub]
if !ok {
return fmt.Errorf("%w: 0xfe sub-opcode 0x%02x not implemented", ErrSSAUnsupported, sub)
}
if _, err := r.ReadU32(); err != nil { // memarg align (validated upstream)
return err
}
// On a memory64 module the memarg offset is a u64, the address operand
// is an i64, and the op routes to the helper's _m64 twin (i64 address
// and offset, u64 effective-address math). atomic.fence above takes no
// address and needs no twin.
helper := spec.helper
var offVal *ssa.Value
if ls.mem64 {
offset, err := r.ReadU64()
if err != nil {
return err
}
offVal = ls.b.Const64(int64(offset))
helper += "_m64"
} else {
offset, err := r.ReadU32()
if err != nil {
return err
}
offVal = ls.b.Const32(int32(offset))
}
extras := make([]*ssa.Value, spec.extra)
for i := spec.extra - 1; i >= 0; i-- {
v, err := ls.pop()
if err != nil {
return err
}
extras[i] = v
}
addr, err := ls.pop()
if err != nil {
return err
}
args := append([]*ssa.Value{addr, offVal}, extras...)
v := ls.b.NewValueAux(ssa.OpAtomicCall, spec.resType, helper, args...)
if strings.HasPrefix(spec.helper, "atomicStore") {
return nil // stores leave nothing on the stack
}
ls.push(v)
return nil
}
// handleFCOp dispatches the wasm 0xFC prefix opcodes: saturating-trunc
// conversions and bulk-memory ops.
func (ls *lowerState) handleFCOp(r *wasm.InstrReader) error {
sub, err := r.ReadU32()
if err != nil {
return err
}
switch sub {
case 0, 1, 2, 3, 4, 5, 6, 7:
// i32.trunc_sat_f32_s / _u, i32.trunc_sat_f64_s / _u,
// i64.trunc_sat_f32_s / _u, i64.trunc_sat_f64_s / _u.
specs := []helperUnary{
{"i32_trunc_sat_f32_s", ssa.TypeI32},
{"i32_trunc_sat_f32_u", ssa.TypeI32},
{"i32_trunc_sat_f64_s", ssa.TypeI32},
{"i32_trunc_sat_f64_u", ssa.TypeI32},
{"i64_trunc_sat_f32_s", ssa.TypeI64},
{"i64_trunc_sat_f32_u", ssa.TypeI64},
{"i64_trunc_sat_f64_s", ssa.TypeI64},
{"i64_trunc_sat_f64_u", ssa.TypeI64},
}
x, err := ls.pop()
if err != nil {
return err
}
s := specs[sub]
ls.push(ls.b.NewValueAux(ssa.OpHelperCall, s.resType, s.helper, x))
return nil
case 8: // memory.init
segIdx, err := r.ReadU32()
if err != nil {
return err
}
if _, err := r.ReadByte(); err != nil { // memory index (must be 0)
return err
}
n, err := ls.pop()
if err != nil {
return err
}
src, err := ls.pop()
if err != nil {
return err
}
dst, err := ls.pop()
if err != nil {
return err
}
ls.b.NewValueAuxInt(ssa.OpMemoryInit, ssa.TypeMem, int64(segIdx), dst, src, n)
return nil
case 9: // data.drop
segIdx, err := r.ReadU32()
if err != nil {
return err
}
ls.b.NewValueAuxInt(ssa.OpDataDrop, ssa.TypeMem, int64(segIdx))
return nil
case 10: // memory.copy
if _, err := r.ReadByte(); err != nil { // dst mem index
return err
}
if _, err := r.ReadByte(); err != nil { // src mem index
return err
}
n, err := ls.pop()
if err != nil {
return err
}
src, err := ls.pop()
if err != nil {
return err
}
dst, err := ls.pop()
if err != nil {
return err
}
ls.b.NewValue(ssa.OpMemoryCopy, ssa.TypeMem, dst, src, n)
return nil
case 11: // memory.fill
if _, err := r.ReadByte(); err != nil {
return err
}
n, err := ls.pop()
if err != nil {
return err
}
val, err := ls.pop()
if err != nil {
return err
}
dst, err := ls.pop()
if err != nil {
return err
}
ls.b.NewValue(ssa.OpMemoryFill, ssa.TypeMem, dst, val, n)
return nil
}
return fmt.Errorf("%w: prefix 0xfc sub-opcode %d not implemented", ErrSSAUnsupported, sub)
}
// handleBlock implements `block blocktype ... end`. The block opens a
// new scope whose `br 0` target is the post-block; the body runs in the
// same SSA basic block until something branches.
func (ls *lowerState) handleBlock(r *wasm.InstrReader) error {
bt, err := r.ReadS33()
if err != nil {
return err
}
results, err := decodeBlockResults(bt)
if err != nil {
return fmt.Errorf("%w: block blocktype %v: %w", ErrSSAUnsupported, bt, err)
}
postBlk := ls.b.NewBlock(ssa.BlockPlain)
ls.ctrl = append(ls.ctrl, &ctrlFrame{
kind: ctrlBlock,
target: postBlk,
fallthrough_: postBlk,
resultCount: len(results),
stackHeightAtEntry: len(ls.stack),
})
return nil
}
// handleLoop implements `loop blocktype ... end`. The loop header is a
// new SSA block reached both from the entry edge and from any
// back-branch (br targeting depth 0). To support back-edges with
// SSA-correct local values we install Braun-style "incomplete phis" at
// the header: one per local, initially carrying the entry-edge value;
// further args are appended as branches into the header land.
func (ls *lowerState) handleLoop(r *wasm.InstrReader) error {
bt, err := r.ReadS33()
if err != nil {
return err
}
results, err := decodeBlockResults(bt)
if err != nil {
return fmt.Errorf("%w: loop blocktype %v: %w", ErrSSAUnsupported, bt, err)
}
header := ls.b.NewBlock(ssa.BlockPlain)
postBlk := ls.b.NewBlock(ssa.BlockPlain)
// Wire current → header as the first edge in. Snapshot is the
// pre-loop state.
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, header)
// Move into header. Install one incomplete-phi value per local
// (Args initially just the entry-edge value; appended as more
// preds arrive). Stack values at this point belong to the
// surrounding scope and stay as-is.
ls.b.SetCurrent(header)
// In mutable-locals mode (try functions) locals live in mutable Go vars,
// not SSA values, so the loop header needs no local phis — the back-edge
// carries them through the vars. (Loops carry no stack params today, so
// the header then needs no phis at all.)
if !ls.mutableLocals {
headerPhis := make([]*ssa.Value, len(ls.locals))
for i, v := range ls.locals {
phi := ls.b.NewValue(ssa.OpPhi, ls.localTypes[i], v)
headerPhis[i] = phi
}
ls.locals = headerPhis
}
ls.ctrl = append(ls.ctrl, &ctrlFrame{
kind: ctrlLoop,
target: header,
fallthrough_: postBlk,
resultCount: len(results),
stackHeightAtEntry: len(ls.stack),
})
return nil
}
// handleTry opens an EH try region. The protected body starts in a fresh
// entry block (a clean boundary the emitter wraps in defer/recover); `end`
// and `br N` targeting this frame go to the post block. A try implies
// mutable-locals mode (set in LowerFunction), so locals are not phi'd here.
func (ls *lowerState) handleTry(r *wasm.InstrReader) error {
bt, err := r.ReadS33()
if err != nil {
return err
}
results, err := decodeBlockResults(bt)
if err != nil {
return fmt.Errorf("%w: try blocktype %v: %w", ErrSSAUnsupported, bt, err)
}
ls.handleTryBranch(len(results))
return nil
}
// handleDelegate closes a `try ... delegate N` — a try whose protected body
// forwards any exception to the catching context of the N-th enclosing label,
// skipping every try nested between the two. We close the body into post with
// NO handler and record the resolved forwarding target on the region:
// label depth N (relative to the context enclosing the try) is walked on the
// control stack, and the catching context is the nearest enclosing try that is
// still in its protected body at or outside that label (a try whose handler is
// already running cannot catch again). No frame at all means the exception
// leaves the function. Mirrors the ctrlTry arm of handleEnd, minus any handler.
func (ls *lowerState) handleDelegate(r *wasm.InstrReader) error {
depth, err := r.ReadU32() // labelidx, relative to the try's outer context
if err != nil {
return err
}
if len(ls.ctrl) == 0 || ls.ctrl[len(ls.ctrl)-1].kind != ctrlTry {
return fmt.Errorf("%w: delegate with no open try", ErrSSAUnsupported)
}
f := ls.ctrl[len(ls.ctrl)-1]
ls.ctrl = ls.ctrl[:len(ls.ctrl)-1]
ls.captureTryBody(f)
f.tryRegion.Delegate = true
// The body's check-and-branches targeted THIS region's dispatch (it was
// the innermost). Forward from there: the state is untouched, so the
// dispatch just branches on to the resolved target — which the popped
// control stack makes currentExcTarget resolve to, except when the
// label named a specific outer try.
forwardFrom := f.excDispatch
// Resolve the label to its catching context. The label indexes the
// control stack with the delegate's own try frame already popped; depth
// == len(ls.ctrl) names the function body (forward to the caller).
if int(depth) < len(ls.ctrl) {
for i := len(ls.ctrl) - 1 - int(depth); i >= 0; i-- {
fr := ls.ctrl[i]
if fr.kind == ctrlTry && !fr.inHandler && fr.tryRegion != nil {
f.tryRegion.DelegateTarget = fr.tryRegion
break
}
}
}
if !ls.unreachable {
ls.recordIncoming(f.target, f.resultCount)
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.target)
}
// Forward the region's exceptions. The delegate's own frame is popped,
// so a label naming the enclosing context resolves through
// currentExcTarget; a label naming a specific outer try (skipping trys
// nested in between) is honoured by targeting that try's dispatch
// directly.
if len(forwardFrom.Preds) > 0 {
saved := ls.b.Current()
savedUnreachable := ls.unreachable
ls.b.SetCurrent(forwardFrom)
ls.unreachable = false
if err := ls.resolveIncoming(forwardFrom, 0, f.stackHeightAtEntry); err != nil {
return err
}
ls.stack = ls.stack[:f.stackHeightAtEntry]
if tr := f.tryRegion.DelegateTarget; tr != nil && tr.Dispatch != nil {
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ls.recordIncoming(tr.Dispatch, 0)
ssa.AddEdge(cur, tr.Dispatch)
ls.unreachable = true
} else {
ls.gotoExcTarget()
}
ls.b.SetCurrent(saved)
ls.unreachable = savedUnreachable
}
sealDeadExcBlocks(f.tryRegion)
if _, ok := ls.incoming[f.target]; ok {
ls.b.SetCurrent(f.target)
ls.unreachable = false
if err := ls.resolveIncoming(f.target, f.resultCount, f.stackHeightAtEntry); err != nil {
return err
}
}
return nil
}
// captureTryBody records, once, a try region's protected-body block set:
// Entry plus every block created while the try was open (nested regions
// included), minus Post. Called when the body closes — at the first catch /
// catch_all, at delegate, or at a handler-less end. The CFG alone cannot
// delimit the body (a br exiting the try early makes its continuation
// reachable from inside), so the emit passes rely on this structural record.
func (ls *lowerState) captureTryBody(f *ctrlFrame) {
tr := f.tryRegion
if tr == nil || tr.Body != nil {
return
}
blocks := ls.b.Func().Blocks
start := -1
for i, b := range blocks {
if b == tr.Entry {
start = i
break
}
}
if start < 0 {
return
}
body := []*ssa.Block{tr.Entry}
for _, b := range blocks[start+1:] {
if b != tr.Post {
body = append(body, b)
}
}
tr.Body = body
}
// handleRethrow implements `rethrow N` — re-raise the exception caught by
// the handler the N-th enclosing label resolves to. Handlers nest, so the
// label does not always name the innermost catch: `rethrow 1` inside an
// inner catch re-raises the OUTER try's exception. The dispatch snapshotted
// tag and operands before any handler body ran, so the re-raise reproduces
// them exactly even if the handler has since called throwing code.
func (ls *lowerState) handleRethrow(r *wasm.InstrReader) error {
depth, err := r.ReadU32() // labelidx; must name a try frame in handler state
if err != nil {
return err
}
if int(depth) >= len(ls.ctrl) {
return fmt.Errorf("%w: rethrow depth %d exceeds control stack", ErrSSAUnsupported, depth)
}
fr := ls.ctrl[len(ls.ctrl)-1-int(depth)]
if fr.kind != ctrlTry || !fr.inHandler || fr.excSavedTag == nil {
return fmt.Errorf("%w: rethrow label does not name a catch handler", ErrSSAUnsupported)
}
ls.raiseAndGo(fr.excSavedTag, fr.excSavedVals...)
return nil
}
func (ls *lowerState) handleCatch(r *wasm.InstrReader) error {
tagIdx, err := r.ReadU32()
if err != nil {
return err
}
return ls.beginHandler(false, tagIdx)
}
func (ls *lowerState) handleCatchAll() error {
return ls.beginHandler(true, 0)
}
// beginHandler closes the current sub-region (the protected body, or the
// previous clause) into post, then opens the next catch / catch_all clause
// on the dispatch's tag-compare chain. For `catch`, the tag's operands are
// pushed as reads of the snapshotted exception state.
func (ls *lowerState) beginHandler(catchAll bool, tagIdx uint32) error {
if len(ls.ctrl) == 0 || ls.ctrl[len(ls.ctrl)-1].kind != ctrlTry {
return fmt.Errorf("%w: catch/catch_all with no open try", ErrSSAUnsupported)
}
f := ls.ctrl[len(ls.ctrl)-1]
ls.captureTryBody(f) // no-op after the first handler
// Close the current sub-region into post (skip if it threw/branched away).
if !ls.unreachable {
ls.recordIncoming(f.target, f.resultCount)
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.target)
}
f.inHandler = true
hBlk, err := ls.beginClause(f, catchAll, tagIdx)
if err != nil {
return err
}
nArgs := 0
if !catchAll {
types, err := ls.tagOperandTypes(tagIdx)
if err != nil {
return fmt.Errorf("%w: %w", ErrSSAUnsupported, err)
}
for i, t := range types {
if i >= len(f.excSavedVals) {
return fmt.Errorf("%w: tag %d arity %d exceeds the module's %d exception slots",
ErrSSAUnsupported, tagIdx, len(types), len(f.excSavedVals))
}
ls.push(ls.b.NewValueAuxInt(ssa.OpCatchArg, t, int64(i), f.excSavedVals[i]))
}
nArgs = len(types)
}
f.tryRegion.Handlers = append(f.tryRegion.Handlers, ssa.TryHandler{
CatchAll: catchAll,
TagIndex: tagIdx,
Block: hBlk,
NumArgs: nArgs,
})
return nil
}
// tagOperandTypes returns the SSA operand types the exception tag carries.
func (ls *lowerState) tagOperandTypes(tagIdx uint32) ([]ssa.Type, error) {
params, err := ls.tagOperandParams(tagIdx)
if err != nil {
return nil, err
}
out := make([]ssa.Type, len(params))
for i, p := range params {
t := toSSAType(p)
if t == ssa.TypeInvalid {
return nil, fmt.Errorf("tag %d operand %d has unsupported type %v", tagIdx, i, p)
}
out[i] = t
}
return out, nil
}
// handleBr implements unconditional `br N`. The Nth-enclosing frame's
// target receives an edge; the current block is sealed Plain → target,
// and execution becomes unreachable until the next structured boundary.
func (ls *lowerState) handleBr(r *wasm.InstrReader) error {
depth, err := r.ReadU32()
if err != nil {
return err
}
if int(depth) >= len(ls.ctrl) {
return fmt.Errorf("br %d: out of range (ctrl depth %d)", depth, len(ls.ctrl))
}
frame := ls.ctrl[len(ls.ctrl)-1-int(depth)]
ls.recordIncoming(frame.target, frame.branchArity())
// For loops, `br 0` is a back-edge into the header phi. Each
// header local phi gets one more incoming arg = current value.
if frame.kind == ctrlLoop {
ls.appendLoopBackArgs(frame.target)
}
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, frame.target)
ls.unreachable = true
return nil
}
// handleBrIf implements conditional `br_if N`. Behaves like br when
// cond != 0 else falls through. The current block becomes an If
// terminator: then-succ = frame.target, else-succ = a new continuation
// block. The current block tail is sealed; subsequent ops emit into the
// continuation.
func (ls *lowerState) handleBrIf(r *wasm.InstrReader) error {
depth, err := r.ReadU32()
if err != nil {
return err
}
if int(depth) >= len(ls.ctrl) {
return fmt.Errorf("br_if %d: out of range (ctrl depth %d)", depth, len(ls.ctrl))
}
cond, err := ls.pop()
if err != nil {
return err
}
frame := ls.ctrl[len(ls.ctrl)-1-int(depth)]
cont := ls.b.NewBlock(ssa.BlockPlain)
// Snapshot for the taken edge.
ls.recordIncoming(frame.target, frame.branchArity())
if frame.kind == ctrlLoop {
ls.appendLoopBackArgs(frame.target)
}
cur := ls.b.Current()
cur.Kind = ssa.BlockIf
cur.Control = cond
ssa.AddEdge(cur, frame.target)
ssa.AddEdge(cur, cont)
ls.b.SetCurrent(cont)
return nil
}
// handleBrTable implements `br_table cases* default`. The selector is
// the i32 on top of the operand stack: if 0 <= sel < len(cases), branch
// to cases[sel]; otherwise branch to default. All targets share the same
// result arity (validated by wasm), so the same top-K stack values are
// passed as the branch payload regardless of which target is taken.
//
// Arity-0 tables (no branch payload) lower to a single BlockBrTable —
// one successor per unique target, selector as Control — which the
// pure emitter renders as a Go switch statement and the gcasm backend
// as a binary-search compare tree. This matters: the Go compiler lowers
// dense switch STATEMENTS to jump tables but never reconstructs a
// switch from an if-else cascade, so the previous chain-of-Ifs
// lowering executed O(len(cases)) sequential compares per dispatch
// (CPython's eval loop: a 255-deep chain, ~127 compares per bytecode).
//
// Payload-carrying tables (branch arity > 0) still lower to the chain
// of If blocks
//
// if sel == 0 { goto cases[0] } else if sel == 1 { goto cases[1] }
// ... else { goto default }
//
// which the goto-based SSA emitter handles natively.
func (ls *lowerState) handleBrTable(r *wasm.InstrReader) error {
cases, err := r.ReadVecU32()
if err != nil {
return err
}
defaultDepth, err := r.ReadU32()
if err != nil {
return err
}
sel, err := ls.pop()
if err != nil {
return err
}
// Validate every depth before we mutate any CFG state, so a bad
// depth surfaces as a clean error instead of a half-constructed
// chain of If blocks.
maxDepth := int(defaultDepth)
for _, d := range cases {
if int(d) > maxDepth {
maxDepth = int(d)
}
}
if maxDepth >= len(ls.ctrl) {
return fmt.Errorf("br_table: depth %d out of range (ctrl depth %d)", maxDepth, len(ls.ctrl))
}
// Validate that every target (cases + default) carries the same
// BRANCH arity. The wasm spec requires this — without the check a
// malformed input would silently miscompile by passing the wrong
// stack-top values through one of the branches. NOTE: the branch
// arity is per-frame-kind — for a loop it is the loop's PARAMETER
// arity (a branch jumps to the header, which re-consumes params),
// NOT its result arity. Using resultCount here would wrongly reject
// valid br_tables that mix a `loop (result T)` (result arity 1, param
// arity 0) with a `block` of result arity 0 — exactly what a
// computed-goto dispatch (e.g. a bytecode interpreter loop) emits.
defFrame := ls.ctrl[len(ls.ctrl)-1-int(defaultDepth)]
wantArity := defFrame.branchArity()
for i, d := range cases {
f := ls.ctrl[len(ls.ctrl)-1-int(d)]
if f.branchArity() != wantArity {
return fmt.Errorf("%w: br_table target %d has branch arity %d, default has %d",
ErrSSAUnsupported, i, f.branchArity(), wantArity)
}
}
// Arity-0 dispatch — the shape a bytecode interpreter's computed
// goto compiles to, and the hot path this lowering exists for —
// becomes a single BlockBrTable with one edge per UNIQUE target.
// The pure emitter turns it into a Go switch (which the Go
// compiler lowers to a jump table) and the gcasm backend into a
// binary-search compare tree; the old equality-If chain cost
// O(len(cases)) sequential compare+branch pairs per dispatch —
// measured at a 255-deep chain on CPython's eval loop, ~127
// compares per bytecode executed.
//
// Payload-carrying br_tables (branch arity > 0) keep the If-chain
// lowering below: their per-edge stack payload rides the same
// recordIncoming machinery, but they are rare and never the hot
// dispatch shape.
if wantArity == 0 {
cur := ls.b.Current()
cur.Kind = ssa.BlockBrTable
cur.Control = sel
succIdx := map[*ssa.Block]int{}
addTarget := func(depth uint32) int {
frame := ls.ctrl[len(ls.ctrl)-1-int(depth)]
if si, ok := succIdx[frame.target]; ok {
return si
}
si := len(cur.Succs)
succIdx[frame.target] = si
ls.recordIncoming(frame.target, 0)
if frame.kind == ctrlLoop {
ls.appendLoopBackArgs(frame.target)
}
ssa.AddEdge(cur, frame.target)
cur.TableCases = append(cur.TableCases, nil)
return si
}
for i, depth := range cases {
si := addTarget(depth)
cur.TableCases[si] = append(cur.TableCases[si], int32(i))
}
cur.TableDefault = addTarget(defaultDepth)
ls.unreachable = true
return nil
}
// Emit one If block per case index. The fall-through path of each
// If becomes the predecessor of the next comparison.
for i, depth := range cases {
frame := ls.ctrl[len(ls.ctrl)-1-int(depth)]
cont := ls.b.NewBlock(ssa.BlockPlain)
ls.recordIncoming(frame.target, frame.branchArity())
if frame.kind == ctrlLoop {
ls.appendLoopBackArgs(frame.target)
}
eq := ls.b.NewValue(ssa.OpEq32, ssa.TypeBool, sel, ls.b.Const32(int32(i)))
cur := ls.b.Current()
cur.Kind = ssa.BlockIf
cur.Control = eq
ssa.AddEdge(cur, frame.target)
ssa.AddEdge(cur, cont)
ls.b.SetCurrent(cont)
}
// Default branch — the final block is sealed as Plain → default.
frame := ls.ctrl[len(ls.ctrl)-1-int(defaultDepth)]
ls.recordIncoming(frame.target, frame.branchArity())
if frame.kind == ctrlLoop {
ls.appendLoopBackArgs(frame.target)
}
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, frame.target)
ls.unreachable = true
return nil
}
// handleReturn implements explicit `return` — drains the operand stack
// of nResults values and routes them as the function result, then
// marks the cursor unreachable. The function-level End will see the
// previously-Ret block and not double-emit.
// handleThrow lowers `throw <tag>`: it pops the tag's operands, writes them
// and the tag into the module's exception state, sets the pending flag, and
// branches to wherever an exception raised here goes — an enclosing try's
// dispatch, or a return that leaves the flag set for the caller.
func (ls *lowerState) handleThrow(r *wasm.InstrReader) error {
tagIdx, err := r.ReadU32()
if err != nil {
return err
}
n, err := ls.tagOperandCount(tagIdx)
if err != nil {
return fmt.Errorf("%w: %w", ErrSSAUnsupported, err)
}
if len(ls.stack) < n {
return fmt.Errorf("%w: throw tag %d needs %d operands, stack has %d", ErrSSAUnsupported, tagIdx, n, len(ls.stack))
}
operands := make([]*ssa.Value, n)
copy(operands, ls.stack[len(ls.stack)-n:])
ls.stack = ls.stack[:len(ls.stack)-n]
ls.raiseAndGo(ls.b.Const32(int32(tagIdx)), operands...)
return nil
}
// tagOperandCount returns how many operands the exception tag tagIdx carries.
func (ls *lowerState) tagOperandCount(tagIdx uint32) (int, error) {
params, err := ls.tagOperandParams(tagIdx)
if err != nil {
return 0, err
}
return len(params), nil
}
// tagOperandParams returns the operand value types of exception tag tagIdx (the
// params of its type). The tag index space is imported tags first, then the
// module's own tag section.
func (ls *lowerState) tagOperandParams(tagIdx uint32) ([]wasm.ValType, error) {
var typeIdx uint32
if tagIdx < ls.mod.NumImportedTags {
k := uint32(0)
found := false
for _, imp := range ls.mod.Imports {
if imp.Kind != wasm.ImportTag {
continue
}
if k == tagIdx {
typeIdx = imp.Tag.TypeIdx
found = true
break
}
k++
}
if !found {
return nil, fmt.Errorf("tag: imported tag %d not found", tagIdx)
}
} else {
di := tagIdx - ls.mod.NumImportedTags
if int(di) >= len(ls.mod.Tags) {
return nil, fmt.Errorf("tag index %d out of range (%d defined tags)", tagIdx, len(ls.mod.Tags))
}
typeIdx = ls.mod.Tags[di].TypeIdx
}
if int(typeIdx) >= len(ls.mod.Types) {
return nil, fmt.Errorf("tag type index %d out of range", typeIdx)
}
return ls.mod.Types[typeIdx].Params, nil
}
func (ls *lowerState) handleReturn() error {
// Inside an inlined callee body, `return` is a branch to the inline
// frame's join block, not a caller return.
if len(ls.inlineFrames) > 0 {
return ls.returnAsInlineBr()
}
nRes := len(ls.ft.Results)
if len(ls.stack) < nRes {
return fmt.Errorf("return with %d stack values, expected %d", len(ls.stack), nRes)
}
rets := make([]*ssa.Value, nRes)
for i := 0; i < nRes; i++ {
rets[i] = ls.stack[len(ls.stack)-nRes+i]
}
cur := ls.b.Current()
cur.Kind = ssa.BlockRet
ls.b.FinishRet(rets...)
ls.unreachable = true
return nil
}
// isMemoryOp reports whether op is a wasm load/store the current
// lowering handles. SIMD (v128.load*) and atomic loads/stores are not
// in scope.
func isMemoryOp(op byte) bool {
switch op {
case wasm.OpI32Load, wasm.OpI64Load, wasm.OpF32Load, wasm.OpF64Load,
wasm.OpI32Load8S, wasm.OpI32Load8U, wasm.OpI32Load16S, wasm.OpI32Load16U,
wasm.OpI64Load8S, wasm.OpI64Load8U, wasm.OpI64Load16S, wasm.OpI64Load16U, wasm.OpI64Load32S, wasm.OpI64Load32U,
wasm.OpI32Store, wasm.OpI64Store, wasm.OpF32Store, wasm.OpF64Store,
wasm.OpI32Store8, wasm.OpI32Store16,
wasm.OpI64Store8, wasm.OpI64Store16, wasm.OpI64Store32:
return true
}
return false
}
// memoryOpSpec describes a wasm memory op's SSA mapping.
type memoryOpSpec struct {
ssaOp ssa.Op
typ ssa.Type // result type for loads, value type for stores
store bool
}
func memoryOpKind(op byte) memoryOpSpec {
switch op {
case wasm.OpI32Load:
return memoryOpSpec{ssa.OpLoad32, ssa.TypeI32, false}
case wasm.OpI32Load8S:
return memoryOpSpec{ssa.OpLoad8S, ssa.TypeI32, false}
case wasm.OpI32Load8U:
return memoryOpSpec{ssa.OpLoad8U, ssa.TypeI32, false}
case wasm.OpI32Load16S:
return memoryOpSpec{ssa.OpLoad16S, ssa.TypeI32, false}
case wasm.OpI32Load16U:
return memoryOpSpec{ssa.OpLoad16U, ssa.TypeI32, false}
case wasm.OpI64Load:
return memoryOpSpec{ssa.OpLoad64, ssa.TypeI64, false}
case wasm.OpI64Load8S:
return memoryOpSpec{ssa.OpLoad8S, ssa.TypeI64, false}
case wasm.OpI64Load8U:
return memoryOpSpec{ssa.OpLoad8U, ssa.TypeI64, false}
case wasm.OpI64Load16S:
return memoryOpSpec{ssa.OpLoad16S, ssa.TypeI64, false}
case wasm.OpI64Load16U:
return memoryOpSpec{ssa.OpLoad16U, ssa.TypeI64, false}
case wasm.OpI64Load32S:
return memoryOpSpec{ssa.OpLoad32S, ssa.TypeI64, false}
case wasm.OpI64Load32U:
return memoryOpSpec{ssa.OpLoad32U, ssa.TypeI64, false}
case wasm.OpF32Load:
return memoryOpSpec{ssa.OpLoadF32, ssa.TypeF32, false}
case wasm.OpF64Load:
return memoryOpSpec{ssa.OpLoadF64, ssa.TypeF64, false}
case wasm.OpI32Store:
return memoryOpSpec{ssa.OpStore32, ssa.TypeI32, true}
case wasm.OpI32Store8:
return memoryOpSpec{ssa.OpStore8, ssa.TypeI32, true}
case wasm.OpI32Store16:
return memoryOpSpec{ssa.OpStore16, ssa.TypeI32, true}
case wasm.OpI64Store:
return memoryOpSpec{ssa.OpStore64, ssa.TypeI64, true}
case wasm.OpI64Store8:
return memoryOpSpec{ssa.OpStore8, ssa.TypeI64, true}
case wasm.OpI64Store16:
return memoryOpSpec{ssa.OpStore16, ssa.TypeI64, true}
case wasm.OpI64Store32:
return memoryOpSpec{ssa.OpStore32, ssa.TypeI64, true}
case wasm.OpF32Store:
return memoryOpSpec{ssa.OpStoreF32, ssa.TypeF32, true}
case wasm.OpF64Store:
return memoryOpSpec{ssa.OpStoreF64, ssa.TypeF64, true}
}
return memoryOpSpec{ssa.OpInvalid, ssa.TypeInvalid, false}
}
// handleMemoryOp decodes a wasm load/store and emits the corresponding
// SSA op. The memarg align is ignored (Go's helpers don't honor it),
// the offset is recorded as AuxInt.
func (ls *lowerState) handleMemoryOp(op byte, r *wasm.InstrReader) error {
_, offset, err := r.ReadMemArg()
if err != nil {
return err
}
spec := memoryOpKind(op)
if spec.ssaOp == ssa.OpInvalid {
return fmt.Errorf("%w: memoryOpKind for 0x%02x", ErrSSAUnsupported, op)
}
if spec.store {
value, err := ls.pop()
if err != nil {
return err
}
base, err := ls.pop()
if err != nil {
return err
}
v := ls.b.NewValueAuxInt(spec.ssaOp, ssa.TypeMem, int64(offset), base, value)
_ = v // stored as a side-effecting value; emit handles as statement
return nil
}
base, err := ls.pop()
if err != nil {
return err
}
ls.push(ls.b.NewValueAuxInt(spec.ssaOp, spec.typ, int64(offset), base))
return nil
}
// handleCall lowers `call <funcidx>`. The callee may be an imported
// function or a defined one; both lower to OpCallImport / OpCallDirect
// respectively, with the function index as AuxInt.
//
// Multi-result returns (≥2) are not supported yet; OpSelect projection
// from a tuple value would be the way to handle them.
func (ls *lowerState) handleCall(r *wasm.InstrReader) error {
funcIdx, err := r.ReadU32()
if err != nil {
return err
}
ft := ls.mod.FuncTypeOf(funcIdx)
if len(ft.Results) > 1 {
return fmt.Errorf("%w: multi-result call (%d results)", ErrSSAUnsupported, len(ft.Results))
}
// Small leaf callees are spliced in instead of called (see
// inline.go): the call — and on gcasm its ABI0 marshalling and any
// cross-chunk forwarder — disappears entirely.
if ls.shouldInline(funcIdx, ft) {
return ls.lowerInlineCall(funcIdx, ft)
}
// Pop args from stack in reverse order.
if len(ls.stack) < len(ft.Params) {
return fmt.Errorf("call fn%d: stack underflow (need %d, have %d)", funcIdx, len(ft.Params), len(ls.stack))
}
argStart := len(ls.stack) - len(ft.Params)
args := append([]*ssa.Value(nil), ls.stack[argStart:]...)
ls.stack = ls.stack[:argStart]
isImport := funcIdx < ls.mod.NumImportedFuncs
var op ssa.Op
var resType ssa.Type
if len(ft.Results) == 1 {
resType = toSSAType(ft.Results[0])
if resType == ssa.TypeInvalid {
return fmt.Errorf("%w: call result type %v unsupported", ErrSSAUnsupported, ft.Results[0])
}
} else {
resType = ssa.TypeMem // sentinel for void-returning call's value placeholder
}
if isImport {
op = ssa.OpCallImport
} else {
op = ssa.OpCallDirect
}
v := ls.b.NewValueAuxInt(op, resType, int64(funcIdx), args...)
if len(ft.Results) == 1 {
ls.push(v)
}
// A callee that can leave an exception pending needs the check-and-
// branch right here, before the result is consumed: on the exception
// path the result is undefined. Host imports never raise.
if !isImport && ls.mayThrowCall(funcIdx) {
ls.emitExcCheck()
}
return nil
}
// maxTagArity mirrors the codegen-side computation: the module's widest
// exception-tag arity, i.e. how many operand slots the exception state has.
func maxTagArity(m *wasm.Module) int {
n := 0
consider := func(typeIdx uint32) {
if int(typeIdx) >= len(m.Types) {
return
}
if c := len(m.Types[typeIdx].Params); c > n {
n = c
}
}
for _, imp := range m.Imports {
if imp.Kind == wasm.ImportTag {
consider(imp.Tag.TypeIdx)
}
}
for _, tg := range m.Tags {
consider(tg.TypeIdx)
}
if n == 0 && (m.NumImportedTags > 0 || len(m.Tags) > 0) {
n = 1
}
return n
}
// handleCallIndirect lowers `call_indirect <typeidx> <tableidx>`. The
// last stack value is the table index; the values below it are the
// call's parameters.
func (ls *lowerState) handleCallIndirect(r *wasm.InstrReader) error {
typeIdx, err := r.ReadU32()
if err != nil {
return err
}
tableIdx, err := r.ReadU32()
if err != nil {
return err
}
_ = tableIdx
ft := ls.mod.Types[typeIdx]
if len(ft.Results) > 1 {
return fmt.Errorf("%w: multi-result call_indirect", ErrSSAUnsupported)
}
if len(ls.stack) < 1+len(ft.Params) {
return fmt.Errorf("call_indirect: stack underflow")
}
tableArg, err := ls.pop()
if err != nil {
return err
}
argStart := len(ls.stack) - len(ft.Params)
args := []*ssa.Value{tableArg}
args = append(args, ls.stack[argStart:]...)
ls.stack = ls.stack[:argStart]
var resType ssa.Type
if len(ft.Results) == 1 {
resType = toSSAType(ft.Results[0])
} else {
resType = ssa.TypeMem
}
v := ls.b.NewValueAuxInt(ssa.OpCallIndirect, resType, int64(typeIdx), args...)
if len(ft.Results) == 1 {
ls.push(v)
}
if ls.mayThrowIndirect(typeIdx) {
ls.emitExcCheck()
}
return nil
}
// globalType returns the SSA type of global index idx, or TypeInvalid
// if the index is out of range or the type isn't lowerable.
func (ls *lowerState) globalType(idx uint32) ssa.Type {
if int(idx) < int(ls.mod.NumImportedGlobals) {
// Imported globals — type lookup via mod.Imports.
var seen uint32
for _, imp := range ls.mod.Imports {
if imp.Kind != wasm.ImportGlobal {
continue
}
if seen == idx {
return toSSAType(imp.Global.Type)
}
seen++
}
return ssa.TypeInvalid
}
localIdx := idx - ls.mod.NumImportedGlobals
if int(localIdx) >= len(ls.mod.Globals) {
return ssa.TypeInvalid
}
return toSSAType(ls.mod.Globals[localIdx].Type.Type)
}
// appendLoopBackArgs extends each phi at the loop header with the
// current per-local value, recording the back-edge contribution.
func (ls *lowerState) appendLoopBackArgs(header *ssa.Block) {
if ls.mutableLocals {
return // no local phis in mutable-locals mode
}
for i, v := range ls.locals {
// Find the phi for local i at the header. We installed them
// at handleLoop in the same order, so it's the i'th OpPhi
// in header.Values.
nFound := 0
for _, candidate := range header.Values {
if candidate.Op != ssa.OpPhi {
continue
}
if nFound == i {
candidate.Args = append(candidate.Args, v)
break
}
nFound++
}
}
}
// handleIf implements `if blocktype ... [else ...] end`. Both branches
// inherit the locals + stack-below-cond snapshot at entry; the join at
// `end` reconciles via phi when a local or stack-result diverges.
func (ls *lowerState) handleIf(r *wasm.InstrReader) error {
// blocktype: s33; 0x40 (empty) or a value type encoded as a negative.
bt, err := r.ReadS33()
if err != nil {
return err
}
results, err := decodeBlockResults(bt)
if err != nil {
return fmt.Errorf("%w: if blocktype %v: %w", ErrSSAUnsupported, bt, err)
}
cond, err := ls.pop()
if err != nil {
return err
}
thenBlk := ls.b.NewBlock(ssa.BlockPlain)
elseBlk := ls.b.NewBlock(ssa.BlockPlain)
postBlk := ls.b.NewBlock(ssa.BlockPlain)
// Make the current block into an If node with two successors.
cur := ls.b.Current()
cur.Kind = ssa.BlockIf
cur.Control = cond
ssa.AddEdge(cur, thenBlk)
ssa.AddEdge(cur, elseBlk)
// Push frame. target == postBlk for br purposes. Snapshot the
// locals so the else-branch (and implicit no-else fall-through)
// starts from the if-entry state, not the then-branch's mutations.
ls.ctrl = append(ls.ctrl, &ctrlFrame{
kind: ctrlIf,
target: postBlk,
fallthrough_: postBlk,
elseBlk: elseBlk,
resultCount: len(results),
stackHeightAtEntry: len(ls.stack),
entryLocals: append([]*ssa.Value(nil), ls.locals...),
})
// Enter the then-block. Locals + stack stay the same.
ls.b.SetCurrent(thenBlk)
return nil
}
// handleElse closes the then-branch of the innermost if frame and
// switches the cursor to the else-branch entry block. Snapshots the
// current locals + (last resultCount) stack values for the post-block
// merge.
func (ls *lowerState) handleElse() error {
if len(ls.ctrl) == 0 || ls.ctrl[len(ls.ctrl)-1].kind != ctrlIf {
return fmt.Errorf("%w: else without matching if", ErrSSAUnsupported)
}
f := ls.ctrl[len(ls.ctrl)-1]
if !ls.unreachable {
ls.recordIncoming(f.target, f.resultCount)
// Wire the then-side block as Plain → post so the CFG is
// well-formed (recordIncoming only captures the data; the
// edge belongs in the block graph).
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.target)
}
ls.stack = ls.stack[:f.stackHeightAtEntry]
// The else-branch starts from the if-entry locals, NOT the
// then-branch's mutated state.
ls.locals = append([]*ssa.Value(nil), f.entryLocals...)
f.haveElse = true
ls.unreachable = false
ls.b.SetCurrent(f.elseBlk)
return nil
}
// handleEnd closes the innermost frame. When that frame is the function-
// level marker (ctrl empty), it finalises the function: collects the
// trailing operand stack as return values and marks the entry/last
// block as Ret. Returns done=true in that case.
func (ls *lowerState) handleEnd() (bool, error) {
if len(ls.ctrl) == 0 {
// Function-level end. Snapshot current results.
nRes := len(ls.ft.Results)
// If the cursor is already unreachable, the current block was
// terminated by the op that set unreachable — an explicit
// `return`, a `br`, or `unreachable`. That terminator already
// finished the block (BlockRet / BlockUnreachable) with the
// correct return values; re-finishing it here would append a
// second set of return markers and clobber the real ones (the
// emit pass reads the LAST nRes markers). So only finish the
// implicit fall-through end.
cur := ls.b.Current()
if ls.unreachable {
if cur.Kind == ssa.BlockInvalid {
// Defensive: a dead trailing block that was never
// terminated — make it well-formed with zero returns.
rets := make([]*ssa.Value, nRes)
for i, t := range ls.ft.Results {
rets[i] = zeroValueOf(ls.b, toSSAType(t))
}
ls.b.FinishRet(rets...)
}
return true, nil
}
if len(ls.stack) < nRes {
return false, fmt.Errorf("%w: function end with %d stack values, expected %d", ErrSSAUnsupported, len(ls.stack), nRes)
}
rets := make([]*ssa.Value, nRes)
copy(rets, ls.stack[len(ls.stack)-nRes:])
cur.Kind = ssa.BlockRet
// FinishRet adds OpCopy markers for the emit pass to find.
ls.b.FinishRet(rets...)
return true, nil
}
// Pop frame.
f := ls.ctrl[len(ls.ctrl)-1]
ls.ctrl = ls.ctrl[:len(ls.ctrl)-1]
switch f.kind {
case ctrlIf:
// Add the current-block snapshot as one more incoming edge to
// post (this is the then-side end if no else, or the else-
// side end if `else` was seen).
if !ls.unreachable {
ls.recordIncoming(f.target, f.resultCount)
}
// If no else was seen, the else-block is an immediate
// fall-through with no extra values: it inherits the entry
// snapshot and just bridges to post. Add an edge from elseBlk
// to post mirroring the original locals + (whatever results
// the fall-through carries from the entry stack).
if !f.haveElse {
// Build a "synthetic" entry-state edge: the else block
// is empty and falls through to post. The locals & stack
// at entry to the else are the same as at entry to the
// if (which were snapshotted on the then-edge as
// f.stackHeightAtEntry).
//
// To synthesise this, we momentarily switch current to
// elseBlk and snapshot. The stack at this point has
// resultCount values only if the if produces results
// without an else, which is illegal per wasm spec (if
// produces results, an else is required). So
// resultCount must be 0 here.
if f.resultCount != 0 {
return false, fmt.Errorf("%w: if without else cannot have block results", ErrSSAUnsupported)
}
// The implicit else edge carries the if-ENTRY locals
// (the if-cond was false, so nothing in the then-branch
// ran). Snapshot from f.entryLocals, not ls.locals which
// still holds the then-branch's mutations.
savedLocals := ls.locals
prev := ls.b.Current()
ls.locals = append([]*ssa.Value(nil), f.entryLocals...)
ls.b.SetCurrent(f.elseBlk)
ls.recordIncoming(f.target, 0)
ls.b.SetCurrent(prev)
ls.locals = savedLocals
}
// Wire elseBlk → postBlk for the no-else case AND elseBlk's
// natural fall-through to post when else IS present.
if !f.haveElse {
ssa.AddEdge(f.elseBlk, f.target)
}
// Make the previous current block (then-side end OR else-
// side end) a Plain edge to post if reachable. (The incoming-
// edge snapshot was already taken above, before the
// no-else else-bridge synthesis.)
if !ls.unreachable {
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.target)
}
// Switch to post only if anything actually flows in. If both
// then- and else-side trapped or branched away (the post block
// is unreachable), keep the cursor where it is and stay in the
// unreachable region so we don't construct an orphan post that
// emits dead code.
if _, ok := ls.incoming[f.target]; ok {
ls.b.SetCurrent(f.target)
ls.unreachable = false
if err := ls.resolveIncoming(f.target, f.resultCount, f.stackHeightAtEntry); err != nil {
return false, err
}
}
case ctrlBlock:
// Fall-through edge from current block into the post block,
// if reachable. The post block becomes the new current; if
// it has no incoming edges (nothing ever branched here and
// the body trapped), the function is just done with the
// block scope; skip the transition.
if !ls.unreachable {
ls.recordIncoming(f.target, f.resultCount)
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.target)
}
if _, ok := ls.incoming[f.target]; ok {
ls.b.SetCurrent(f.target)
ls.unreachable = false
if err := ls.resolveIncoming(f.target, f.resultCount, f.stackHeightAtEntry); err != nil {
return false, err
}
}
case ctrlLoop:
// Fall-through past the loop goes to the post-loop block.
// Loop header phis have been accumulating back-edge args
// throughout the body; nothing more to do here. Seal them
// implicitly: if a phi's args are all identical, we can
// optimise by replacing with the value — but we leave that
// for ConstProp / phielim.
if !ls.unreachable {
ls.recordIncoming(f.fallthrough_, f.resultCount)
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.fallthrough_)
}
if _, ok := ls.incoming[f.fallthrough_]; ok {
ls.b.SetCurrent(f.fallthrough_)
ls.unreachable = false
if err := ls.resolveIncoming(f.fallthrough_, f.resultCount, f.stackHeightAtEntry); err != nil {
return false, err
}
}
case ctrlTry:
// The `end` closes the LAST clause into post (the body and earlier
// clauses were closed at each catch).
ls.captureTryBody(f) // no-op unless this is a handler-less try..end
// No clause matched ⇒ keep propagating. The frame is already popped,
// so this resolves to the ENCLOSING context.
ls.closeChain(f)
sealDeadExcBlocks(f.tryRegion, f.excChain)
if !ls.unreachable {
ls.recordIncoming(f.target, f.resultCount)
cur := ls.b.Current()
cur.Kind = ssa.BlockPlain
ssa.AddEdge(cur, f.target)
}
if _, ok := ls.incoming[f.target]; ok {
ls.b.SetCurrent(f.target)
ls.unreachable = false
if err := ls.resolveIncoming(f.target, f.resultCount, f.stackHeightAtEntry); err != nil {
return false, err
}
}
}
return false, nil
}
// recordIncoming snapshots the current locals + last `resultCount` stack
// values as an edge flowing into target.
func (ls *lowerState) recordIncoming(target *ssa.Block, resultCount int) {
from := ls.b.Current()
// In mutable-locals mode, locals live in mutable Go vars that persist
// across blocks, so they are not threaded through phis — snapshot none.
var locSnap []*ssa.Value
if !ls.mutableLocals {
locSnap = append([]*ssa.Value(nil), ls.locals...)
}
var stackSnap []*ssa.Value
if resultCount > 0 {
base := len(ls.stack) - resultCount
if base < 0 {
base = 0
}
stackSnap = append([]*ssa.Value(nil), ls.stack[base:]...)
}
ls.incoming[target] = append(ls.incoming[target], incomingEdge{
from: from,
locals: locSnap,
stack: stackSnap,
})
}
// resolveIncoming materialises phis at target's entry for each local
// (and stack-result slot) whose values differ across predecessors, then
// updates the active locals/stack to point at those phis. Single-pred
// blocks simply adopt the predecessor's state without phis.
//
// baseHeight is the operand-stack height of the structured-control
// region's ENTRY — the number of values that sit below the region's
// results. The post-region stack is reset to exactly
// baseHeight + resultCount values: baseHeight original base values plus
// the merged results. This explicit reset is essential because the
// current ls.stack may be a *stale* unreachable-branch stack (e.g. the
// then-branch of an if ended in `br`, leaving its own pushed values on
// ls.stack); trimming `len(ls.stack)-resultCount` would then preserve
// those leftovers and corrupt the base stack.
func (ls *lowerState) resolveIncoming(target *ssa.Block, resultCount, baseHeight int) error {
edges := ls.incoming[target]
delete(ls.incoming, target)
if len(edges) == 0 {
// Block is unreachable. Leave as-is; caller may set it to
// Ret synthetically.
return nil
}
if baseHeight < 0 || baseHeight > len(ls.stack) {
return fmt.Errorf("resolveIncoming: baseHeight %d out of range (stack %d)", baseHeight, len(ls.stack))
}
if len(edges) == 1 {
ls.locals = append([]*ssa.Value(nil), edges[0].locals...)
// Reset to the region-entry base, then append the single
// edge's result snapshot.
ls.stack = append(ls.stack[:baseHeight:baseHeight], edges[0].stack...)
return nil
}
// Multi-pred: emit phis. The current block must be the target.
if ls.b.Current() != target {
return fmt.Errorf("resolveIncoming: current block %d != target %d", ls.b.Current().ID, target.ID)
}
// CRITICAL: a phi's Args must be ordered to match target.Preds —
// phi.Args[i] is the value flowing in along target.Preds[i]. The
// incoming-edge list was built in branch-processing order, which
// is NOT the same as the pred-edge insertion order. Re-order the
// edges to follow target.Preds, matching by predecessor block.
ordered := make([]incomingEdge, len(target.Preds))
for i, pe := range target.Preds {
found := false
for _, e := range edges {
if e.from == pe.Block {
ordered[i] = e
found = true
break
}
}
if !found {
return fmt.Errorf("resolveIncoming: no incoming edge recorded for pred b%d of b%d", pe.Block.ID, target.ID)
}
}
edges = ordered
// Locals.
nLocals := len(edges[0].locals)
mergedLocals := make([]*ssa.Value, nLocals)
for i := 0; i < nLocals; i++ {
args := make([]*ssa.Value, len(edges))
allSame := true
for j, e := range edges {
args[j] = e.locals[i]
if args[j] != args[0] {
allSame = false
}
}
if allSame {
mergedLocals[i] = args[0]
continue
}
phi := ls.b.NewValue(ssa.OpPhi, ls.localTypes[i], args...)
mergedLocals[i] = phi
}
ls.locals = mergedLocals
// Stack results.
mergedStack := make([]*ssa.Value, resultCount)
for i := 0; i < resultCount; i++ {
args := make([]*ssa.Value, len(edges))
allSame := true
var t ssa.Type
for j, e := range edges {
args[j] = e.stack[i]
t = e.stack[i].Type
if args[j] != args[0] {
allSame = false
}
}
if allSame {
mergedStack[i] = args[0]
continue
}
phi := ls.b.NewValue(ssa.OpPhi, t, args...)
mergedStack[i] = phi
}
// Reset to the region-entry base height (discarding any stale
// unreachable-branch leftovers), then push the merged results.
ls.stack = append(ls.stack[:baseHeight:baseHeight], mergedStack...)
return nil
}
// decodeBlockResults parses a wasm block-type s33 byte. Returns the
// list of result types. Empty block (0x40) → no results. Single
// inline value type → one-element slice. Function-type-index encoding
// (positive s33) is rejected here; that's a future feature.
func decodeBlockResults(bt int64) ([]wasm.ValType, error) {
if bt == -0x40 || bt == 0x40 {
return nil, nil
}
switch bt {
case -0x01: // i32 (encoded as 0x7f → s33 -1)
return []wasm.ValType{wasm.ValI32}, nil
case -0x02:
return []wasm.ValType{wasm.ValI64}, nil
case -0x03:
return []wasm.ValType{wasm.ValF32}, nil
case -0x04:
return []wasm.ValType{wasm.ValF64}, nil
case -0x05: // v128 (encoded as 0x7b → s33 -5)
return []wasm.ValType{wasm.ValV128}, nil
}
return nil, fmt.Errorf("blocktype %v not supported (function-type-index encoding)", bt)
}
// push / pop wrap the operand stack with error reporting.
func (ls *lowerState) push(v *ssa.Value) {
ls.stack = append(ls.stack, v)
}
func (ls *lowerState) pop() (*ssa.Value, error) {
if len(ls.stack) == 0 {
return nil, fmt.Errorf("ssa lowering: pop on empty stack")
}
v := ls.stack[len(ls.stack)-1]
ls.stack = ls.stack[:len(ls.stack)-1]
return v, nil
}
// zeroValueOf emits a zero constant of the given type. Used when a
// path falls off the end unreachable but the wasm signature demands
// results.
func zeroValueOf(b *ssa.FuncBuilder, t ssa.Type) *ssa.Value {
switch t {
case ssa.TypeI32:
return b.Const32(0)
case ssa.TypeI64:
return b.Const64(0)
case ssa.TypeF32:
return b.NewValueAuxInt(ssa.OpConstF32, ssa.TypeF32, 0)
case ssa.TypeF64:
return b.NewValueAuxInt(ssa.OpConstF64, ssa.TypeF64, 0)
case ssa.TypeV128:
return b.NewValueAux(ssa.OpSimdConst, ssa.TypeV128, [2]uint64{0, 0})
}
return b.Const32(0)
}
// toSSATypes maps wasm value types to ssa.Type.
func toSSATypes(in []wasm.ValType) []ssa.Type {
out := make([]ssa.Type, len(in))
for i, t := range in {
out[i] = toSSAType(t)
}
return out
}
func toSSAType(t wasm.ValType) ssa.Type {
switch t {
case wasm.ValI32:
return ssa.TypeI32
case wasm.ValI64:
return ssa.TypeI64
case wasm.ValF32:
return ssa.TypeF32
case wasm.ValF64:
return ssa.TypeF64
case wasm.ValV128:
return ssa.TypeV128
}
return ssa.TypeInvalid
}
// setupLocals emits OpParam values for parameters and OpConst zeros for
// the function's declared locals. Returns the per-local current-value
// slice + per-local types (for phi typing later).
func setupLocals(b *ssa.FuncBuilder, fn wasm.Function, ft wasm.FuncType) ([]*ssa.Value, []ssa.Type, error) {
var locals []*ssa.Value
var types []ssa.Type
for i, p := range ft.Params {
t := toSSAType(p)
if t == ssa.TypeInvalid {
return nil, nil, fmt.Errorf("%w: parameter %d has unsupported type %v", ErrSSAUnsupported, i, p)
}
locals = append(locals, b.Param(i, t))
types = append(types, t)
}
r := wasm.NewInstrReader(fn.Body)
nDecls, err := r.ReadU32()
if err != nil {
return nil, nil, err
}
for i := uint32(0); i < nDecls; i++ {
count, err := r.ReadU32()
if err != nil {
return nil, nil, err
}
t, err := r.ReadByte()
if err != nil {
return nil, nil, err
}
st := toSSAType(wasm.ValType(t))
if st == ssa.TypeInvalid {
return nil, nil, fmt.Errorf("%w: local block %d has unsupported type 0x%02x", ErrSSAUnsupported, i, t)
}
for j := uint32(0); j < count; j++ {
var zero *ssa.Value
switch st {
case ssa.TypeI32:
zero = b.Const32(0)
case ssa.TypeI64:
zero = b.Const64(0)
case ssa.TypeF32:
zero = b.NewValueAuxInt(ssa.OpConstF32, ssa.TypeF32, 0)
case ssa.TypeF64:
zero = b.NewValueAuxInt(ssa.OpConstF64, ssa.TypeF64, 0)
case ssa.TypeV128:
zero = b.NewValueAux(ssa.OpSimdConst, ssa.TypeV128, [2]uint64{0, 0})
default:
return nil, nil, fmt.Errorf("%w: zero-init local of type %v", ErrSSAUnsupported, st)
}
locals = append(locals, zero)
types = append(types, st)
}
}
return locals, types, nil
}
func skipLocalDecls(r *wasm.InstrReader) error {
nDecls, err := r.ReadU32()
if err != nil {
return err
}
for i := uint32(0); i < nDecls; i++ {
if _, err := r.ReadU32(); err != nil {
return err
}
if _, err := r.ReadByte(); err != nil {
return err
}
}
return nil
}
// isGtGe reports whether the wasm op is a greater-than or
// greater-or-equal comparison (which lower to the corresponding lt/le
// SSA op with swapped arguments).
func isGtGe(op byte) bool {
switch op {
case wasm.OpI32GtS, wasm.OpI32GtU, wasm.OpI32GeS, wasm.OpI32GeU,
wasm.OpI64GtS, wasm.OpI64GtU, wasm.OpI64GeS, wasm.OpI64GeU:
return true
}
return false
}
// isSimpleBinary reports whether op is a stack-consuming binary op
// supported by the current lowering directly via simpleBinaryOp (no
// helper-call indirection).
func isSimpleBinary(op byte) bool {
switch op {
case wasm.OpI32Add, wasm.OpI32Sub, wasm.OpI32Mul, wasm.OpI32And, wasm.OpI32Or, wasm.OpI32Xor,
wasm.OpI32Shl, wasm.OpI32ShrS, wasm.OpI32ShrU,
wasm.OpI64Add, wasm.OpI64Sub, wasm.OpI64Mul, wasm.OpI64And, wasm.OpI64Or, wasm.OpI64Xor,
wasm.OpI64Shl, wasm.OpI64ShrS, wasm.OpI64ShrU,
wasm.OpI32Eq, wasm.OpI32Ne, wasm.OpI32LtS, wasm.OpI32LtU, wasm.OpI32LeS, wasm.OpI32LeU,
wasm.OpI32GtS, wasm.OpI32GtU, wasm.OpI32GeS, wasm.OpI32GeU,
wasm.OpI64Eq, wasm.OpI64Ne, wasm.OpI64LtS, wasm.OpI64LtU, wasm.OpI64LeS, wasm.OpI64LeU,
wasm.OpI64GtS, wasm.OpI64GtU, wasm.OpI64GeS, wasm.OpI64GeU:
return true
}
return false
}
// simpleBinaryOp maps a wasm opcode to (ssa.Op, result type).
func simpleBinaryOp(op byte) (ssa.Op, ssa.Type) {
switch op {
case wasm.OpI32Add:
return ssa.OpAdd32, ssa.TypeI32
case wasm.OpI32Sub:
return ssa.OpSub32, ssa.TypeI32
case wasm.OpI32Mul:
return ssa.OpMul32, ssa.TypeI32
case wasm.OpI32And:
return ssa.OpAnd32, ssa.TypeI32
case wasm.OpI32Or:
return ssa.OpOr32, ssa.TypeI32
case wasm.OpI32Xor:
return ssa.OpXor32, ssa.TypeI32
case wasm.OpI32Shl:
return ssa.OpShl32, ssa.TypeI32
case wasm.OpI32ShrS:
return ssa.OpShrS32, ssa.TypeI32
case wasm.OpI32ShrU:
return ssa.OpShrU32, ssa.TypeI32
case wasm.OpI64Add:
return ssa.OpAdd64, ssa.TypeI64
case wasm.OpI64Sub:
return ssa.OpSub64, ssa.TypeI64
case wasm.OpI64Mul:
return ssa.OpMul64, ssa.TypeI64
case wasm.OpI64And:
return ssa.OpAnd64, ssa.TypeI64
case wasm.OpI64Or:
return ssa.OpOr64, ssa.TypeI64
case wasm.OpI64Xor:
return ssa.OpXor64, ssa.TypeI64
case wasm.OpI64Shl:
return ssa.OpShl64, ssa.TypeI64
case wasm.OpI64ShrS:
return ssa.OpShrS64, ssa.TypeI64
case wasm.OpI64ShrU:
return ssa.OpShrU64, ssa.TypeI64
case wasm.OpI32Eq:
return ssa.OpEq32, ssa.TypeBool
case wasm.OpI32Ne:
return ssa.OpNe32, ssa.TypeBool
case wasm.OpI32LtS:
return ssa.OpLtS32, ssa.TypeBool
case wasm.OpI32LtU:
return ssa.OpLtU32, ssa.TypeBool
case wasm.OpI32LeS:
return ssa.OpLeS32, ssa.TypeBool
case wasm.OpI32LeU:
return ssa.OpLeU32, ssa.TypeBool
case wasm.OpI32GtS:
// gt is lt with swapped args; we'll wrap at emit time.
return ssa.OpLtS32, ssa.TypeBool
case wasm.OpI32GtU:
return ssa.OpLtU32, ssa.TypeBool
case wasm.OpI32GeS:
return ssa.OpLeS32, ssa.TypeBool
case wasm.OpI32GeU:
return ssa.OpLeU32, ssa.TypeBool
case wasm.OpI64Eq:
return ssa.OpEq64, ssa.TypeBool
case wasm.OpI64Ne:
return ssa.OpNe64, ssa.TypeBool
case wasm.OpI64LtS:
return ssa.OpLtS64, ssa.TypeBool
case wasm.OpI64LtU:
return ssa.OpLtU64, ssa.TypeBool
case wasm.OpI64LeS:
return ssa.OpLeS64, ssa.TypeBool
case wasm.OpI64LeU:
return ssa.OpLeU64, ssa.TypeBool
case wasm.OpI64GtS:
return ssa.OpLtS64, ssa.TypeBool
case wasm.OpI64GtU:
return ssa.OpLtU64, ssa.TypeBool
case wasm.OpI64GeS:
return ssa.OpLeS64, ssa.TypeBool
case wasm.OpI64GeU:
return ssa.OpLeU64, ssa.TypeBool
}
panic("simpleBinaryOp: not a binary op")
}
// helperBinary describes a wasm binary op that lowers to a 2-arg call
// to a pre-existing pure helper (rotation, min/max, copysign, float
// compares).
type helperBinary struct {
helper string
resType ssa.Type
}
// helperUnary describes a wasm unary op that lowers to a 1-arg helper
// call (eqz, clz/ctz/popcnt, float unary, conversions, sign-extend).
type helperUnary struct {
helper string
resType ssa.Type
}
func isHelperBinary(op byte) bool {
switch op {
case wasm.OpI32Rotl, wasm.OpI32Rotr, wasm.OpI64Rotl, wasm.OpI64Rotr,
wasm.OpI32DivS, wasm.OpI32DivU, wasm.OpI32RemS, wasm.OpI32RemU,
wasm.OpI64DivS, wasm.OpI64DivU, wasm.OpI64RemS, wasm.OpI64RemU,
wasm.OpF32Add, wasm.OpF32Sub, wasm.OpF32Mul, wasm.OpF32Div,
wasm.OpF64Add, wasm.OpF64Sub, wasm.OpF64Mul, wasm.OpF64Div,
wasm.OpF32Min, wasm.OpF32Max, wasm.OpF32Copysign,
wasm.OpF64Min, wasm.OpF64Max, wasm.OpF64Copysign,
wasm.OpF32Eq, wasm.OpF32Ne, wasm.OpF32Lt, wasm.OpF32Gt, wasm.OpF32Le, wasm.OpF32Ge,
wasm.OpF64Eq, wasm.OpF64Ne, wasm.OpF64Lt, wasm.OpF64Gt, wasm.OpF64Le, wasm.OpF64Ge:
return true
}
return false
}
func helperBinarySpec(op byte) helperBinary {
switch op {
case wasm.OpI32Rotl:
return helperBinary{"i32_rotl", ssa.TypeI32}
case wasm.OpI32Rotr:
return helperBinary{"i32_rotr", ssa.TypeI32}
case wasm.OpI64Rotl:
return helperBinary{"i64_rotl", ssa.TypeI64}
case wasm.OpI64Rotr:
return helperBinary{"i64_rotr", ssa.TypeI64}
case wasm.OpI32DivS:
return helperBinary{"i32_div_s", ssa.TypeI32}
case wasm.OpI32DivU:
return helperBinary{"i32_div_u_s", ssa.TypeI32}
case wasm.OpI32RemS:
return helperBinary{"i32_rem_s", ssa.TypeI32}
case wasm.OpI32RemU:
return helperBinary{"i32_rem_u_s", ssa.TypeI32}
case wasm.OpI64DivS:
return helperBinary{"i64_div_s", ssa.TypeI64}
case wasm.OpI64DivU:
return helperBinary{"i64_div_u_s", ssa.TypeI64}
case wasm.OpI64RemS:
return helperBinary{"i64_rem_s", ssa.TypeI64}
case wasm.OpI64RemU:
return helperBinary{"i64_rem_u_s", ssa.TypeI64}
case wasm.OpF32Add:
return helperBinary{"f32_add", ssa.TypeF32}
case wasm.OpF32Sub:
return helperBinary{"f32_sub", ssa.TypeF32}
case wasm.OpF32Mul:
return helperBinary{"f32_mul", ssa.TypeF32}
case wasm.OpF32Div:
return helperBinary{"f32_div", ssa.TypeF32}
case wasm.OpF64Add:
return helperBinary{"f64_add", ssa.TypeF64}
case wasm.OpF64Sub:
return helperBinary{"f64_sub", ssa.TypeF64}
case wasm.OpF64Mul:
return helperBinary{"f64_mul", ssa.TypeF64}
case wasm.OpF64Div:
return helperBinary{"f64_div", ssa.TypeF64}
case wasm.OpF32Min:
return helperBinary{"f32_min", ssa.TypeF32}
case wasm.OpF32Max:
return helperBinary{"f32_max", ssa.TypeF32}
case wasm.OpF32Copysign:
return helperBinary{"f32_copysign", ssa.TypeF32}
case wasm.OpF64Min:
return helperBinary{"f64_min", ssa.TypeF64}
case wasm.OpF64Max:
return helperBinary{"f64_max", ssa.TypeF64}
case wasm.OpF64Copysign:
return helperBinary{"f64_copysign", ssa.TypeF64}
case wasm.OpF32Eq:
return helperBinary{"f32_eq", ssa.TypeI32}
case wasm.OpF32Ne:
return helperBinary{"f32_ne", ssa.TypeI32}
case wasm.OpF32Lt:
return helperBinary{"f32_lt", ssa.TypeI32}
case wasm.OpF32Gt:
return helperBinary{"f32_gt", ssa.TypeI32}
case wasm.OpF32Le:
return helperBinary{"f32_le", ssa.TypeI32}
case wasm.OpF32Ge:
return helperBinary{"f32_ge", ssa.TypeI32}
case wasm.OpF64Eq:
return helperBinary{"f64_eq", ssa.TypeI32}
case wasm.OpF64Ne:
return helperBinary{"f64_ne", ssa.TypeI32}
case wasm.OpF64Lt:
return helperBinary{"f64_lt", ssa.TypeI32}
case wasm.OpF64Gt:
return helperBinary{"f64_gt", ssa.TypeI32}
case wasm.OpF64Le:
return helperBinary{"f64_le", ssa.TypeI32}
case wasm.OpF64Ge:
return helperBinary{"f64_ge", ssa.TypeI32}
}
panic(fmt.Sprintf("helperBinarySpec: unknown op 0x%02x", op))
}
func isHelperUnary(op byte) bool {
switch op {
// i32.eqz / i64.eqz are NOT here: they lower to first-class
// OpEq{32,64}-against-zero comparisons (see the dedicated case in
// lowerOp) so branches on them fuse instead of calling a helper.
case wasm.OpI32Clz, wasm.OpI32Ctz, wasm.OpI32Popcnt,
wasm.OpI64Clz, wasm.OpI64Ctz, wasm.OpI64Popcnt,
wasm.OpF32Abs, wasm.OpF32Neg, wasm.OpF32Ceil, wasm.OpF32Floor, wasm.OpF32Trunc, wasm.OpF32Nearest, wasm.OpF32Sqrt,
wasm.OpF64Abs, wasm.OpF64Neg, wasm.OpF64Ceil, wasm.OpF64Floor, wasm.OpF64Trunc, wasm.OpF64Nearest, wasm.OpF64Sqrt,
wasm.OpI32WrapI64,
wasm.OpI32TruncF32S, wasm.OpI32TruncF32U, wasm.OpI32TruncF64S, wasm.OpI32TruncF64U,
wasm.OpI64ExtendI32S, wasm.OpI64ExtendI32U,
wasm.OpI64TruncF32S, wasm.OpI64TruncF32U, wasm.OpI64TruncF64S, wasm.OpI64TruncF64U,
wasm.OpF32ConvertI32S, wasm.OpF32ConvertI32U, wasm.OpF32ConvertI64S, wasm.OpF32ConvertI64U,
wasm.OpF32DemoteF64,
wasm.OpF64ConvertI32S, wasm.OpF64ConvertI32U, wasm.OpF64ConvertI64S, wasm.OpF64ConvertI64U,
wasm.OpF64PromoteF32,
wasm.OpI32ReinterpretF32, wasm.OpI64ReinterpretF64, wasm.OpF32ReinterpretI32, wasm.OpF64ReinterpretI64,
wasm.OpI32Extend8S, wasm.OpI32Extend16S,
wasm.OpI64Extend8S, wasm.OpI64Extend16S, wasm.OpI64Extend32S:
return true
}
return false
}
func helperUnarySpec(op byte) helperUnary {
switch op {
case wasm.OpI32Clz:
return helperUnary{"i32_clz", ssa.TypeI32}
case wasm.OpI32Ctz:
return helperUnary{"i32_ctz", ssa.TypeI32}
case wasm.OpI32Popcnt:
return helperUnary{"i32_popcnt", ssa.TypeI32}
case wasm.OpI64Clz:
return helperUnary{"i64_clz", ssa.TypeI64}
case wasm.OpI64Ctz:
return helperUnary{"i64_ctz", ssa.TypeI64}
case wasm.OpI64Popcnt:
return helperUnary{"i64_popcnt", ssa.TypeI64}
case wasm.OpF32Abs:
return helperUnary{"f32_abs", ssa.TypeF32}
case wasm.OpF32Neg:
return helperUnary{"f32_neg", ssa.TypeF32}
case wasm.OpF32Ceil:
return helperUnary{"f32_ceil", ssa.TypeF32}
case wasm.OpF32Floor:
return helperUnary{"f32_floor", ssa.TypeF32}
case wasm.OpF32Trunc:
return helperUnary{"f32_trunc", ssa.TypeF32}
case wasm.OpF32Nearest:
return helperUnary{"f32_nearest", ssa.TypeF32}
case wasm.OpF32Sqrt:
return helperUnary{"f32_sqrt", ssa.TypeF32}
case wasm.OpF64Abs:
return helperUnary{"f64_abs", ssa.TypeF64}
case wasm.OpF64Neg:
return helperUnary{"f64_neg", ssa.TypeF64}
case wasm.OpF64Ceil:
return helperUnary{"f64_ceil", ssa.TypeF64}
case wasm.OpF64Floor:
return helperUnary{"f64_floor", ssa.TypeF64}
case wasm.OpF64Trunc:
return helperUnary{"f64_trunc", ssa.TypeF64}
case wasm.OpF64Nearest:
return helperUnary{"f64_nearest", ssa.TypeF64}
case wasm.OpF64Sqrt:
return helperUnary{"f64_sqrt", ssa.TypeF64}
case wasm.OpI32WrapI64:
return helperUnary{"i32_wrap_i64", ssa.TypeI32}
case wasm.OpI32TruncF32S:
return helperUnary{"i32_trunc_f32_s", ssa.TypeI32}
case wasm.OpI32TruncF32U:
return helperUnary{"i32_trunc_f32_u", ssa.TypeI32}
case wasm.OpI32TruncF64S:
return helperUnary{"i32_trunc_f64_s", ssa.TypeI32}
case wasm.OpI32TruncF64U:
return helperUnary{"i32_trunc_f64_u", ssa.TypeI32}
case wasm.OpI64ExtendI32S:
return helperUnary{"i64_extend_i32_s", ssa.TypeI64}
case wasm.OpI64ExtendI32U:
return helperUnary{"i64_extend_i32_u", ssa.TypeI64}
case wasm.OpI64TruncF32S:
return helperUnary{"i64_trunc_f32_s", ssa.TypeI64}
case wasm.OpI64TruncF32U:
return helperUnary{"i64_trunc_f32_u", ssa.TypeI64}
case wasm.OpI64TruncF64S:
return helperUnary{"i64_trunc_f64_s", ssa.TypeI64}
case wasm.OpI64TruncF64U:
return helperUnary{"i64_trunc_f64_u", ssa.TypeI64}
case wasm.OpF32ConvertI32S:
return helperUnary{"f32_convert_i32_s", ssa.TypeF32}
case wasm.OpF32ConvertI32U:
return helperUnary{"f32_convert_i32_u", ssa.TypeF32}
case wasm.OpF32ConvertI64S:
return helperUnary{"f32_convert_i64_s", ssa.TypeF32}
case wasm.OpF32ConvertI64U:
return helperUnary{"f32_convert_i64_u", ssa.TypeF32}
case wasm.OpF32DemoteF64:
return helperUnary{"f32_demote_f64", ssa.TypeF32}
case wasm.OpF64ConvertI32S:
return helperUnary{"f64_convert_i32_s", ssa.TypeF64}
case wasm.OpF64ConvertI32U:
return helperUnary{"f64_convert_i32_u", ssa.TypeF64}
case wasm.OpF64ConvertI64S:
return helperUnary{"f64_convert_i64_s", ssa.TypeF64}
case wasm.OpF64ConvertI64U:
return helperUnary{"f64_convert_i64_u", ssa.TypeF64}
case wasm.OpF64PromoteF32:
return helperUnary{"f64_promote_f32", ssa.TypeF64}
case wasm.OpI32ReinterpretF32:
return helperUnary{"i32_reinterpret_f32", ssa.TypeI32}
case wasm.OpI64ReinterpretF64:
return helperUnary{"i64_reinterpret_f64", ssa.TypeI64}
case wasm.OpF32ReinterpretI32:
return helperUnary{"f32_reinterpret_i32", ssa.TypeF32}
case wasm.OpF64ReinterpretI64:
return helperUnary{"f64_reinterpret_i64", ssa.TypeF64}
case wasm.OpI32Extend8S:
return helperUnary{"i32_extend8_s", ssa.TypeI32}
case wasm.OpI32Extend16S:
return helperUnary{"i32_extend16_s", ssa.TypeI32}
case wasm.OpI64Extend8S:
return helperUnary{"i64_extend8_s", ssa.TypeI64}
case wasm.OpI64Extend16S:
return helperUnary{"i64_extend16_s", ssa.TypeI64}
case wasm.OpI64Extend32S:
return helperUnary{"i64_extend32_s", ssa.TypeI64}
}
panic(fmt.Sprintf("helperUnarySpec: unknown op 0x%02x", op))
}