Understanding SSA
gohawk’s analyzers read the SSA form of your functions, not their source text.
This page explains what that form is, why it helps, and how to read the output
of gohawk ssa.
What SSA is
Static single assignment form rewrites a function so that every value is defined exactly once. There are no variables that change over time: each assignment in the source becomes a fresh, uniquely named value, and every use refers to the one definition that produced it.
Three ideas follow from that rule, and the rest of the form is built from them.
- Values and instructions. A function becomes a list of instructions.
Most instructions produce a value, written
tN, that later instructions consume by name. A value’s identity is the instruction that produced it, so “the file returned byos.Open” is one specific value that every later use names directly. - Basic blocks. Instructions are grouped into blocks, each a straight
line of code with one entry and one exit.
if, loops,&&,||, andselectbecome edges between blocks; the last instruction of a block is a jump, a conditional branch, or a return. - Merges. When two paths assign different values to what was one source
variable, the block where they meet begins with a
phiinstruction that selects the value according to which predecessor ran. A loop variable is a phi of its initial value and its next value. A variable that is never reassigned across a branch needs no phi at all.
Source variables whose address is taken, which a closure captures, or which a
deferred call must observe are the exception: they become a local or new
allocation, a cell, with explicit stores into it and loads out of it. The
cell is a value; what it holds at any point is whatever the reaching store
put there.
Why gohawk uses it
gohawk asks questions like “is this file closed on every path that returns?” SSA makes those questions precise.
- A value is defined in one place. To ask whether two expressions mean the same file, gohawk compares values, not variable names. A value it cannot trace back is treated as a different value, not guessed at.
- Every path is visible. Blocks and the edges between them are the
control-flow graph, so “every return path” is a walk over that graph. An
if err != nilsplits into a path where the file was opened and one where it wasn’t. - Merges are visible. When a value can come from more than one place, a
phisays so. - Calls show their kind. A call records whether it goes to a known
function, a function value, or an interface method.
deferandgowrap the same call.
The cost is that SSA is not your source. defer file.Close() is not a
statement to find; it is a defer instruction on a specific value. This page
exists to make that translation easy.
Reading a dump
The block below is the real output of gohawk ssa -func CopyHeader on this
small function, regenerated with the documentation so it cannot drift from
what the tool prints:
func CopyHeader(path string, fallback string) (string, error) { file, err := os.Open(path) if err != nil { return "", err } defer file.Close() buffer := make([]byte, 64) count, err := file.Read(buffer) header := fallback if err == nil && count > 0 { header = string(buffer[:count]) } log := func() { _ = header } log() return header, nil}// tools/gendocs/ssaexample/example.go:13:6# Package: github.com/kojah/gohawk/tools/gendocs/ssaexample# Location: tools/gendocs/ssaexample/example.go:13:6# Recover: 3# Locals:# 0: t0 string# 1: t1 errorfunc CopyHeader(path string, fallback string) (string, error):0: entry P:0 S:2 t0 = local string () *string t1 = local error () *error t2 = os.Open(path) (*os.File, error) t3 = extract t2 #0 *os.File t4 = extract t2 #1 error t5 = t4 != nil:error bool if t5 goto 1 else 21: if.then P:1 S:0 idom:0 *t0 = "":string *t1 = t4 rundefers t6 = *t0 string t7 = *t1 error return t6, t72: if.done P:1 S:2 idom:0 defer (*os.File).Close(t3) t8 = new [64]byte (makeslice) *[64]byte t9 = slice t8[:64:int] []byte t10 = (*os.File).Read(t3, t9) (n int, err error) t11 = extract t10 #0 int t12 = extract t10 #1 error t13 = new string (header) *string *t13 = fallback t14 = t12 == nil:error bool if t14 goto 6 else 53: recover P:0 S:0 t15 = *t0 string t16 = *t1 error return t15, t164: if.then P:1 S:1 idom:6 t17 = slice t9[:t11] []byte t18 = convert string <- []byte (t17) string *t13 = t18 jump 55: if.done P:3 S:0 idom:2 t19 = make closure CopyHeader$1 [t13] func() t20 = t19() () t21 = *t13 string *t0 = t21 *t1 = nil:error rundefers t22 = *t0 string t23 = *t1 error return t22, t236: cond.true P:1 S:2 idom:2 t24 = t11 > 0:int bool if t24 goto 4 else 5
// tools/gendocs/ssaexample/example.go:25:9# Name: github.com/kojah/gohawk/tools/gendocs/ssaexample.CopyHeader$1# Package: github.com/kojah/gohawk/tools/gendocs/ssaexample# Location: tools/gendocs/ssaexample/example.go:25:9# Parent: CopyHeader# Free variables:# 0: header *stringfunc CopyHeader$1():0: entry P:0 S:0 t0 = *header string returnRead it top to bottom with the following in mind.
- The header.
# Recover: 3names the block that runs if a deferred call recovers from a panic.# Localslists the cells the function allocates. The named results became cellst0andt1because a deferred call could observe them. - Block labels. Each block shows its index, a name such as
if.thenthat records the source construct it came from, and how many blocks lead into and out of it. - The call and its results.
os.Openreturns two results.extract t2 #0andextract t2 #1split them into the file and the error. The file ist3from here on, and every later use namest3, including the deferred close. - The branch.
t5 = t4 != nil:errorfollowed byif t5 goto 1 else 2is the error check. The file is only open on the path through block 2. - The defer.
defer (*os.File).Close(t3)records the call when the defer runs;rundefersmarks where deferred calls execute, just before eachreturn. - The captured variable.
headerbecame a cell,t13 = new string (header), because the closure captures it. Both assignments are stores intot13, and the closureCopyHeader$1reads it with*header. - The condition.
err == nil && count > 0became two blocks, because the second half only runs when the first is true. - The conversion.
convert string <- []byte (t17)turns the byte slice into a string.
Common instructions
| Form | What it means |
|---|---|
Call |
a function or method call |
Extract |
one result of a call that returns several |
If, Jump, Return |
a conditional branch, an unconditional edge, and a return |
Phi |
a value that depends on which path reached this block |
Alloc, Store, UnOp |
a cell for a variable, a store into it, and a load out of it (*cell) |
Defer, RunDefers |
registering a deferred call, and running the registered ones before a return |
Go |
starting a goroutine |
MakeClosure |
creating a closure; it does not run it |
MakeChan, Send, Select |
making a channel, sending on it, and a select statement |
FieldAddr, IndexAddr |
the address of a struct field or of an element |
The full list of forms gohawk handles, and how the analyzers use each one, is in the SSA development notes.