Files
9d2559e75d asmgen: SSA-based dual-arch asm emit (amd64 + arm64) (#7)
Replace the legacy direct-opcode compiler with an SSA pipeline that
emits per-function amd64.s and arm64.s for every wasm function. A
pure-Go body is also emitted (build-constrained on other GOOS/GOARCH)
and shares the same New() / exported-method surface, so callers never
see the path distinction.

Asm pipeline
- SSA always on; unsupported wasm features are now hard errors.
  Translate now drives Parse -> SSA build -> passes (Simplify, DCE,
  CSE) -> arch-specific emit.
- Per-arch block-local linear-scan regalloc with slot reuse and
  m-pointer caching (the *Module pointer is pinned in a reserved
  register across the function body — R11 on amd64, R4 on arm64 —
  so per-access reloads from the FP frame are gone).
- Cross-block stack allocation via interference-graph slot sharing
  reduces the typical per-function frame to 8-16 bytes.
- Peephole + branch fusion: drop dead spills, elide empty else
  fall-through, fold constant displacement into indexed load/store,
  fuse AND+NE+BlockIf into BTL+JCC.
- A new cross-block linear-scan allocator with Belady eviction
  lives in internal/regalloc/ and is gated behind
  WASM2GO_NEWREGALLOC for measurement.

Output shape
- Bulk-dispatch exports: when -bulk-export-prefix matches at least
  one export, each <prefix><svc>_<mt> becomes a standalone
  Inv_<svc>_<mt> function with inlined trap-recovery (defer +
  recover + mutable-global snapshot/restore). The Go linker drops
  whichever Inv_* the consumer never calls.
- Cross-package asm CALL: when every byte of an import path maps to
  a plan-9 identifier rune ("/" -> U+2215, "." -> U+00B7), the CALL
  target emits the direct cross-package symbol; otherwise asm falls
  back to a per-chunk Go-body trampoline.
- Multi-package layout for large modules: bodies split across
  p0..p<N> with go:linkname bridges in alias.go and per-chunk
  amd64.s / arm64.s sidecars. Single-file mode remains supported
  for modules under the internal multi-file threshold.

Coverage and tests
- internal/asmgen, internal/codegen, internal/regalloc, internal/ssa,
  internal/wasm pass with -race. Total cross-package statement
  coverage 85.8%, above the enforced 85% threshold.
- A new TestRecoverTrapsAreSurfacedAsErrors end-to-end test
  (testdata/cg_recover.wat) pins the Inv_<svc>_<mt> trap-recovery
  contract: wasm traps surface as errors (no Go panic), mutable
  globals restore to wrapper-entry values, and 5000 trap+recover
  cycles leak no goroutines and keep the Go heap delta under 1 MiB
  after a forced GC.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-10 10:54:22 +09:00

206 lines
6.6 KiB
Go

package ssa
import (
"encoding/json"
"fmt"
"sort"
)
// MemMetrics accumulates memory-access classification counts across an
// entire module. It is the data behind the memory-promotion observability
// requirement: report, in `promoted / total` (y/x) form, how much of
// the linear-memory traffic the transpiler proved function-local.
type MemMetrics struct {
// Counts over every memory access in every SSA-lowered function.
Total int
Frame int
Rodata int
Slab int
// SSAFunctions is the number of functions analysed (those that
// fully lowered to SSA). Functions that fell back to the legacy
// path are not counted — their memory traffic is not observable
// through this analysis.
SSAFunctions int
FuncsWithFrame int // functions with a detected stack frame
FuncsFrameKept int // ... whose frame did not escape (promotable)
// PerFunc keeps a one-line record per analysed function for the
// sidecar report. Keyed insertion order is preserved via the
// parallel funcOrder slice.
PerFunc map[string]FuncMemRecord
funcOrder []string
// OptInsBefore / OptInsAfter are the total SSA instruction (value)
// counts across all optimised functions, summed before and after
// the optimization pipeline. The difference quantifies how much
// folding the passes achieved.
OptInsBefore int
OptInsAfter int
// StructuredFuncs / GotoFuncs count how the SSA-emitted functions
// were rendered: structured (if/else + for) vs the goto/label
// fallback.
StructuredFuncs int
GotoFuncs int
}
// FuncMemRecord is the per-function memory summary.
type FuncMemRecord struct {
Name string `json:"fn"`
Total int `json:"total"`
Frame int `json:"frame"`
Rodata int `json:"rodata"`
Slab int `json:"slab"`
FrameSize int64 `json:"frame_size"`
FrameEscaped bool `json:"frame_escaped"`
EscapeReason string `json:"escape_reason,omitempty"`
}
// NewMemMetrics returns an empty accumulator.
func NewMemMetrics() *MemMetrics {
return &MemMetrics{PerFunc: map[string]FuncMemRecord{}}
}
// Add folds one function's classification into the running totals.
func (m *MemMetrics) Add(f *Func, mc *MemClass) {
m.SSAFunctions++
rec := FuncMemRecord{Name: f.Name}
for _, c := range mc.Class {
rec.Total++
switch c {
case AliasFrame:
rec.Frame++
case AliasRodata:
rec.Rodata++
default:
rec.Slab++
}
}
if mc.FramePtr != nil {
m.FuncsWithFrame++
rec.FrameSize = mc.FrameSize
rec.FrameEscaped = mc.FrameEscapes
rec.EscapeReason = mc.EscapeReason
if !mc.FrameEscapes {
m.FuncsFrameKept++
}
}
m.Total += rec.Total
m.Frame += rec.Frame
m.Rodata += rec.Rodata
m.Slab += rec.Slab
if _, seen := m.PerFunc[f.Name]; !seen {
m.funcOrder = append(m.funcOrder, f.Name)
}
m.PerFunc[f.Name] = rec
}
// AddOpt folds one function's pre/post optimization instruction counts
// into the running totals.
func (m *MemMetrics) AddOpt(before, after int) {
m.OptInsBefore += before
m.OptInsAfter += after
}
// OptReduction returns the fraction of SSA instructions the
// optimization pipeline eliminated, in [0,1].
func (m *MemMetrics) OptReduction() float64 {
if m.OptInsBefore == 0 {
return 0
}
return float64(m.OptInsBefore-m.OptInsAfter) / float64(m.OptInsBefore)
}
// Promoted is y in the y/x ratio: accesses proven function-local.
func (m *MemMetrics) Promoted() int { return m.Frame + m.Rodata }
// Rate returns the promotion ratio in [0,1]; 0 when there are no
// accesses at all.
func (m *MemMetrics) Rate() float64 {
if m.Total == 0 {
return 0
}
return float64(m.Promoted()) / float64(m.Total)
}
// Summary renders the human-readable stderr report.
func (m *MemMetrics) Summary() string {
pct := func(n int) string {
if m.Total == 0 {
return " 0.0%"
}
return fmt.Sprintf("%5.1f%%", 100*float64(n)/float64(m.Total))
}
s := "wasm2go: SSA optimization + memory promotion summary\n"
if m.OptInsBefore > 0 {
s += fmt.Sprintf(" SSA instructions: %8d -> %d (-%.1f%% folded)\n",
m.OptInsBefore, m.OptInsAfter, 100*m.OptReduction())
}
if tot := m.StructuredFuncs + m.GotoFuncs; tot > 0 {
s += fmt.Sprintf(" emit form: %8d structured / %d goto (%.1f%% structured)\n",
m.StructuredFuncs, m.GotoFuncs, 100*float64(m.StructuredFuncs)/float64(tot))
}
s += fmt.Sprintf(" SSA-lowered functions: %8d\n", m.SSAFunctions)
s += fmt.Sprintf(" functions with a frame: %8d (%d non-escaping)\n", m.FuncsWithFrame, m.FuncsFrameKept)
s += fmt.Sprintf(" total memory accesses: %8d\n", m.Total)
s += fmt.Sprintf(" promoted (y/x): %8d / %d (%s)\n", m.Promoted(), m.Total, pct(m.Promoted()))
s += fmt.Sprintf(" frame: %8d (%s)\n", m.Frame, pct(m.Frame))
s += fmt.Sprintf(" rodata: %8d (%s)\n", m.Rodata, pct(m.Rodata))
s += fmt.Sprintf(" slab (not promoted): %8d (%s)\n", m.Slab, pct(m.Slab))
return s
}
// FuncRecords returns the per-function records in analysis order.
func (m *MemMetrics) FuncRecords() []FuncMemRecord {
out := make([]FuncMemRecord, 0, len(m.funcOrder))
for _, name := range m.funcOrder {
out = append(out, m.PerFunc[name])
}
return out
}
// TopSlabFunctions returns up to n function records with the most
// un-promoted (slab) accesses — the highest-value targets for
// improving the promotion rate.
func (m *MemMetrics) TopSlabFunctions(n int) []FuncMemRecord {
recs := m.FuncRecords()
sort.SliceStable(recs, func(i, j int) bool { return recs[i].Slab > recs[j].Slab })
if len(recs) > n {
recs = recs[:n]
}
return recs
}
// JSON renders the full metrics — module totals plus every
// per-function record — as an indented JSON document for the
// `-promotion-report` sidecar.
func (m *MemMetrics) JSON() ([]byte, error) {
type doc struct {
SSAFunctions int `json:"ssa_functions"`
FuncsWithFrame int `json:"funcs_with_frame"`
FuncsFrameKept int `json:"funcs_frame_non_escaping"`
Total int `json:"total_accesses"`
Promoted int `json:"promoted"`
Frame int `json:"frame"`
Rodata int `json:"rodata"`
Slab int `json:"slab"`
Rate float64 `json:"promotion_rate"`
Functions []FuncMemRecord `json:"functions"`
}
d := doc{
SSAFunctions: m.SSAFunctions,
FuncsWithFrame: m.FuncsWithFrame,
FuncsFrameKept: m.FuncsFrameKept,
Total: m.Total,
Promoted: m.Promoted(),
Frame: m.Frame,
Rodata: m.Rodata,
Slab: m.Slab,
Rate: m.Rate(),
Functions: m.FuncRecords(),
}
return json.MarshalIndent(d, "", " ")
}