Files
f30ec292fd Support the wasm SIMD proposal with native asm lowering (arm64/amd64) (#40)
* Support the wasm SIMD proposal with native asm lowering (arm64/amd64)

One revision spanning the SIMD (0xfd v128) proposal end to end: SSA
representation, lowering, scalar reference helpers, and a gcasm asm
backend that turns the hot paths into native SIMD — measured on a
full llama.cpp build transpiled to pure Go, which this makes run at
V8-class speed (prompt eval beats V8 on arm64).

Frontend and SSA:
- v128 values are [2]uint64 pairs in little-endian lane order; pure
  ops lower to CSE-able helper calls, memory ops keep statement
  order, v128.const/shuffle ride as [2]uint64 aux. The full scalar
  helper set is the reference semantics (saturation, NaN
  propagation, round-to-even, swizzle OOB, shift masking).
- Exception handling lowers to plain branches instead of
  panic/recover — a check-and-branch after each may-raise call, a
  per-try dispatch block running the catch-clause chain — covering
  the C++-shaped patterns (multi-clause catch, delegate targets,
  rethrow depths) without runtime unwinding, so EH bodies stay
  eligible for the asm backend. This replaces the closure/recover
  lowering that #39 extended; its tests are retained unchanged.
- Giant functions outline into tractable units (whole loops
  included, packed u64 boundaries for v128 values), keeping gc
  compile times and register pressure sane on llama.cpp-sized
  modules.

gcasm backend:
- SIMD helper calls splice inline as native NEON/SSE instead of
  marshalled calls; v128 SSA values travel register-to-register in
  pair form. Call-tree windows fuse into single splices with region
  invariant hoisting, bounds-check coalescing, store sinks and
  scalar-chain internalization; whole countdown loops fuse with an
  in-splice unroll lane.
- Superinstruction selection inside fused regions: 8-bit dot
  patterns (SMULL+SADALP), SDOT for full 16-byte dots, FMUL by
  element, scale-lookup addressing, inlined integer division, the
  software fp32->fp16 idiom lowered to FCVT (both the table-gather
  and the packed store form), and ggml q8_0 row/column pairing via
  the traits nrows patch with an SMMLA 2x2 tile kernel.
- Runtime CPU-feature dispatch gates spliced bodies (dotprod on
  arm64, x86-64-v2 SSE on amd64); WASM2GO_FAST_MATH opts into
  raw-SDOT/FMLA/dual-accumulator forms that trade wasm bit-exactness
  for native-build-style rounding, validated by token-level
  equivalence against native llama.cpp.
- Guard-page linear memory eliminates per-access bounds checks;
  jump tables replace the compare tree for br_table.

Tooling and CI:
- WASM2GO_F16_TABLE asserts the address of ggml's runtime-built f16
  table; the transpiler warns when the asserted address matches no
  gather site (the address moves with every data-layout shift, and a
  stale assertion silently costs ~7% decode throughput).
- The coverage gate runs with -count=1: a cached package result
  merges incomplete coverage data into the -coverpkg profile,
  deflating the total by 10+ points depending on what the runner's
  restored cache contained — the gate must never consume cached
  results.
- Coverage threshold moves to 83% for the enlarged tree; transpile
  output is deterministic and outline rejections are explained in
  the build log.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Replace environment-variable switches with typed Options; drop debug code

Configuration now flows through one channel: transpile.Options gains
OutlineMinValues, SIMDUnroll, FuseLoops, FuseLoopUnroll, F16TableAddr
and FastMath (with matching CLI flags), the gcasm backend receives
them as a Config threaded through ModuleOffsets, and no code path
branches on an environment variable anymore. Integrator entry points
(the wasmify protoc plugin, the CLI) convert their external
configuration into these options exactly once.

Deleted outright, per the no-env rule and because they were
diagnostics rather than build modes:
- every WASM2GO_NO_* kill switch and bisection knob (SSA pass
  disabling, fallback ranges, fuse-window caps, outline limits,
  inliner windows) — git history keeps them for the next hunt;
- every env-gated debug printer (fuse/inline/outline/nrc2/f16/SSA
  dumps and the near-miss tracers) and the capture-tree keepers;
- the dormant multi-exit loop structuring (opt-in, measured slower)
  and the SSA f32->f16 rewrite entry superseded by the gcasm
  peephole (its shared idiom matchers stay for the f16 store pass);
- the runtime WASM2GO_CPU_PORTABLE override in the generated CPU
  feature probes.

The nil-offsets error paths exposed by the threading get nil-safe
Config accessors, and the outline share/v128-min/inliner caps become
plain constants at their measured production values.

Behavior under default options is unchanged: transpiled output for
the non-SIMD and EH fixtures is byte-identical to the previous
commit, and the only SIMD-bundle difference is the removed env check
in the emitted cpufeat runtime files.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 23:28:14 +09:00

217 lines
5.6 KiB
Go

package wasm
// WebAssembly MVP opcodes plus the 0xfc prefix.
// For the full list see https://webassembly.github.io/spec/core/binary/instructions.html
const (
OpUnreachable byte = 0x00
OpNop byte = 0x01
OpBlock byte = 0x02
OpLoop byte = 0x03
OpIf byte = 0x04
OpElse byte = 0x05
// Legacy exception-handling proposal (what clang/wasi-sdk emits for
// setjmp/longjmp under `-mllvm -wasm-enable-sjlj`). try opens a handler
// region with a blocktype; catch/catch_all begin handlers; throw raises a
// tag; rethrow/delegate re-raise. These carry the setjmp/longjmp lowering.
OpTry byte = 0x06
OpCatch byte = 0x07
OpThrow byte = 0x08
OpRethrow byte = 0x09
OpDelegate byte = 0x18
OpCatchAll byte = 0x19
OpEnd byte = 0x0b
OpBr byte = 0x0c
OpBrIf byte = 0x0d
OpBrTable byte = 0x0e
OpReturn byte = 0x0f
OpCall byte = 0x10
OpCallIndirect byte = 0x11
OpDrop byte = 0x1a
OpSelect byte = 0x1b
OpSelectT byte = 0x1c
OpLocalGet byte = 0x20
OpLocalSet byte = 0x21
OpLocalTee byte = 0x22
OpGlobalGet byte = 0x23
OpGlobalSet byte = 0x24
OpI32Load byte = 0x28
OpI64Load byte = 0x29
OpF32Load byte = 0x2a
OpF64Load byte = 0x2b
OpI32Load8S byte = 0x2c
OpI32Load8U byte = 0x2d
OpI32Load16S byte = 0x2e
OpI32Load16U byte = 0x2f
OpI64Load8S byte = 0x30
OpI64Load8U byte = 0x31
OpI64Load16S byte = 0x32
OpI64Load16U byte = 0x33
OpI64Load32S byte = 0x34
OpI64Load32U byte = 0x35
OpI32Store byte = 0x36
OpI64Store byte = 0x37
OpF32Store byte = 0x38
OpF64Store byte = 0x39
OpI32Store8 byte = 0x3a
OpI32Store16 byte = 0x3b
OpI64Store8 byte = 0x3c
OpI64Store16 byte = 0x3d
OpI64Store32 byte = 0x3e
OpMemSize byte = 0x3f
OpMemGrow byte = 0x40
OpI32Const byte = 0x41
OpI64Const byte = 0x42
OpF32Const byte = 0x43
OpF64Const byte = 0x44
OpI32Eqz byte = 0x45
OpI32Eq byte = 0x46
OpI32Ne byte = 0x47
OpI32LtS byte = 0x48
OpI32LtU byte = 0x49
OpI32GtS byte = 0x4a
OpI32GtU byte = 0x4b
OpI32LeS byte = 0x4c
OpI32LeU byte = 0x4d
OpI32GeS byte = 0x4e
OpI32GeU byte = 0x4f
OpI64Eqz byte = 0x50
OpI64Eq byte = 0x51
OpI64Ne byte = 0x52
OpI64LtS byte = 0x53
OpI64LtU byte = 0x54
OpI64GtS byte = 0x55
OpI64GtU byte = 0x56
OpI64LeS byte = 0x57
OpI64LeU byte = 0x58
OpI64GeS byte = 0x59
OpI64GeU byte = 0x5a
OpF32Eq byte = 0x5b
OpF32Ne byte = 0x5c
OpF32Lt byte = 0x5d
OpF32Gt byte = 0x5e
OpF32Le byte = 0x5f
OpF32Ge byte = 0x60
OpF64Eq byte = 0x61
OpF64Ne byte = 0x62
OpF64Lt byte = 0x63
OpF64Gt byte = 0x64
OpF64Le byte = 0x65
OpF64Ge byte = 0x66
OpI32Clz byte = 0x67
OpI32Ctz byte = 0x68
OpI32Popcnt byte = 0x69
OpI32Add byte = 0x6a
OpI32Sub byte = 0x6b
OpI32Mul byte = 0x6c
OpI32DivS byte = 0x6d
OpI32DivU byte = 0x6e
OpI32RemS byte = 0x6f
OpI32RemU byte = 0x70
OpI32And byte = 0x71
OpI32Or byte = 0x72
OpI32Xor byte = 0x73
OpI32Shl byte = 0x74
OpI32ShrS byte = 0x75
OpI32ShrU byte = 0x76
OpI32Rotl byte = 0x77
OpI32Rotr byte = 0x78
OpI64Clz byte = 0x79
OpI64Ctz byte = 0x7a
OpI64Popcnt byte = 0x7b
OpI64Add byte = 0x7c
OpI64Sub byte = 0x7d
OpI64Mul byte = 0x7e
OpI64DivS byte = 0x7f
OpI64DivU byte = 0x80
OpI64RemS byte = 0x81
OpI64RemU byte = 0x82
OpI64And byte = 0x83
OpI64Or byte = 0x84
OpI64Xor byte = 0x85
OpI64Shl byte = 0x86
OpI64ShrS byte = 0x87
OpI64ShrU byte = 0x88
OpI64Rotl byte = 0x89
OpI64Rotr byte = 0x8a
OpF32Abs byte = 0x8b
OpF32Neg byte = 0x8c
OpF32Ceil byte = 0x8d
OpF32Floor byte = 0x8e
OpF32Trunc byte = 0x8f
OpF32Nearest byte = 0x90
OpF32Sqrt byte = 0x91
OpF32Add byte = 0x92
OpF32Sub byte = 0x93
OpF32Mul byte = 0x94
OpF32Div byte = 0x95
OpF32Min byte = 0x96
OpF32Max byte = 0x97
OpF32Copysign byte = 0x98
OpF64Abs byte = 0x99
OpF64Neg byte = 0x9a
OpF64Ceil byte = 0x9b
OpF64Floor byte = 0x9c
OpF64Trunc byte = 0x9d
OpF64Nearest byte = 0x9e
OpF64Sqrt byte = 0x9f
OpF64Add byte = 0xa0
OpF64Sub byte = 0xa1
OpF64Mul byte = 0xa2
OpF64Div byte = 0xa3
OpF64Min byte = 0xa4
OpF64Max byte = 0xa5
OpF64Copysign byte = 0xa6
OpI32WrapI64 byte = 0xa7
OpI32TruncF32S byte = 0xa8
OpI32TruncF32U byte = 0xa9
OpI32TruncF64S byte = 0xaa
OpI32TruncF64U byte = 0xab
OpI64ExtendI32S byte = 0xac
OpI64ExtendI32U byte = 0xad
OpI64TruncF32S byte = 0xae
OpI64TruncF32U byte = 0xaf
OpI64TruncF64S byte = 0xb0
OpI64TruncF64U byte = 0xb1
OpF32ConvertI32S byte = 0xb2
OpF32ConvertI32U byte = 0xb3
OpF32ConvertI64S byte = 0xb4
OpF32ConvertI64U byte = 0xb5
OpF32DemoteF64 byte = 0xb6
OpF64ConvertI32S byte = 0xb7
OpF64ConvertI32U byte = 0xb8
OpF64ConvertI64S byte = 0xb9
OpF64ConvertI64U byte = 0xba
OpF64PromoteF32 byte = 0xbb
OpI32ReinterpretF32 byte = 0xbc
OpI64ReinterpretF64 byte = 0xbd
OpF32ReinterpretI32 byte = 0xbe
OpF64ReinterpretI64 byte = 0xbf
OpI32Extend8S byte = 0xc0
OpI32Extend16S byte = 0xc1
OpI64Extend8S byte = 0xc2
OpI64Extend16S byte = 0xc3
OpI64Extend32S byte = 0xc4
// OpRefNull / OpRefFunc model reference-typed operands. wasm2go does not
// implement reference types, but the multi-package call-graph scanner
// recognises OpRefFunc as a function reference so it can record those
// edges alongside direct OpCall edges.
OpRefNull byte = 0xd0
OpRefFunc byte = 0xd2
OpPrefixFC byte = 0xfc // saturating-trunc, bulk-memory
OpPrefixFD byte = 0xfd // SIMD proposal: v128 ops
OpPrefixFE byte = 0xfe // threads proposal: atomics
)