mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
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]>
2727 lines
88 KiB
Go
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))
|
|
}
|