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]>
877 lines
26 KiB
Go
877 lines
26 KiB
Go
package ssa
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// Loop outlining.
|
|
//
|
|
// gc's register allocator and the structured emitter both degrade on
|
|
// giant translated functions: allocation quality collapses into spill
|
|
// storms, and CFGs past the duplication budget fall to goto form where
|
|
// loop fusion never fires. Outlining moves a whole natural loop into
|
|
// its own function — the parent shrinks and the extracted body becomes
|
|
// a small, structurable unit — without changing any observable
|
|
// behaviour: the loop's boundary values become plain parameters and
|
|
// results, and traps keep propagating as panics.
|
|
//
|
|
// A loop is eligible when its call boundary is trivially representable:
|
|
//
|
|
// - exactly one entry edge (a unique preheader → header edge),
|
|
// - exactly one exit TARGET block, with every phi in it agreeing on
|
|
// one value across all exiting edges (so the collapsed edge keeps
|
|
// the phi well-formed),
|
|
// - at most one value defined inside and used outside (the single
|
|
// call result the downstream call marshalling supports), of scalar
|
|
// type,
|
|
// - no v128 values crossing the boundary in either direction (they
|
|
// ride as uint64 pairs and would need multi-results),
|
|
// - the enclosing function has no EH state (try regions / mutable
|
|
// locals).
|
|
//
|
|
// Constants living outside the loop are cloned into the extracted
|
|
// body instead of being passed, which keeps parameter lists to the
|
|
// values that actually vary.
|
|
|
|
// OutlinedFunc pairs an extracted function with its boundary shape.
|
|
// Packed marks a boundary too wide for the register ABI: the caller
|
|
// passes one pointer to a uint64 slot array instead of individual
|
|
// scalar arguments (emission-level concern; the SSA signature keeps
|
|
// the logical parameter list either way).
|
|
type OutlinedFunc struct {
|
|
Fn *Func
|
|
Packed bool
|
|
}
|
|
|
|
// outlinePackedMax bounds a packed boundary: beyond this the loop is
|
|
// carrying so much context that extraction stops being attractive.
|
|
// Must match the module scratch array length in the emitter.
|
|
const outlinePackedMax = 128
|
|
|
|
// outlinePackAmortRatio is the minimum body-values-per-boundary-slot
|
|
// ratio for a PACKED extraction on a memory64 module: the caller
|
|
// writes every slot into the scratch array and the outlined function
|
|
// reads them all back on every call, and with i64 locals doubling the
|
|
// callers' register pressure the round trip stops paying for wide
|
|
// boundaries. Measured on the largest integration module: gating these packs
|
|
// gains ~17% memory64 prompt throughput (the ~130-slot conversion
|
|
// loop), while the SAME gate on wasm32 LOSES ~25% — its outlined
|
|
// conversion loop compiles better extracted than inline — so the
|
|
// ratio applies only when wide.
|
|
const outlinePackAmortRatio = 24
|
|
|
|
// outlineV128MinBody is the minimum body size for a v128-boundary loop.
|
|
const outlineV128MinBody = 600
|
|
|
|
// outlineMaxShare rejects a body covering most of the function (the
|
|
// giant driver loop): outlining it would just move the problem, and
|
|
// it would take every nested loop with it into a function that is
|
|
// never re-scanned. Measured on the largest integration module: extracting drained
|
|
// drivers (rejecting them only while a nested eligible loop remained)
|
|
// was a consistent ~0.6% tg regression, so the hard cap stays.
|
|
const outlineMaxShare = 0.8
|
|
|
|
// OutlineLoops extracts eligible loops of at least minValues values
|
|
// from f. name generates the extracted function's name from the loop
|
|
// header's block ID (the caller owns naming policy). The parent is
|
|
// modified in place and re-verified; extracted functions are returned
|
|
// ready for their own lowering pipeline.
|
|
func OutlineLoops(f *Func, name func(header BlockID) string, minValues int, wide bool) ([]OutlinedFunc, error) {
|
|
if f.MutableLocals || len(f.TryRegions) > 0 {
|
|
return nil, nil
|
|
}
|
|
total := 0
|
|
for _, b := range f.Blocks {
|
|
total += len(b.Values)
|
|
}
|
|
if total < minValues {
|
|
return nil, nil
|
|
}
|
|
var out []OutlinedFunc
|
|
taken := map[BlockID]bool{} // blocks already extracted (stale IDs guarded by re-detection)
|
|
// Re-detect after each extraction: the surgery invalidates block
|
|
// sets, and one extraction can make an enclosing loop eligible.
|
|
for {
|
|
cand := findOutlineCandidate(f, minValues, taken, wide)
|
|
if cand == nil {
|
|
break
|
|
}
|
|
g, err := extractLoop(f, cand, name(cand.header.ID))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("outline %s L%d: %w", f.Name, cand.header.ID, err)
|
|
}
|
|
// Foreign-edge sweep before Verify: a leftover reference to a
|
|
// moved block would send Verify's dominator fixpoint into a
|
|
// cycle instead of an error.
|
|
for _, fn := range []*Func{f, g} {
|
|
own := map[*Block]bool{}
|
|
for _, b := range fn.Blocks {
|
|
own[b] = true
|
|
}
|
|
for _, b := range fn.Blocks {
|
|
for _, e := range b.Preds {
|
|
if !own[e.Block] {
|
|
return nil, fmt.Errorf("outline %s: %s block L%d (kind %d) pred references foreign block L%d (kind %d); cand header L%d exit L%d", f.Name, fn.Name, b.ID, b.Kind, e.Block.ID, e.Block.Kind, cand.header.ID, cand.exit.ID)
|
|
}
|
|
}
|
|
for _, e := range b.Succs {
|
|
if !own[e.Block] {
|
|
return nil, fmt.Errorf("outline %s: %s block L%d succ references foreign block", f.Name, fn.Name, b.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := Verify(f); err != nil {
|
|
return nil, fmt.Errorf("outline %s L%d: parent: %w", f.Name, cand.header.ID, err)
|
|
}
|
|
if err := Verify(g); err != nil {
|
|
return nil, fmt.Errorf("outline %s L%d: extracted: %w", f.Name, cand.header.ID, err)
|
|
}
|
|
out = append(out, OutlinedFunc{Fn: g, Packed: cand.packed})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type outlineCand struct {
|
|
header *Block
|
|
body map[BlockID]bool
|
|
nBody int // values in body
|
|
liveIns []*Value
|
|
liveOut *Value
|
|
// exit is the single exit target, or nil when exit2 is set: a
|
|
// TWO-exit loop returns an encoded int64 — exit index in the
|
|
// high word, the (optional, i32) live-out in the low — and the
|
|
// parent dispatches on the index.
|
|
exit *Block
|
|
exit2 [2]*Block
|
|
prei int // index of the preheader edge in header.Preds
|
|
packed bool
|
|
}
|
|
|
|
// findOutlineCandidate returns the largest eligible loop of f, or nil.
|
|
func findOutlineCandidate(f *Func, minValues int, taken map[BlockID]bool, wide bool) *outlineCand {
|
|
idom := Dominators(f)
|
|
dominates := func(a, b *Block) bool {
|
|
for b != nil {
|
|
if b == a {
|
|
return true
|
|
}
|
|
p := idom[b.ID]
|
|
if p == b {
|
|
return false
|
|
}
|
|
b = p
|
|
}
|
|
return false
|
|
}
|
|
total := 0
|
|
for _, b := range f.Blocks {
|
|
total += len(b.Values)
|
|
}
|
|
bodies := map[BlockID]map[BlockID]bool{}
|
|
headers := map[BlockID]*Block{}
|
|
for _, b := range f.Blocks {
|
|
for _, s := range b.Succs {
|
|
h := s.Block
|
|
if !dominates(h, b) {
|
|
continue
|
|
}
|
|
body := bodies[h.ID]
|
|
if body == nil {
|
|
body = map[BlockID]bool{h.ID: true}
|
|
bodies[h.ID] = body
|
|
headers[h.ID] = h
|
|
}
|
|
stack := []*Block{b}
|
|
for len(stack) > 0 {
|
|
x := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
if body[x.ID] {
|
|
continue
|
|
}
|
|
body[x.ID] = true
|
|
for _, p := range x.Preds {
|
|
stack = append(stack, p.Block)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
blockOf := map[BlockID]*Block{}
|
|
for _, b := range f.Blocks {
|
|
blockOf[b.ID] = b
|
|
}
|
|
var best *outlineCand
|
|
for hid, body := range bodies {
|
|
if taken[hid] {
|
|
continue
|
|
}
|
|
n := 0
|
|
for id := range body {
|
|
n += len(blockOf[id].Values)
|
|
}
|
|
if n < minValues {
|
|
continue
|
|
}
|
|
if float64(n) > outlineMaxShare*float64(total) {
|
|
continue
|
|
}
|
|
c := checkOutlineCand(f, headers[hid], body, minValues, dominates, wide)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
// Ties break on header ID: bodies is a map, and a map-order
|
|
// winner makes the whole extraction sequence — block
|
|
// renumbering, emission order, helper interning — differ from
|
|
// run to run on identical input.
|
|
if best == nil || c.nBody > best.nBody ||
|
|
(c.nBody == best.nBody && c.header.ID < best.header.ID) {
|
|
best = c
|
|
}
|
|
}
|
|
if best != nil {
|
|
taken[best.header.ID] = true
|
|
}
|
|
return best
|
|
}
|
|
|
|
// checkOutlineCand tests eligibility and computes the boundary. Size
|
|
// and share policy live in findOutlineCandidate; this checks only the
|
|
// boundary shape.
|
|
func checkOutlineCand(f *Func, h *Block, body map[BlockID]bool, minValues int, dominates func(a, b *Block) bool, wide bool) *outlineCand {
|
|
c := &outlineCand{header: h, body: body}
|
|
reject := func(format string, args ...any) *outlineCand {
|
|
return nil
|
|
}
|
|
blockOf := map[BlockID]*Block{}
|
|
for _, b := range f.Blocks {
|
|
blockOf[b.ID] = b
|
|
}
|
|
for id := range body {
|
|
if blockOf[id] == nil {
|
|
return nil // stale candidate from a previous extraction
|
|
}
|
|
c.nBody += len(blockOf[id].Values)
|
|
}
|
|
if c.nBody < minValues {
|
|
return reject("size gate: %d values (min %d)", c.nBody, minValues)
|
|
}
|
|
// Single entry edge, and no side entrance into the body: an
|
|
// outside pred of a non-header body block (an irreducible region)
|
|
// would keep referencing the moved blocks after extraction.
|
|
c.prei = -1
|
|
for i, p := range h.Preds {
|
|
if body[p.Block.ID] {
|
|
continue
|
|
}
|
|
if c.prei != -1 {
|
|
return reject("multiple entry edges into header (preds L%d and L%d)", h.Preds[c.prei].Block.ID, p.Block.ID)
|
|
}
|
|
c.prei = i
|
|
}
|
|
if c.prei == -1 {
|
|
return reject("no entry edge from outside the body")
|
|
}
|
|
for id := range body {
|
|
if id == h.ID {
|
|
continue
|
|
}
|
|
for _, p := range blockOf[id].Preds {
|
|
if !body[p.Block.ID] {
|
|
return reject("side entrance: outside block L%d preds body block L%d", p.Block.ID, id)
|
|
}
|
|
}
|
|
}
|
|
// Defs inside; ops that cannot be moved.
|
|
defsIn := map[ValueID]*Value{}
|
|
for id := range body {
|
|
for _, v := range blockOf[id].Values {
|
|
defsIn[v.ID] = v
|
|
switch v.Op {
|
|
case OpLocalGet, OpLocalSet:
|
|
return reject("mutable-local op %s inside body (block L%d)", v.Op, id)
|
|
}
|
|
}
|
|
switch blockOf[id].Kind {
|
|
case BlockThrow:
|
|
return reject("throw block L%d inside body", id)
|
|
}
|
|
}
|
|
// Live-ins: invariant pure chains are recomputed inside the body,
|
|
// so only their FRONTIER (loads, phis, params — values whose
|
|
// results depend on when they run) rides as parameters.
|
|
liveInSet := map[ValueID]*Value{}
|
|
rematMemo := map[ValueID]bool{}
|
|
var canRemat func(v *Value) bool
|
|
canRemat = func(v *Value) bool {
|
|
if r, ok := rematMemo[v.ID]; ok {
|
|
return r
|
|
}
|
|
rematMemo[v.ID] = false // cycle guard (phis are excluded anyway)
|
|
ok := outlineRemat(v)
|
|
for _, a := range v.Args {
|
|
if !ok {
|
|
break
|
|
}
|
|
if defsIn[a.ID] != nil {
|
|
ok = false // inside-defined feeder: not an invariant chain
|
|
break
|
|
}
|
|
if !canRemat(a) && !outlineScalarType(a.Type) {
|
|
ok = false
|
|
break
|
|
}
|
|
}
|
|
rematMemo[v.ID] = ok
|
|
return ok
|
|
}
|
|
var noteIn func(a *Value) bool
|
|
noteIn = func(a *Value) bool {
|
|
if defsIn[a.ID] != nil {
|
|
return true
|
|
}
|
|
if canRemat(a) {
|
|
for _, aa := range a.Args {
|
|
if !noteIn(aa) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
if !outlineScalarType(a.Type) && a.Type != TypeV128 {
|
|
return false
|
|
}
|
|
liveInSet[a.ID] = a
|
|
return true
|
|
}
|
|
for id := range body {
|
|
for _, v := range blockOf[id].Values {
|
|
for _, a := range v.Args {
|
|
if !noteIn(a) {
|
|
return reject("non-scalar live-in: v%d (op %s, type %s) feeding v%d (op %s) in block L%d", a.ID, a.Op, a.Type, v.ID, v.Op, id)
|
|
}
|
|
}
|
|
}
|
|
if ctl := blockOf[id].Control; ctl != nil && !noteIn(ctl) {
|
|
return reject("non-scalar live-in as control: v%d (op %s, type %s) of block L%d", ctl.ID, ctl.Op, ctl.Type, id)
|
|
}
|
|
}
|
|
ints, floats, vecs := 0, 0, 0
|
|
for _, v := range liveInSet {
|
|
c.liveIns = append(c.liveIns, v)
|
|
switch v.Type {
|
|
case TypeF32, TypeF64:
|
|
floats++
|
|
case TypeV128:
|
|
vecs++
|
|
default:
|
|
ints++
|
|
}
|
|
}
|
|
if vecs > 0 && c.nBody < outlineV128MinBody {
|
|
return reject("v128 boundary needs %d+ values to amortize (have %d)", outlineV128MinBody, c.nBody)
|
|
}
|
|
if ints > outlineIntArgBudget || floats > outlineFloatArgBudget || vecs > 0 {
|
|
slots := ints + floats + 2*vecs
|
|
if slots > outlinePackedMax {
|
|
return reject("packed cap: %d int + %d float + %d v128 live-ins > %d slots", ints, floats, vecs, outlinePackedMax)
|
|
}
|
|
if wide && c.nBody < slots*outlinePackAmortRatio {
|
|
return reject("packed boundary unamortized: %d slots against %d body values (need %dx)", slots, c.nBody, outlinePackAmortRatio)
|
|
}
|
|
c.packed = true
|
|
}
|
|
sort.Slice(c.liveIns, func(i, j int) bool { return c.liveIns[i].ID < c.liveIns[j].ID })
|
|
// Live-outs and the single exit target.
|
|
liveOutSet := map[ValueID]*Value{}
|
|
var exits []*Block
|
|
for _, b := range f.Blocks {
|
|
if body[b.ID] {
|
|
for _, s := range b.Succs {
|
|
if body[s.Block.ID] {
|
|
continue
|
|
}
|
|
seen := false
|
|
for _, e := range exits {
|
|
if e == s.Block {
|
|
seen = true
|
|
}
|
|
}
|
|
if !seen {
|
|
exits = append(exits, s.Block)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
for _, v := range b.Values {
|
|
for _, a := range v.Args {
|
|
if defsIn[a.ID] != nil {
|
|
liveOutSet[a.ID] = a
|
|
}
|
|
}
|
|
}
|
|
if ctl := b.Control; ctl != nil && defsIn[ctl.ID] != nil {
|
|
liveOutSet[ctl.ID] = ctl
|
|
}
|
|
}
|
|
if len(exits) == 0 || len(exits) > 2 || len(liveOutSet) > 1 {
|
|
var outs []ValueID
|
|
for id := range liveOutSet {
|
|
outs = append(outs, id)
|
|
}
|
|
sortValueIDs(outs)
|
|
return reject("boundary shape: %d exit targets, %d live-outs %v", len(exits), len(liveOutSet), outs)
|
|
}
|
|
for _, v := range liveOutSet {
|
|
c.liveOut = v
|
|
}
|
|
if len(exits) == 1 {
|
|
if c.liveOut != nil && !outlineScalarType(c.liveOut.Type) {
|
|
return reject("live-out v%d has non-scalar type %s", c.liveOut.ID, c.liveOut.Type)
|
|
}
|
|
c.exit = exits[0]
|
|
} else {
|
|
// Two-exit form: the encoded return carries an i32 live-out
|
|
// in the low word, so wider types stay ineligible. Sort by
|
|
// block ID for a deterministic index assignment.
|
|
if c.liveOut != nil && c.liveOut.Type != TypeI32 {
|
|
return reject("two-exit live-out v%d has type %s (only i32 rides the encoded return)", c.liveOut.ID, c.liveOut.Type)
|
|
}
|
|
if exits[0].ID > exits[1].ID {
|
|
exits[0], exits[1] = exits[1], exits[0]
|
|
}
|
|
// Both return paths encode the live-out, so its definition
|
|
// must dominate every exiting block of either target.
|
|
if c.liveOut != nil {
|
|
for _, b := range f.Blocks {
|
|
if !body[b.ID] {
|
|
continue
|
|
}
|
|
for _, sc := range b.Succs {
|
|
if !body[sc.Block.ID] && !dominates(c.liveOut.Block, b) {
|
|
return reject("two-exit live-out v%d (block L%d) does not dominate exiting block L%d", c.liveOut.ID, c.liveOut.Block.ID, b.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
c.exit2 = [2]*Block{exits[0], exits[1]}
|
|
}
|
|
// Every phi in each exit target must agree on ONE value across
|
|
// its exiting edges — the collapsed call edge keeps a single arg.
|
|
phiTargets := exits
|
|
for _, exit := range phiTargets {
|
|
for _, v := range exit.Values {
|
|
if v.Op != OpPhi {
|
|
continue
|
|
}
|
|
var common *Value
|
|
for i, p := range exit.Preds {
|
|
if !body[p.Block.ID] {
|
|
continue
|
|
}
|
|
if i >= len(v.Args) {
|
|
return reject("exit L%d phi v%d has fewer args than preds", exit.ID, v.ID)
|
|
}
|
|
if common == nil {
|
|
common = v.Args[i]
|
|
} else if common != v.Args[i] {
|
|
return reject("exit L%d phi v%d disagrees across exiting edges (v%d vs v%d)", exit.ID, v.ID, common.ID, v.Args[i].ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The header's phis must take scalars on the entry edge (checked
|
|
// above via liveIns) — nothing extra here.
|
|
return c
|
|
}
|
|
|
|
func sortValueIDs(ids []ValueID) {
|
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
|
}
|
|
|
|
func outlineScalarType(t Type) bool {
|
|
switch t {
|
|
case TypeI32, TypeI64, TypeF32, TypeF64:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isOutlineConst reports values cheap and safe to clone into the
|
|
// extracted body instead of passing as parameters.
|
|
func isOutlineConst(v *Value) bool {
|
|
switch v.Op {
|
|
case OpConst32, OpConst64, OpConstF32, OpConstF64:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// outlineRemat reports whether a value may be recomputed inside the
|
|
// extracted body: pure scalar computation whose value cannot depend on
|
|
// WHEN it runs. Memory and global reads are excluded — re-executing
|
|
// them at the call site could observe stores that happened after the
|
|
// original — as are phis, params and anything v128-typed (v128 never
|
|
// crosses the boundary). Bool values (comparisons) are also excluded,
|
|
// which keeps loops with outside-defined bool feeders ineligible:
|
|
// admitting them was measured as a ~1% decode regression on the largest
|
|
// module — the bigger loops it unlocks absorb inner loops that are
|
|
// worth more as separate extractions.
|
|
func outlineRemat(v *Value) bool {
|
|
if v.HasSideEffect() {
|
|
return false
|
|
}
|
|
if !outlineScalarType(v.Type) && !isOutlineConst(v) {
|
|
return false
|
|
}
|
|
switch v.Op {
|
|
case OpPhi, OpParam, OpGlobalGet, OpSelect, OpSimdCall, OpSimdMemCall,
|
|
OpLoad8U, OpLoad8S, OpLoad16U, OpLoad16S, OpLoad32, OpLoad32U,
|
|
OpLoad32S, OpLoad64, OpLoadF32, OpLoadF64,
|
|
OpLocalGet, OpLocalSet, OpMemGrow:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// outlineArgRegBudget is the cross-arch minimum of Go's ABIInternal
|
|
// argument registers, minus the module pointer: amd64 assigns 9
|
|
// integer registers (arm64 has 16) and 15 float registers. Boundaries
|
|
// beyond this would marshal through the stack — a shape the asm
|
|
// call-site marshalling does not support, and pure-Go fallbacks are
|
|
// forbidden on the asm GOARCHs.
|
|
const (
|
|
outlineIntArgBudget = 8
|
|
outlineFloatArgBudget = 15
|
|
)
|
|
|
|
// extractLoop performs the surgery described in the package comment.
|
|
func extractLoop(f *Func, c *outlineCand, gname string) (*Func, error) {
|
|
body := c.body
|
|
twoExit := c.exit == nil
|
|
// Pointer-identity membership: block IDs are reassigned when the
|
|
// body is adopted into the new function, so every membership test
|
|
// after that point must NOT go through IDs.
|
|
inBody := map[*Block]bool{}
|
|
for _, b := range f.Blocks {
|
|
if body[b.ID] {
|
|
inBody[b] = true
|
|
}
|
|
}
|
|
h := c.header
|
|
var results []Type
|
|
if twoExit {
|
|
// Encoded return: exit index in the high word, the optional
|
|
// i32 live-out in the low.
|
|
results = []Type{TypeI64}
|
|
} else if c.liveOut != nil {
|
|
results = []Type{c.liveOut.Type}
|
|
}
|
|
params := make([]Type, len(c.liveIns))
|
|
for i, v := range c.liveIns {
|
|
params[i] = v.Type
|
|
}
|
|
g := NewFunc(gname, FuncSig{Params: params, Results: results})
|
|
|
|
// Entry block with the parameter values, then a clone site for
|
|
// outside constants.
|
|
entry := g.NewBlock(BlockPlain)
|
|
g.SetEntry(entry)
|
|
paramOf := map[ValueID]*Value{}
|
|
for i, v := range c.liveIns {
|
|
p := g.newValue(OpParam, v.Type, nil, entry, int64(i), nil)
|
|
entry.Values = append(entry.Values, p)
|
|
paramOf[v.ID] = p
|
|
}
|
|
// Invariant chains recompute in the entry block: clones are
|
|
// memoized and emitted in dependency order, with frontier values
|
|
// reading their parameter.
|
|
constOf := map[ValueID]*Value{}
|
|
var cloneChain func(v *Value) *Value
|
|
cloneChain = func(v *Value) *Value {
|
|
if cl := constOf[v.ID]; cl != nil {
|
|
return cl
|
|
}
|
|
if p := paramOf[v.ID]; p != nil {
|
|
return p
|
|
}
|
|
var args []*Value
|
|
for _, a := range v.Args {
|
|
args = append(args, cloneChain(a))
|
|
}
|
|
cl := g.newValue(v.Op, v.Type, args, entry, v.AuxInt, v.Aux)
|
|
entry.Values = append(entry.Values, cl)
|
|
constOf[v.ID] = cl
|
|
return cl
|
|
}
|
|
// Return block(s); the trailing copy is the returned value
|
|
// (BlockRet convention: last values match FuncSig.Results). The
|
|
// two-exit form gets one ret block per exit target, each
|
|
// returning its encoded index.
|
|
ret := g.NewBlock(BlockRet)
|
|
var ret2 *Block
|
|
if twoExit {
|
|
ret2 = g.NewBlock(BlockRet)
|
|
}
|
|
|
|
// Adopt the body blocks in the parent's deterministic order.
|
|
defsIn := map[ValueID]bool{}
|
|
var moved []*Block
|
|
keep := f.Blocks[:0]
|
|
for _, b := range f.Blocks {
|
|
if inBody[b] {
|
|
moved = append(moved, b)
|
|
for _, v := range b.Values {
|
|
defsIn[v.ID] = true
|
|
}
|
|
} else {
|
|
keep = append(keep, b)
|
|
}
|
|
}
|
|
f.Blocks = keep
|
|
remap := func(a *Value) *Value {
|
|
if defsIn[a.ID] {
|
|
return a
|
|
}
|
|
if p := paramOf[a.ID]; p != nil {
|
|
return p
|
|
}
|
|
return cloneChain(a)
|
|
}
|
|
for _, b := range moved {
|
|
b.f = g
|
|
b.ID = g.nextBlockID
|
|
g.nextBlockID++
|
|
g.Blocks = append(g.Blocks, b)
|
|
for _, v := range b.Values {
|
|
for i, a := range v.Args {
|
|
v.Args[i] = remap(a)
|
|
}
|
|
}
|
|
if b.Control != nil {
|
|
b.Control = remap(b.Control)
|
|
}
|
|
}
|
|
// Entry wiring: the preheader edge position now belongs to g's
|
|
// entry, so header phis keep their positional args.
|
|
pre := h.Preds[c.prei]
|
|
h.Preds[c.prei] = Edge{Block: entry, Index: 0}
|
|
entry.Succs = []Edge{{Block: h, Index: c.prei}}
|
|
// Exit wiring: every edge that left the loop now returns instead.
|
|
retOf := func(target *Block) *Block {
|
|
if twoExit && target == c.exit2[1] {
|
|
return ret2
|
|
}
|
|
return ret
|
|
}
|
|
for _, b := range moved {
|
|
for i, s := range b.Succs {
|
|
if s.Block == c.exit || (twoExit && (s.Block == c.exit2[0] || s.Block == c.exit2[1])) {
|
|
r := retOf(s.Block)
|
|
b.Succs[i] = Edge{Block: r, Index: len(r.Preds)}
|
|
r.Preds = append(r.Preds, Edge{Block: b, Index: i})
|
|
}
|
|
}
|
|
}
|
|
if twoExit {
|
|
for idx, r := range []*Block{ret, ret2} {
|
|
enc := g.newValue(OpConst64, TypeI64, nil, r, int64(idx)<<32, nil)
|
|
r.Values = append(r.Values, enc)
|
|
if c.liveOut != nil {
|
|
ext := g.newValue(OpExtend32To64U, TypeI64, []*Value{c.liveOut}, r, 0, nil)
|
|
or := g.newValue(OpOr64, TypeI64, []*Value{enc, ext}, r, 0, nil)
|
|
r.Values = append(r.Values, ext, or)
|
|
enc = or
|
|
}
|
|
cp := g.newValue(OpCopy, TypeI64, []*Value{enc}, r, 0, nil)
|
|
r.Values = append(r.Values, cp)
|
|
}
|
|
} else if c.liveOut != nil {
|
|
cp := g.newValue(OpCopy, c.liveOut.Type, []*Value{c.liveOut}, ret, 0, nil)
|
|
ret.Values = append(ret.Values, cp)
|
|
}
|
|
// Adopt values into g's table with fresh IDs; clear them from the
|
|
// parent's (Compact would strand pointers otherwise).
|
|
for _, b := range moved {
|
|
for _, v := range b.Values {
|
|
f.Values[v.ID] = nil
|
|
v.ID = ValueID(len(g.Values))
|
|
g.Values = append(g.Values, v)
|
|
}
|
|
}
|
|
compactParentValues(f)
|
|
|
|
// Parent-side call replacement: preheader → call block → exit(s).
|
|
kind := BlockPlain
|
|
if twoExit {
|
|
kind = BlockIf
|
|
}
|
|
callBlk := f.NewBlock(kind)
|
|
resType := TypeMem // void-call sentinel, keeps statement order
|
|
if twoExit {
|
|
resType = TypeI64
|
|
} else if c.liveOut != nil {
|
|
resType = c.liveOut.Type
|
|
}
|
|
callArgs := append([]*Value(nil), c.liveIns...)
|
|
call := f.newValue(OpCallDirect, resType, callArgs, callBlk, -1, gname)
|
|
callBlk.Values = append(callBlk.Values, call)
|
|
var outVal *Value // decoded live-out replacing outside uses
|
|
if twoExit {
|
|
c32 := f.newValue(OpConst64, TypeI64, nil, callBlk, 32, nil)
|
|
shr := f.newValue(OpShrU64, TypeI64, []*Value{call, c32}, callBlk, 0, nil)
|
|
idx := f.newValue(OpTrunc64To32, TypeI32, []*Value{shr}, callBlk, 0, nil)
|
|
callBlk.Values = append(callBlk.Values, c32, shr, idx)
|
|
callBlk.Control = idx
|
|
if c.liveOut != nil {
|
|
outVal = f.newValue(OpTrunc64To32, TypeI32, []*Value{call}, callBlk, 0, nil)
|
|
callBlk.Values = append(callBlk.Values, outVal)
|
|
}
|
|
} else if c.liveOut != nil {
|
|
outVal = call
|
|
}
|
|
// preheader edge → call block.
|
|
pre.Block.Succs[pre.Index] = Edge{Block: callBlk, Index: 0}
|
|
callBlk.Preds = []Edge{{Block: pre.Block, Index: pre.Index}}
|
|
if twoExit {
|
|
// BlockIf: control non-zero → Succs[0]. Index 1 encodes
|
|
// exit2[1], so it takes the then-slot.
|
|
for slot, exit := range []*Block{c.exit2[1], c.exit2[0]} {
|
|
var keptIdx []int
|
|
for i, p := range exit.Preds {
|
|
if !inBody[p.Block] {
|
|
keptIdx = append(keptIdx, i)
|
|
}
|
|
}
|
|
newPreds := make([]Edge, 0, len(keptIdx)+1)
|
|
for _, i := range keptIdx {
|
|
p := exit.Preds[i]
|
|
p.Block.Succs[p.Index] = Edge{Block: exit, Index: len(newPreds)}
|
|
newPreds = append(newPreds, p)
|
|
}
|
|
callBlk.Succs = append(callBlk.Succs, Edge{Block: exit, Index: len(newPreds)})
|
|
for _, v := range exit.Values {
|
|
if v.Op != OpPhi {
|
|
continue
|
|
}
|
|
var loopArg *Value
|
|
newArgs := make([]*Value, 0, len(newPreds)+1)
|
|
for _, i := range keptIdx {
|
|
newArgs = append(newArgs, v.Args[i])
|
|
}
|
|
for i, p := range exit.Preds {
|
|
if inBody[p.Block] {
|
|
loopArg = v.Args[i]
|
|
break
|
|
}
|
|
}
|
|
if loopArg == c.liveOut && c.liveOut != nil {
|
|
loopArg = outVal
|
|
}
|
|
if loopArg == nil {
|
|
return nil, fmt.Errorf("exit phi v%d has no loop arg", v.ID)
|
|
}
|
|
newArgs = append(newArgs, loopArg)
|
|
v.Args = newArgs
|
|
}
|
|
exit.Preds = append(newPreds, Edge{Block: callBlk, Index: slot})
|
|
}
|
|
// Remaining outside uses of the live-out become the decode.
|
|
if c.liveOut != nil {
|
|
for _, b := range f.Blocks {
|
|
for _, v := range b.Values {
|
|
if v == call || v == outVal {
|
|
continue
|
|
}
|
|
for i, a := range v.Args {
|
|
if a == c.liveOut {
|
|
v.Args[i] = outVal
|
|
}
|
|
}
|
|
}
|
|
if b.Control == c.liveOut {
|
|
b.Control = outVal
|
|
}
|
|
}
|
|
}
|
|
return g, nil
|
|
}
|
|
// exit preds: drop the loop edges, add the call block once.
|
|
exit := c.exit
|
|
var keptIdx []int
|
|
for i, p := range exit.Preds {
|
|
if !inBody[p.Block] {
|
|
keptIdx = append(keptIdx, i)
|
|
}
|
|
}
|
|
newPreds := make([]Edge, 0, len(keptIdx)+1)
|
|
for _, i := range keptIdx {
|
|
p := exit.Preds[i]
|
|
p.Block.Succs[p.Index] = Edge{Block: exit, Index: len(newPreds)}
|
|
newPreds = append(newPreds, p)
|
|
}
|
|
callBlk.Succs = []Edge{{Block: exit, Index: len(newPreds)}}
|
|
newPreds = append(newPreds, Edge{Block: callBlk, Index: 0})
|
|
// Phi args follow the pred rebuild; the collapsed loop edges agree
|
|
// on one value (eligibility), which the call result replaces when
|
|
// loop-defined.
|
|
for _, v := range exit.Values {
|
|
if v.Op != OpPhi {
|
|
continue
|
|
}
|
|
var loopArg *Value
|
|
newArgs := make([]*Value, 0, len(newPreds))
|
|
for _, i := range keptIdx {
|
|
newArgs = append(newArgs, v.Args[i])
|
|
}
|
|
for i, p := range exit.Preds {
|
|
if inBody[p.Block] {
|
|
loopArg = v.Args[i]
|
|
break
|
|
}
|
|
}
|
|
if loopArg == c.liveOut && c.liveOut != nil {
|
|
loopArg = outVal
|
|
}
|
|
if loopArg == nil {
|
|
return nil, fmt.Errorf("exit phi v%d has no loop arg", v.ID)
|
|
}
|
|
newArgs = append(newArgs, loopArg)
|
|
v.Args = newArgs
|
|
}
|
|
exit.Preds = newPreds
|
|
// Remaining outside uses of the live-out become the call result.
|
|
if c.liveOut != nil {
|
|
for _, b := range f.Blocks {
|
|
for _, v := range b.Values {
|
|
if v == call {
|
|
continue
|
|
}
|
|
for i, a := range v.Args {
|
|
if a == c.liveOut {
|
|
v.Args[i] = outVal
|
|
}
|
|
}
|
|
}
|
|
if b.Control == c.liveOut {
|
|
b.Control = outVal
|
|
}
|
|
}
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
// compactParentValues rebuilds f.Values without the nil slots left by
|
|
// an extraction, renumbering IDs densely.
|
|
func compactParentValues(f *Func) {
|
|
vals := []*Value{nil}
|
|
for _, b := range f.Blocks {
|
|
for _, v := range b.Values {
|
|
v.ID = ValueID(len(vals))
|
|
vals = append(vals, v)
|
|
}
|
|
}
|
|
f.Values = vals
|
|
f.nextValueID = ValueID(len(vals))
|
|
}
|