diff --git a/README.md b/README.md index 08dca5e5..4d0786e2 100644 --- a/README.md +++ b/README.md @@ -236,25 +236,15 @@ For resetting architecture configurations, use the command `make distclean`. ## Intermediate Representation -To visualize the SSA control-flow graph, use the standalone `--dot` target. -It writes Graphviz DOT with one cluster per function and IR instruction nodes -grouped by basic block; no executable is generated. The graph is printed before -phi values are unwound into edge copies, so the phi nodes are still in it, and -functions the input cannot reach -- most of the embedded C library -- are -pruned first. The default output replaces the input suffix with `.dot`. +The middle end is SSA-based, and two options expose it. `--dot` draws the +control-flow graph as it stands after SSA construction and pruning; `--dump-ir` +prints the instructions as text, once before optimization and once after +register allocation. Both use `tests/fib.c` below as the running example, a +recursive Fibonacci function and its caller: -```shell -$ out/shecc --dot -o fib.dot tests/fib.c -$ dot -Tsvg fib.dot -o fib.svg -``` - -Graphviz is needed to render the result, but not to produce it, and nothing in -`make check` depends on it. - -Once the option `--dump-ir` is passed to `shecc`, the intermediate representation (IR) -will be generated. Take the file `tests/fib.c` for example. It consists of a recursive -Fibonacci sequence function. ```c +#include + int fib(int n) { if (n == 0) @@ -263,47 +253,169 @@ int fib(int n) return 1; return fib(n - 1) + fib(n - 2); } + +int main() +{ + printf("F(10) = %d\n", fib(10)); + return 0; +} ``` -Execute the following to generate IR: +### Control-flow graph + +`--dot` writes Graphviz DOT and stops; no executable is generated. Without +`-o`, the name is the input path with its final suffix replaced, or `.dot` +appended when it has none, so `tests/fib.c` would write `tests/fib.dot` into the +source tree. + +```shell +$ out/shecc --dot -o fib.dot tests/fib.c +$ dot -Tsvg fib.dot -o fib.svg +``` + +Graphviz renders the result but is not needed to produce it, and nothing in +`make check` depends on it. + +The output holds one cluster per function definition, one nested cluster per +basic block, +and one node per IR instruction; a block holding no instruction gets a single +placeholder node labeled `pseudo`. Edges inside a block are instruction order, +and the edges leaving a block's last node are control flow. The graph is emitted +before phi values are unwound into edge copies, so the phi nodes are still in +it, and functions the input cannot reach are pruned first. That pruning is what +keeps the picture readable: with the embedded C library prepended, `fib.c` gives +48 function bodies, of which 22 are reachable. The 20 that are neither `fib` nor +`main` are what `printf` drags in transitively, down to `malloc` and `abort`. + +The figure below is the `fib` and `main` clusters of that output, with the +run-specific pointer values dropped from the labels. Those two are the last +clusters emitted, so keeping everything from `fib` to the end of the file is +exactly the input's own code; that is what makes the crude recipe below work, +not a general way to slice one function out: + +```shell +$ { echo 'strict digraph CFG {'; echo 'node [shape=box]' + awk '/\(fib\)"$/ { keep = 1; print prev } keep; { prev = $0 }' fib.dot; } | + sed -E 's/"(BasicBlock )?0x[0-9a-f]+ \((.+)\)"/"\2"/' > fib-fn.dot +$ dot -Tpng -Granksep=0.3 -Nfontname=Helvetica fib-fn.dot -o docs/fib-cfg.png +``` + +![SSA control-flow graph of tests/fib.c](docs/fib-cfg.png) + +Reading `fib` from the top: + +* `.label.1986` and `.label.1987` carry no instructions. Every function gets a + dedicated entry block and a single exit block; the entry block is empty only + when the body opens with a control-flow construct, as `fib` does with its + `if`, while `main` starts straight into instructions. All three `RETURN`s edge + into the exit, so the graph stays single-entry and single-exit no matter how + many `return` statements the C code spells out. +* `.label.1988` is the `n == 0` test: materialize the constant, compare, then + `BRANCH`. The true edge leaves the bottom-left corner of a `BRANCH` node and + the false edge the bottom-right; where the two targets land is up to Graphviz, + so the edges cross whenever it places them the other way around. +* `.label.1991` is `return 0`. `.label.1992` is the else arm, an empty block + that falls into `.label.1995`, the `n == 1` test. Those empty blocks are + branch targets the parser reserved before it knew what the arm would contain. +* `.label.1998` is `return 1`, and `.label.1999` falls into `.label.2002`, the + recursive tail. +* Each call in `.label.2002` is a `PUSH` per argument, a `CALL`, and a + `RETURN VALUE` naming the result. Argument passing stays explicit in the IR, + so the register allocator, not the front end, decides which registers carry + them. + +Subscripts in the figure are SSA versions: `n₀` is the incoming parameter, +`.t1247₀` the first definition of the temporary `.t1247`. `fib` needs no phi, +because control flow never merges after a write here; every value is defined +once on the path that reaches its use. A phi appears where it does merge. In the +same graph, `strlen` carries `i₂ := PHI(i₁, i₃)` at its loop header, one argument +per incoming edge: `i₁` from the block ahead of the loop, `i₃` from the back edge. + +Temporary and label numbers shift whenever `lib/c.c` changes. Read them as +identity within one dump, not as stable names. + +### Instruction dump + ```shell $ out/shecc --dump-ir -o fib tests/fib.c ``` -Line-by-line explanation between C source and IR (variable and label numbering may differ): -```c -C Source IR Explanation --------------------+--------------------------------------+-------------------------------------------------------------------------------------- -int fib(int n) def int @fib(int %n) -{ { - if (n == 0) const %.t871, 0 Load constant 0 into a temporary variable ".t871" - %.t872 = eq %n, %.t871 Test if "n" is equal to ".t871", store result in ".t872" - br %.t872, .label.1430, .label.1431 If ".t872" is non-zero, branch to label ".label.1430", otherwise to ".label.1431" - .label.1430: - return 0; const %.t873, 0 Load constant 0 into a temporary variable ".t873" - ret %.t873 Return ".t873" - .label.1431: - else if (n == 1) const %.t874, 1 Load constant 1 into a temporary variable ".t874" - %.t875 = eq %n, %.t874 Test if "n" is equal to ".t874", store result in ".t875" - br %.t875, .label.1434, .label.1435 If ".t875" is true, branch to ".label.1434", otherwise to ".label.1435" - .label.1434: - return 1; const %.t876, 1 Load constant 1 into a temporary variable ".t876" - ret %.t876 Return ".t876" - .label.1435: - return fib(n - 1) const %.t877, 1 Load constant 1 into ".t877" - %.t878 = sub %n, %.t877 Subtract ".t877" from "n", store in ".t878" - push %.t878 Prepare argument ".t878" for function call - call @fib, 1 Call function "@fib" with 1 argument - + retval %.t879 Store the return value in ".t879" - fib(n - 2); const %.t880, 2 Load constant 2 into ".t880" - %.t881 = sub %n, %.t880 Subtract ".t880" from "n", store in ".t881" - push %.t881 Prepare argument ".t881" for function call - call @fib, 1 Call function "@fib" with 1 argument - retval %.t882 Store the return value in ".t882" - %.t883 = add %.t879, %.t882 Add ".t879" and ".t882", store in ".t883" - ret %.t883 Return ".t883" -} } +The first dump, delimited by `====` and +`====`, is the IR before optimization, walked in +dominator-tree order. Three things have already happened to it: small callees +are inlined into their callers, the phis are unwound into copies, and the +printer then elides those copies along with the SSA subscripts, so `--dot` is +the only view that shows either. Pruning has not run yet, so every function with +a body is listed, libc included. Line by line against the C source: + ``` +C source IR Notes +---------------------+---------------------------------------+--------------------------------- +int fib(int n) def int @fib(int %n) { %n parameter, %.tN temporary +{ + if (n == 0) const %.t1247, 0 + %.t1248 = eq %n, %.t1247 + br %.t1248, .label.1991, .label.1992 true target first, false second + return 0; .label.1991: + const %.t1249, 0 + ret %.t1249 + else if (n == 1) .label.1995: reached via empty .label.1992 + const %.t1250, 1 + %.t1251 = eq %n, %.t1250 + br %.t1251, .label.1998, .label.1999 + return 1; .label.1998: + const %.t1252, 1 + ret %.t1252 + return fib(n - 1) .label.2002: + const %.t1253, 1 + %.t1254 = sub %n, %.t1253 + push %.t1254 one push per argument + call @fib + retval %.t1255 names the callee's result + + fib(n - 2); const %.t1256, 2 + %.t1257 = sub %n, %.t1256 + push %.t1257 + call @fib + retval %.t1258 + %.t1259 = add %.t1255, %.t1258 + ret %.t1259 +} } +``` + +A block earns a label line only by holding instructions, and the first one that +qualifies is folded into the function header. That is why neither `.label.1992`, +an empty block the graph shows, nor `.label.1988`, which holds the first three +instructions, appears as a label here. The graph is the better view of control +flow; this dump is the better view of what each block computes. + +The second dump follows register allocation, the peephole pass, the +architecture-specific lowering, and the flattening of the CFG into a linear +list. It is the last IR before machine code: + +``` +fib: + store %x0, 16(sp) + li %x1, $0 + %x2 = eq %x0, %x1 + br %x2 + ... + li %x1, $2 + load %x2, 16(sp) + %x2 = sub %x2, %x1 + store %x0, 24(sp) + %x0 = %x2 + call @fib +``` + +The listing above is the default Arm configuration; the register numbers follow +whatever target `make config` selected. `%xN` are post-allocation register +slots rather than SSA values, and how many exist and which hardware register +each maps to is the backend's business. `16(sp)` is a stack slot: `n` is spilled +on entry because it stays live across both calls, and the first call's result is +spilled in turn while the second one runs. `br` prints only its condition, but +both targets are still on the instruction; the backend encodes the true one as an +offset and lets the false one fall through when it is the next block in the +list. ## C99 Compliance diff --git a/docs/fib-cfg.png b/docs/fib-cfg.png new file mode 100644 index 00000000..09c8ede3 Binary files /dev/null and b/docs/fib-cfg.png differ