SSA Backend — Specification¶
A reference for the gc SSA backend: the value/block/op model, a representative pass list with one-line purposes, rewrite-rule syntax, GOSSAFUNC usage, and the generic→arch lowering concept. Pass names and rules track a specific Go release; the authoritative source is cmd/compile/internal/ssa/compile.go in your tree (examples here reflect Go 1.25).
1. The SSA model¶
| Type | File | Holds |
|---|---|---|
ssa.Func | func.go | one function: entry block, all blocks, *Config, name |
ssa.Block | block.go | basic block: Kind, Values, Controls, succ/pred edges |
ssa.Value | value.go | ID, Op, *types.Type, AuxInt, Aux, Args []*Value |
ssa.Op | op.go/opGen.go | the operation enum (generic + per-arch) |
Invariants:
- Single assignment: each
Value.IDis defined exactly once. - Phi:
OpPhivalues sit at a block's start; arg i corresponds to predecessor edge i. - Block kinds:
BlockPlain(one succ),BlockIf(two succs on a bool control),BlockRet,BlockExit, plus arch-specific branch kinds after lowering (BlockAMD64EQ, …). - Aux/AuxInt: encode immediates, symbols, types-of-interest for an op.
2. Representative pass list (purpose in one line)¶
From the passes slice in compile.go. Order matters; required passes always run.
| Pass | Purpose |
|---|---|
| number lines | attach line numbers for debug info |
| early phielim and copyelim | drop trivial phis/copies from construction |
| early deadcode | remove obviously dead generated code |
| decompose user | split structs/strings/slices/interfaces into scalar parts |
| opt | apply generic rewrite rules (fold, simplify, strength-reduce) |
| zero arg cse / generic cse | merge identical pure values |
| phiopt | simplify phis into selects/bool ops where possible |
| nilcheckelim | remove redundant nil checks |
| prove | derive integer ranges/relations → bounds-check elimination |
| expand calls | lower the abstract call op to ABI register/stack moves |
| late opt | second generic-rewrite round after expansion |
| sccp | sparse conditional constant propagation |
| branchelim | turn small if/else into branchless CondSelect |
| check bce | (debug) report remaining bounds checks |
| dse | dead-store elimination |
| memcombine | combine adjacent loads/stores into wider ops |
| writebarrier | expand GC write barriers |
| lower | rewrite generic ops → arch ops (machine boundary) |
| addressing modes | fold address arithmetic into arch addressing modes |
| late lower / pair | further arch peepholes; pair loads/stores |
| lowered cse | CSE again on lowered ops |
| critical | split critical edges (so phi moves have a home) |
| layout | order blocks |
| schedule | order values within each block |
| flagalloc | allocate the condition-flags register |
| regalloc | assign registers + spill slots |
| trim / stackalloc | remove empty blocks; lay out the stack frame |
3. Rewrite-rule syntax¶
Rules live in cmd/compile/internal/ssa/_gen/: generic.rules (machine-independent) and <ARCH>.rules (lowering). Form:
- Lowercase tokens (
x,c,d) bind subtrees, AuxInts ([c]), or Aux ({s}). <t>binds/asserts the value's type;_matches anything.&& goExpris a Go boolean guard evaluated on the bound names.=>is the standard rewrite;->(typed) historically required an explicit type. A(...)on the right may nest to build a tree.
Examples (verbatim from _gen/generic.rules):
(Add64 (Const64 [c]) (Const64 [d])) => (Const64 [c+d])
(Mul64 (Const64 [c]) (Const64 [d])) => (Const64 [c*d])
(IsInBounds x x) => (ConstBool [false])
(IsInBounds (ZeroExt8to64 _) (Const64 [c])) && (1<<8) <= c => (ConstBool [true])
(Sub64 x (Const64 <t> [c])) && x.Op != OpConst64 => (Add64 (Const64 <t> [-c]) x)
Op definitions (arg count, commutativity, type) are in genericOps.go / <ARCH>Ops.go, e.g. {name: "Add64", argLength: 2, commutative: true}.
4. rulegen¶
_gen/main.go + rulegen.go (the rulegen program) read every .rules file and emit the matcher functions rewriteValuegeneric.go, rewriteValue<ARCH>.go, rewriteBlock<ARCH>.go. The opt/lower passes call these generated functions in a fixpoint loop until no rule fires. Regenerate with go generate (or go run *.go) inside _gen/. Generated files are never hand-edited.
5. GOSSAFUNC¶
GOSSAFUNC=<func> go build . # dump all passes for <func> to ssa.html
GOSSAFUNC=<func>:<pass> go build . # dump only that one pass column
GOSSAFUNC='f1 f2' go build . # multiple funcs (see GOSSADIR)
GOSSADIR=dir GOSSAFUNC=... # write per-function files into dir
<func>is the linker symbol:Name,(*T).Method,pkg.Name.- Output
ssa.html: leftmost column = source; each subsequent column = SSA after one pass; click a value to highlight all uses; greyed = dead.
Related compile debug flags (-gcflags='-d=...'):
| Flag | Effect |
|---|---|
ssa/check_bce/debug=1 | print bounds checks NOT eliminated |
ssa/prove/debug=1 | trace the prove pass's deductions |
ssa/<pass>/off=1 | disable a pass (diagnosis only) |
-m / -m -m | report inlining and escape decisions |
-S | print final assembly |
6. Generic → arch lowering¶
Before lower, values use generic ops with no register/addressing concept. The lower pass applies <ARCH>.rules to map each generic op to one or more arch ops; the addressing modes pass then folds address math into the target's modes.
generic: v = Add64 a b
amd64 (post-lower): v = ADDQ a b
arm64 (post-lower): v = ADD a b
generic: Load (PtrIndex base idx) mem
amd64 (folded): MOVQloadidx8 base idx mem
Every generic op must have a lowering on every supported architecture; checkLower enforces that no generic op survives into codegen. After lowering, scheduling, flag/register allocation, and stack layout, the obj writer emits machine code.
Further reading¶
compile.go,func.go,block.go,value.go,op.go,README.mdin$(go env GOROOT)/src/cmd/compile/internal/ssa/_gen/README,_gen/generic.rules,_gen/<ARCH>.rules,_gen/main.gogo doc cmd/compile;go tool compile -d help(debug flag list)- Compiler overview:
$(go env GOROOT)/src/cmd/compile/README.md