Files
7a32e95b57 Remove environment-variable branches; fusion debug becomes an option (#43)
* Remove environment-variable branches; fusion debug becomes an option

Every behavioral switch must be a typed Option; environment
variables are not part of the tool's interface. This drops the
diagnostic scaffolding the memory64 branch accumulated and converts
the one broadly useful switch:

- WASM2GO_FUSE_DEBUG -> Options.FuseDebug (CLI -fuse-debug). The
  fusion-refusal histogram stays available, now discoverable and
  typed; the ad-hoc large-loop body dumps, candidate-collection
  probes, and size-tagged reject variants it grew during diagnosis
  are gone (plain reject tags remain).
- WASM2GO_CHASE_MAX / WASM2GO_CHASE_COUNT: the chase-site bisect
  counter is removed outright (window-level differential tests
  superseded it).
- WASM2GO_REGALLOC_BISECT_LO/_HI: the per-function regalloc bisect
  block and its parseTrailingInt helper are removed; regalloc is
  unconditional again.
- WASM2GO_REGALLOC_DEBUG / WASM2GO_REGALLOC_DUMP: removed along
  with the verifyRegHomes / dumpRegHomes debug-only routines they
  gated.
- WASM2GO_ASM_EMIT_DEBUG: removed; the stub fallback itself is
  unchanged.

No os.Getenv remains outside tests. Output is unchanged (the
removed code was diagnosis-only); full suites and lint pass.

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

* Make vec_dot row pairing opt-in; scrub project names from source

The row/column pairing recognizer targeted one specific runtime by
construction: it ran unconditionally on every module and hardcoded
that runtime's type-enum value for the entry to pair. Recognition
was already structural (byte-verified trait rows, self-typed entry,
exact eight-parameter signature, ambiguity disables), but the
trigger was not generic.

Options.VecDotPairEntry (CLI -vec-dot-pair-entry) now selects the
trait-table entry to pair; zero — the default — disables the scan
entirely. The caller supplies its runtime's enum value, removing
the hardcoded constant. With the entry supplied the output is
byte-identical to the previous behavior; without it no module is
touched (verified both ways on the largest integration module).

Alongside, every reference to specific downstream projects in
non-test sources (identifiers, stderr messages, comments) is
rewritten in structural terms — the transpiler's contracts are
shapes and byte-verified layouts, not project names. The "nrc"
in nrc2 is documented as the trait signature's rows-per-call
parameter rather than borrowed vocabulary.

Callers that relied on the automatic scan must pass their entry
index (the integration pipeline gains -vec-dot-pair-entry 8).

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

---------

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

273 lines
7.4 KiB
Go

package pass
import "github.com/goccy/wasm2go/internal/ssa"
// RecognizeF16Gather rewrites the f16->f32 table-gather idiom.
// The wasm loads four contiguous f16 values widened into i32x4 lanes,
// then rebuilds an f32x4 by looking each lane up in a module-resident
// conversion table:
//
// V = v128_load16x4_u(addr, off)
// e_k = i32x4_extract_lane(V, k)
// L0 = v128_load32_zero(e_0<<2, T) // T may ride the
// L_k = v128_load32_lane(e_k<<2 + T, 0, k, L_{k-1}) // memarg or addr
//
// tableOK reports whether the table at T is byte-for-byte the IEEE
// binary16->binary32 conversion (the caller verifies the module's
// initial data image). When it is, the whole chain equals a pure lane
// conversion of V — the arm64 splicer emits XTN + FCVTL — and the
// rewrite is bit-exact for every input including NaN payloads.
//
// The tail load is rewritten in place to the pure op; the inner loads
// are neutered to zero constants (they are pinned as potentially
// trapping, but their addresses lie inside the verified table region,
// which sits in the initial data image and therefore inside memory
// forever), and the extracts and shifts die in DCE. V itself and its
// trap behavior are untouched.
func RecognizeF16Gather(f *ssa.Func, tableOK func(base uint32) bool) bool {
if tableOK == nil {
return false
}
uses := map[*ssa.Value]int{}
for _, b := range f.Blocks {
for _, v := range b.Values {
for _, a := range v.Args {
uses[a]++
}
}
}
changed := false
for _, b := range f.Blocks {
for i := 0; i < len(b.Values); i++ {
if rewriteF16Gather(f, b, i, uses, tableOK) {
changed = true
}
}
}
return changed
}
// f16Effectful reports ops that may write memory or transfer control —
// the barrier set for reordering the scalar loads into one widening
// load. Loads are not barriers (loads cannot conflict with loads).
func f16Effectful(v *ssa.Value) bool {
switch v.Op {
case ssa.OpStore8, ssa.OpStore16, ssa.OpStore32, ssa.OpStore64,
ssa.OpCallDirect, ssa.OpCallIndirect, ssa.OpCallImport,
ssa.OpMemGrow, ssa.OpHelperCall:
return true
case ssa.OpSimdMemCall:
return true // conservatively: includes SIMD stores
}
return false
}
func rewriteF16Gather(f *ssa.Func, blk *ssa.Block, tailIdx int, uses map[*ssa.Value]int, tableOK func(base uint32) bool) bool {
tail := blk.Values[tailIdx]
if !isSimdMem(tail, "simd_v128_load32_lane") || len(tail.Args) != 4 ||
!isConst32(tail.Args[2], 3) {
return false
}
var loads [3]*ssa.Value
var srcs [4]f16Src
var offs [4]int64
tOff, ok := constOf(tail.Args[1])
if !ok {
return false
}
offs[3] = tOff
s3, ok := matchF16LaneAddr(tail.Args[0], 3)
if !ok {
return false
}
srcs[3] = s3
cur := tail.Args[3]
for lane := int64(2); lane >= 1; lane-- {
if !isSimdMem(cur, "simd_v128_load32_lane") || len(cur.Args) != 4 ||
!isConst32(cur.Args[2], lane) || uses[cur] != 1 {
return false
}
if offs[lane], ok = constOf(cur.Args[1]); !ok {
return false
}
sk, ok := matchF16LaneAddr(cur.Args[0], lane)
if !ok {
return false
}
srcs[lane] = sk
loads[lane] = cur
cur = cur.Args[3]
}
if !isSimdMem(cur, "simd_v128_load32_zero") || len(cur.Args) != 2 || uses[cur] != 1 {
return false
}
s0, ok := matchF16LaneAddr(cur.Args[0], 0)
if !ok {
return false
}
if offs[0], ok = constOf(cur.Args[1]); !ok {
return false
}
srcs[0] = s0
loads[0] = cur
// One table constant everywhere: each lane's effective address is
// (h<<2) + table, with the table addend split arbitrarily between
// the address expression and the memarg offset.
table := srcs[3].table + offs[3]
if table == 0 {
return false
}
for k := 0; k < 4; k++ {
if srcs[k].table+offs[k] != table {
return false
}
}
if table < 0 || !tableOK(uint32(table)) {
return false
}
var V *ssa.Value
switch {
case srcs[0].vec != nil:
// Widening-load variant: all lanes extract from one
// v128_load16x4_u.
V = srcs[0].vec
if srcs[1].vec != V || srcs[2].vec != V || srcs[3].vec != V ||
!isSimdMem(V, "simd_v128_load16x4_u") {
return false
}
case srcs[0].ld != nil:
// Scalar-load variant: four i32.load16_u at base+c+2k. The +2k
// may ride the load's AuxInt or explicit Add32-with-constant
// address arithmetic; normalize each address to (root, total).
root0, c := memRoot(srcs[0].ld)
for k := int64(0); k < 4; k++ {
ld := srcs[k].ld
if ld == nil {
return false
}
rk, ck := memRoot(ld)
if rk != root0 || ck != c+2*k || ld.Block != blk || uses[ld] != 1 {
return false
}
}
first := tailIdx
involved := map[*ssa.Value]bool{tail: true, loads[0]: true, loads[1]: true, loads[2]: true}
for k := 0; k < 4; k++ {
involved[srcs[k].ld] = true
}
for i := 0; i < tailIdx; i++ {
if involved[blk.Values[i]] && i < first {
first = i
}
}
for i := first; i < tailIdx; i++ {
v := blk.Values[i]
if !involved[v] && f16Effectful(v) {
return false
}
}
cst := blk.NewValueBefore(f, tailIdx, ssa.OpConst32, ssa.TypeI32, srcs[0].ld.AuxInt, nil)
V = blk.NewValueBefore(f, tailIdx+1, ssa.OpSimdMemCall, ssa.TypeV128, 0, "simd_v128_load16x4_u", srcs[0].ld.Args[0], cst)
for k := 0; k < 4; k++ {
ld := srcs[k].ld
ld.Op = ssa.OpConst32
ld.AuxInt = 0
ld.Aux = nil
ld.Args = nil
}
default:
return false
}
// Rewrite the tail into the pure conversion and neuter the inner
// loads (see the function comment for why that is sound).
tail.Op = ssa.OpSimdCall
tail.Aux = "simd_f16x4_cvt"
tail.AuxInt = 0
tail.Args = []*ssa.Value{V}
for _, l := range loads {
l.Op = ssa.OpSimdConst
l.AuxInt = 0
l.Aux = [2]uint64{0, 0}
l.Args = nil
}
return true
}
// f16Src is one matched lane source: either an extract from a
// widening v128 load (vec) or a scalar i32.load16_u (ld).
type f16Src struct {
vec *ssa.Value // load16x4_u the lane extracts from
ld *ssa.Value // OpLoad16U feeding the lane directly
table int64
}
// matchF16LaneAddr matches `SRC << 2 [+ T]` where SRC is
// `i32x4_extract_lane(V, lane)` or a scalar `i32.load16_u`.
func matchF16LaneAddr(a *ssa.Value, lane int64) (f16Src, bool) {
var out f16Src
if a.Op == ssa.OpAdd32 && len(a.Args) == 2 {
x, c := a.Args[0], a.Args[1]
if _, isC := constOf(x); isC {
x, c = c, x
}
t, isC := constOf(c)
if !isC {
return out, false
}
out.table = t
a = x
}
if a.Op != ssa.OpShl32 || len(a.Args) != 2 || !isConst32(a.Args[1], 2) {
return out, false
}
ex := a.Args[0]
if ex.Op == ssa.OpLoad16U && len(ex.Args) == 1 && ex.Type == ssa.TypeI32 {
out.ld = ex
return out, true
}
if ex.Op != ssa.OpSimdCall || len(ex.Args) != 2 || !isConst32(ex.Args[1], lane) {
return out, false
}
if n, _ := ex.Aux.(string); n != "simd_i32x4_extract_lane" {
return out, false
}
out.vec = ex.Args[0]
return out, true
}
func isSimdMem(v *ssa.Value, name string) bool {
if v.Op != ssa.OpSimdMemCall {
return false
}
n, _ := v.Aux.(string)
return n == name
}
func constOf(v *ssa.Value) (int64, bool) {
if v.Op != ssa.OpConst32 && v.Op != ssa.OpConst64 {
return 0, false
}
return v.AuxInt, true
}
// memRoot normalizes a load's address to (root value, total constant
// offset): the AuxInt memarg plus any Add32-with-constant chain in
// the address expression.
func memRoot(ld *ssa.Value) (*ssa.Value, int64) {
total := ld.AuxInt
a := ld.Args[0]
for (a.Op == ssa.OpAdd32 || a.Op == ssa.OpAdd64) && len(a.Args) == 2 {
x, c := a.Args[0], a.Args[1]
if _, isC := constOf(x); isC {
x, c = c, x
}
cv, isC := constOf(c)
if !isC {
break
}
total += cv
a = x
}
return a, total
}