Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 163 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdio.h>

int fib(int n)
{
if (n == 0)
Expand All @@ -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 `==<START OF INSN DUMP>==` and
`==<END OF INSN DUMP>==`, 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

Expand Down
Binary file added docs/fib-cfg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.