mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* 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]>
444 lines
12 KiB
Go
444 lines
12 KiB
Go
package pass
|
|
|
|
import (
|
|
"github.com/goccy/wasm2go/internal/ssa"
|
|
)
|
|
|
|
// RecognizeF16Store vectorizes the f16 STORE side of the conversion kernels:
|
|
// four lanes of an f32x4 each run the software fp32->fp16 rounding
|
|
// idiom (the same one RecognizeF32ToF16 matches) and are stored with
|
|
// i32.store16. The four conversions collapse into one packed op,
|
|
//
|
|
// bits = f16x4_cvt_bits(W) // u64: 16 bits per lane, LE
|
|
//
|
|
// (arm64: FCVTN + a NaN blend forcing sign|0x7E00, exactly the idiom
|
|
// semantics), and each store's VALUE becomes a shift of the packed
|
|
// word. The stores themselves — addresses, order, memory effects —
|
|
// are untouched, so the rewrite has no trap or ordering concerns;
|
|
// the orphaned per-lane idiom subgraphs die in DCE.
|
|
// lane16 is one recognized 16-bit store of a packed group.
|
|
type lane16 struct {
|
|
store *ssa.Value
|
|
blk *ssa.Block
|
|
idx int // position of the store in its block
|
|
}
|
|
|
|
func RecognizeF16Store(f *ssa.Func) bool {
|
|
uses := map[*ssa.Value]int{}
|
|
for _, b := range f.Blocks {
|
|
for _, v := range b.Values {
|
|
for _, a := range v.Args {
|
|
uses[a]++
|
|
}
|
|
}
|
|
}
|
|
|
|
// One group per source vector W, collected function-wide: each
|
|
// lane's conversion diamond joins in its own block, so the four
|
|
// stores usually sit in a chain of blocks.
|
|
groups := map[*ssa.Value]*[4]lane16{}
|
|
order := []*ssa.Value{}
|
|
for _, b := range f.Blocks {
|
|
for i, v := range b.Values {
|
|
if v.Op != ssa.OpStore16 || len(v.Args) < 2 {
|
|
continue
|
|
}
|
|
or := v.Args[1]
|
|
if or.Op != ssa.OpOr32 || len(or.Args) != 2 || uses[or] != 1 {
|
|
continue
|
|
}
|
|
var fv, w *ssa.Value
|
|
for k := 0; k < 2; k++ {
|
|
if gotF, gotW, ok := matchF16IdiomBits(or.Args[k], or.Args[1-k]); ok {
|
|
fv, w = gotF, gotW
|
|
break
|
|
}
|
|
}
|
|
if w == nil {
|
|
continue
|
|
}
|
|
// The lane arrives as an f32 extract, an i32 bits extract
|
|
// (i32x4.extract_lane of the float vector), or an i32 bits
|
|
// extract reinterpreted to f32.
|
|
ex := w
|
|
if fv != nil {
|
|
ex = fv
|
|
if ex.Op == ssa.OpHelperCall && len(ex.Args) == 1 {
|
|
if n, _ := ex.Aux.(string); n == "f32_reinterpret_i32" {
|
|
ex = ex.Args[0]
|
|
}
|
|
}
|
|
}
|
|
if ex.Op != ssa.OpSimdCall || len(ex.Args) != 2 {
|
|
continue
|
|
}
|
|
if n, _ := ex.Aux.(string); n != "simd_f32x4_extract_lane" && n != "simd_i32x4_extract_lane" {
|
|
continue
|
|
}
|
|
fv = ex
|
|
ln, ok := constOf(fv.Args[1])
|
|
if !ok || ln < 0 || ln > 3 {
|
|
continue
|
|
}
|
|
W := fv.Args[0]
|
|
g := groups[W]
|
|
if g == nil {
|
|
g = &[4]lane16{}
|
|
groups[W] = g
|
|
order = append(order, W)
|
|
}
|
|
if g[ln].store != nil {
|
|
g[ln].store = nil // duplicate lane: poison the slot
|
|
continue
|
|
}
|
|
g[ln] = lane16{store: v, blk: b, idx: i}
|
|
}
|
|
}
|
|
if len(groups) == 0 {
|
|
return false
|
|
}
|
|
idom := ssa.Dominators(f)
|
|
dominates := func(a, b *ssa.Block) bool {
|
|
for b != nil {
|
|
if b == a {
|
|
return true
|
|
}
|
|
p := idom[b.ID]
|
|
if p == b {
|
|
return false
|
|
}
|
|
b = p
|
|
}
|
|
return false
|
|
}
|
|
changed := false
|
|
for _, W := range order {
|
|
g := groups[W]
|
|
ok := true
|
|
anchor := -1 // lane whose store dominates the other three
|
|
for k := 0; k < 4 && ok; k++ {
|
|
if g[k].store == nil {
|
|
ok = false
|
|
break
|
|
}
|
|
dom := true
|
|
for j := 0; j < 4; j++ {
|
|
if j == k {
|
|
continue
|
|
}
|
|
if g[j].store == nil {
|
|
dom = false
|
|
break
|
|
}
|
|
if g[k].blk == g[j].blk {
|
|
if g[k].idx > g[j].idx {
|
|
dom = false
|
|
}
|
|
} else if !dominates(g[k].blk, g[j].blk) {
|
|
dom = false
|
|
}
|
|
}
|
|
if dom {
|
|
anchor = k
|
|
}
|
|
}
|
|
if !ok || anchor == -1 {
|
|
continue
|
|
}
|
|
b, first := g[anchor].blk, g[anchor].idx
|
|
bits := b.NewValueBefore(f, first, ssa.OpSimdCall, ssa.TypeI64, 0, "simd_f16x4_cvt_bits", W)
|
|
c32 := b.NewValueBefore(f, first+1, ssa.OpConst64, ssa.TypeI64, 32, nil)
|
|
hi64 := b.NewValueBefore(f, first+2, ssa.OpShrU64, ssa.TypeI64, 0, nil, bits, c32)
|
|
lo := b.NewValueBefore(f, first+3, ssa.OpTrunc64To32, ssa.TypeI32, 0, nil, bits)
|
|
hi := b.NewValueBefore(f, first+4, ssa.OpTrunc64To32, ssa.TypeI32, 0, nil, hi64)
|
|
c16 := b.NewValueBefore(f, first+5, ssa.OpConst32, ssa.TypeI32, 16, nil)
|
|
shift := map[int]*ssa.Value{}
|
|
for k := 0; k < 4; k++ {
|
|
half := lo
|
|
if k >= 2 {
|
|
half = hi
|
|
}
|
|
v := half
|
|
if k&1 == 1 {
|
|
sv, okS := shift[k/2]
|
|
if !okS {
|
|
sv = b.NewValueBefore(f, first+6, ssa.OpShrU32, ssa.TypeI32, 0, nil, half, c16)
|
|
shift[k/2] = sv
|
|
}
|
|
v = sv
|
|
}
|
|
// store16 truncates to 16 bits, so no masking needed.
|
|
g[k].store.Args[1] = v
|
|
}
|
|
changed = true
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// MergeF16Stores collapses each packed-conversion store group —
|
|
// four 16-bit stores whose values are shift/truncate selections of
|
|
// one simd_f16x4_cvt_bits word (the shape RecognizeF16Store leaves
|
|
// behind) — into a single 64-bit store of that word. It runs AFTER
|
|
// the empty-diamond fold: the group's stores sit in a straight-line
|
|
// block chain only once the orphaned NaN-select diamonds between
|
|
// them are gone. Conditions: lane addresses contiguous
|
|
// (base+0/2/4/6) and nothing memory-like between the stores in
|
|
// execution order. The merged store lands at the LAST group store's
|
|
// position (every operand dominates it); an out-of-bounds tail then
|
|
// traps with none of the four writes landed, where the unmerged form
|
|
// would have written the in-bounds lanes first — the same
|
|
// earlier-trap/no-partial-write relaxation the bounds-coalescing
|
|
// pass applies to SIMD spans.
|
|
func MergeF16Stores(f *ssa.Func) bool {
|
|
type packGroup struct {
|
|
lanes [4]lane16
|
|
bits *ssa.Value
|
|
}
|
|
groups := map[*ssa.Value]*packGroup{}
|
|
order := []*ssa.Value{}
|
|
for _, b := range f.Blocks {
|
|
for i, v := range b.Values {
|
|
if v.Op != ssa.OpStore16 || len(v.Args) < 2 {
|
|
continue
|
|
}
|
|
bits, ln := f16PackedLane(v.Args[1])
|
|
if bits == nil {
|
|
continue
|
|
}
|
|
g := groups[bits]
|
|
if g == nil {
|
|
g = &packGroup{bits: bits}
|
|
groups[bits] = g
|
|
order = append(order, bits)
|
|
}
|
|
if g.lanes[ln].store != nil {
|
|
g.lanes[ln].store = nil // duplicate: poison
|
|
continue
|
|
}
|
|
g.lanes[ln] = lane16{store: v, blk: b, idx: i}
|
|
}
|
|
}
|
|
changed := false
|
|
for _, bits := range order {
|
|
g := groups[bits]
|
|
ok := true
|
|
for k := 0; k < 4; k++ {
|
|
ok = ok && g.lanes[k].store != nil
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
if mergeF16StoreGroup(&g.lanes, bits) {
|
|
changed = true
|
|
}
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// f16PackedLane classifies a store value as lane k of a packed
|
|
// f16x4_cvt_bits word, or returns nil.
|
|
func f16PackedLane(v *ssa.Value) (*ssa.Value, int) {
|
|
lane := 0
|
|
if v.Op == ssa.OpShrU32 && len(v.Args) == 2 {
|
|
if c, ok := constOf(v.Args[1]); ok && c == 16 {
|
|
lane++
|
|
v = v.Args[0]
|
|
}
|
|
}
|
|
if v.Op != ssa.OpTrunc64To32 || len(v.Args) != 1 {
|
|
return nil, 0
|
|
}
|
|
v = v.Args[0]
|
|
if v.Op == ssa.OpShrU64 && len(v.Args) == 2 {
|
|
c := v.Args[1]
|
|
if (c.Op == ssa.OpConst64 || c.Op == ssa.OpConst32) && c.AuxInt == 32 {
|
|
lane += 2
|
|
v = v.Args[0]
|
|
}
|
|
}
|
|
if v.Op != ssa.OpSimdCall {
|
|
return nil, 0
|
|
}
|
|
if n, _ := v.Aux.(string); n != "simd_f16x4_cvt_bits" {
|
|
return nil, 0
|
|
}
|
|
return v, lane
|
|
}
|
|
|
|
// mergeF16StoreGroup performs the walk and rewrite for one group; the
|
|
// entry point is the first-executed store, found by trying each lane
|
|
// as the walk start (the chain is short, the walk cheap).
|
|
func mergeF16StoreGroup(g *[4]lane16, bits *ssa.Value) bool {
|
|
root0, off0 := f16StoreAddr(g[0].store)
|
|
for k := 1; k < 4; k++ {
|
|
r, o := f16StoreAddr(g[k].store)
|
|
if r != root0 || o != off0+int64(2*k) {
|
|
return false
|
|
}
|
|
}
|
|
inGroup := func(v *ssa.Value) int {
|
|
for k := 0; k < 4; k++ {
|
|
if g[k].store == v {
|
|
return k
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
for start := 0; start < 4; start++ {
|
|
cur, i := g[start].blk, g[start].idx
|
|
var last *lane16
|
|
seen := 0
|
|
for hops := 0; hops < 64; hops++ {
|
|
bail := false
|
|
for ; i < len(cur.Values); i++ {
|
|
v := cur.Values[i]
|
|
if k := inGroup(v); k >= 0 {
|
|
if k != start && seen == 0 {
|
|
// A group store precedes this start: wrong entry.
|
|
bail = true
|
|
break
|
|
}
|
|
seen++
|
|
last = &g[k]
|
|
if seen == 4 {
|
|
last.store.Op = ssa.OpStore64
|
|
last.store.Args[0] = g[0].store.Args[0]
|
|
last.store.Args[1] = bits
|
|
last.store.AuxInt = g[0].store.AuxInt
|
|
for k := 0; k < 4; k++ {
|
|
if &g[k] != last {
|
|
removeValue(g[k].blk, g[k].store)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
if f16MemLike(v, nil, nil) {
|
|
if seen > 0 {
|
|
return false
|
|
}
|
|
bail = true
|
|
break
|
|
}
|
|
}
|
|
if bail {
|
|
break
|
|
}
|
|
if len(cur.Succs) != 1 {
|
|
// Chain ends before completing the group: wrong start
|
|
// (walking from a non-first store hits the region's
|
|
// exit or loop branch first). Try the next start.
|
|
break
|
|
}
|
|
cur, i = cur.Succs[0].Block, 0
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// FuseF16CvtStores rewrites store64(addr, f16x4_cvt_bits(W)) — the
|
|
// shape MergeF16Stores leaves — into the single fused memory op
|
|
//
|
|
// simd_v128_f16x4_cvt_store(addr, off, W)
|
|
//
|
|
// so the conversion AND the store ride inside a fused region (and a
|
|
// fused loop) instead of round-tripping the vector through a GPR
|
|
// pair at the region boundary. Runs after MergeF16Stores; requires
|
|
// the packed word to have no other use. wide selects the memory64
|
|
// spelling: an i64 offset constant and the simd_m64_* helper.
|
|
func FuseF16CvtStores(f *ssa.Func, wide bool) bool {
|
|
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, v := range b.Values {
|
|
if v.Op != ssa.OpStore64 || len(v.Args) < 2 {
|
|
continue
|
|
}
|
|
bits := v.Args[1]
|
|
if bits.Op != ssa.OpSimdCall || uses[bits] != 1 {
|
|
continue
|
|
}
|
|
if n, _ := bits.Aux.(string); n != "simd_f16x4_cvt_bits" {
|
|
continue
|
|
}
|
|
W := bits.Args[0]
|
|
offOp, offType, helper := ssa.OpConst32, ssa.TypeI32, "simd_v128_f16x4_cvt_store"
|
|
if wide {
|
|
offOp, offType, helper = ssa.OpConst64, ssa.TypeI64, "simd_m64_v128_f16x4_cvt_store"
|
|
}
|
|
off := b.NewValueBefore(f, i, offOp, offType, v.AuxInt, nil)
|
|
v.Op = ssa.OpSimdMemCall
|
|
v.Type = ssa.TypeI32
|
|
v.Aux = helper
|
|
v.AuxInt = 0
|
|
v.Args = []*ssa.Value{v.Args[0], off, W}
|
|
changed = true
|
|
}
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// f16StoreAddr is memRoot with constant leaves folded into the
|
|
// offset (root nil): two stores at literal addresses compare by the
|
|
// address value, not by which OpConst32 spells it.
|
|
func f16StoreAddr(st *ssa.Value) (*ssa.Value, int64) {
|
|
root, off := memRoot(st)
|
|
if c, ok := constOf(root); ok {
|
|
return nil, off + c
|
|
}
|
|
return root, off
|
|
}
|
|
|
|
// f16PureHelpers are the arithmetic helper calls of the software
|
|
// f32->f16 idiom: pure value computations that the store rewrite
|
|
// orphans and DCE later deletes. The merge walk runs BEFORE that DCE,
|
|
// so it must see through them; every other helper call blocks the
|
|
// merge.
|
|
var f16PureHelpers = map[string]bool{
|
|
"i32_reinterpret_f32": true,
|
|
"f32_reinterpret_i32": true,
|
|
"f32_abs": true,
|
|
"f32_mul": true,
|
|
"f32_add": true,
|
|
}
|
|
|
|
// f16MemLike reports whether v touches memory or transfers control —
|
|
// anything the store merge must not move a write across.
|
|
func f16MemLike(v *ssa.Value, exclude1, exclude2 *ssa.Value) bool {
|
|
if v == exclude1 || v == exclude2 {
|
|
return false
|
|
}
|
|
switch v.Op {
|
|
case ssa.OpLoad8U, ssa.OpLoad8S, ssa.OpLoad16U, ssa.OpLoad16S,
|
|
ssa.OpLoad32, ssa.OpLoad32U, ssa.OpLoad32S, ssa.OpLoad64,
|
|
ssa.OpLoadF32, ssa.OpLoadF64,
|
|
ssa.OpStore8, ssa.OpStore16, ssa.OpStore32, ssa.OpStore64,
|
|
ssa.OpStoreF32, ssa.OpStoreF64,
|
|
ssa.OpCallDirect, ssa.OpCallIndirect, ssa.OpCallImport,
|
|
ssa.OpMemGrow, ssa.OpSimdMemCall:
|
|
return true
|
|
case ssa.OpHelperCall:
|
|
name, _ := v.Aux.(string)
|
|
return !f16PureHelpers[name]
|
|
}
|
|
return false
|
|
}
|
|
|
|
// removeValue drops v from b.Values.
|
|
func removeValue(b *ssa.Block, v *ssa.Value) {
|
|
for i, x := range b.Values {
|
|
if x == v {
|
|
b.Values = append(b.Values[:i], b.Values[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|