Style guide for course authors¶
Twenty-five chapters written by different authors must read as one course. This guide covers how to write. DEPTH_CONTRACT.md covers what must be there. Start every chapter from CHAPTER_TEMPLATE/. Companion references: NOTATION.md (house notation), REFERENCES.md (bibliography format), LABS.md (lab specs), REVIEW_CHECKLIST.md (what the reviewer will check).
The primary reading surface is the course website (./course serve, §13): write for it. Everything must also stay readable as raw Markdown on GitHub.
1. Voice¶
- Talk to the learner as "you". Use the active voice and the present tense. "The worklist holds every block whose IN set may change", not "It can be observed that…".
- Assume a strong programmer who is new to compilers. Define every term the first time you use it, and link back to the chapter that introduced it when you reuse it.
- Be precise before being friendly. Make exact claims ("Θ(n) passes on the ladder graph") rather than impressions ("can be slow").
- Don't hype. Avoid "simply", "obviously", "just", "magic", "powerful". If a step is easy, the reader will notice.
- Spelling:
- Use American spelling and one word for "dataflow".
- Write "reverse postorder (RPO)" at first use.
- Write "phi" in prose,
phiin LLVM IR, and φ in math. - Write "LLVM 23", not "latest LLVM".
- Show, then name. Open a technique with the problem and a tiny example. Formal definitions come after.
2. Files and naming¶
chapters/NN-slug/
├── README.md technique map (template: CHAPTER_TEMPLATE/README.md)
├── lessons/
│ ├── 01-first-family.md one file per technique family; NN = order within the chapter (01, 02, …)
│ └── 02-second-family.md kebab-case topic, e.g. 02-dominance-frontiers.md
├── exercises.md implementation tasks, written as specs (LABS.md) with collapsible hints
├── references.yaml the annotated bibliography (REFERENCES.md) — you edit this
├── references.md GENERATED by `./course refs build NN` — never edit by hand
├── flashcards.tsv front<TAB>back<TAB>tags
├── quiz.yaml GENERATED from solutions/quizzes/chNN.yaml — never edit by hand
└── figures/ only if Mermaid cannot express it (SVG, < 200 KB, source kept alongside)
- Lesson file names must match
NN-kebab-topic.md../course validateenforces this. - Link with relative paths:
[Lesson 2](lessons/02-dominance-frontiers.md)and[Ch 14](../14-dataflow/README.md). - Code paths are repo-relative in backticks:
pebble/lib/Analysis/Dominance.cpp.
3. Markdown conventions¶
- Headings:
- Use one
#title per file. In lessons it's# Lesson NN.k — <Family>, e.g.# Lesson 15.1 — Dominator algorithms. - The nine sections are
##with numbers (DEPTH_CONTRACT §2), and techniques are###. - Line breaks: don't hard-wrap at a fixed column. Put one paragraph on one line, or use a line break after each sentence (easier diffs). Both are fine, but pick one per file.
- Boxes (admonitions) carry every formal statement and every callout. The syntax is
!!! <type> "Title"followed by a blank line and the body indented by four spaces (see §5 for the theory boxes). Callout types: !!! note: a useful aside.!!! pitfall: a common mistake. Every lesson has at least one.!!! info "LLVM": how LLVM does it, with a source pointer.!!! invariant: the claim a proof relies on (when it isn't already a numbered lemma).!!! tip "Try it": a drill or command to run.??? <type> "Title"makes any box collapsible (use it for long real-world output).
The older blockquote callouts (> **Note** …) still render and are accepted in chapters that have not been upgraded yet; new text uses boxes.
- Tables hold anything with ≥ 3 parallel facts. Left-align text; right-align numbers when a column is purely numeric.
- Collapsible hints and spoilers (exercises only):
<details markdown="1"><summary>Hint 1 — where to start</summary>
Text of the hint (blank line after <summary> so Markdown renders inside).
</details>
4. Worked examples: trace tables¶
Every worked example shows every iteration (DEPTH_CONTRACT item 3). The course standard formats are below. The drill worked solutions (./course drill <name> --solution) print these same formats, so a lesson's tables and the drills look the same.
Fixed-point over nodes or symbols: rows are the items, columns are passes. Include the initial state and the final "no change" pass.
| node | init | pass 1 | pass 2 |
|---|---|---|---|
| A | {A} | {A} | {A} |
| B | ⊤ | {A,B} | {A,B} |
| C | ⊤ | {A,B,C} | {A,B,C} |
Say what changed in each pass as a bullet list under the table: "pass 1: Dom(B) shrank from ⊤ to {A,B}…".
Worklist or stack algorithms: one row per step, and every data structure gets a column.
| step | action | worklist (front→back) | changed | IN/OUT or stack |
|---|---|---|---|---|
| 1 | pop B | C D | OUT(B) = | … |
Rules:
- Sets are written {a, b}, sorted: nodes in RPO or alphabetical, terminals alphabetical, $ and ε last. Write {} for the empty set in tables; ∅ is fine in math. Compact {A,B,C} without spaces is fine inside wide tables.
- Highlight changes with bold.
- When a drill exists, generate the example with the drill (fixed seed) or check it through the oracle functions in tools/course/lib/. Never hand-compute a table that the course can compute.
5. Mathematics¶
Theory is written in LaTeX inside Markdown, rigorously, in the house notation of NOTATION.md. The website renders it with KaTeX; GitHub renders it with MathJax.
- Inline math:
$\mathrm{idom}(n)$. Display math:$$ … $$on lines of its own, with a blank line before and after. Multi-line derivations use\begin{aligned} … \end{aligned}inside$$:
$$
\begin{aligned}
\mathrm{Dom}(r) &= \{r\} \\
\mathrm{Dom}(n) &= \{n\} \cup \bigcap_{p \in \mathrm{preds}(n)} \mathrm{Dom}(p) \qquad (n \neq r)
\end{aligned}
$$
- Use math for every formula, set, relation and bound —
$O(e \log n)$, not "O(e log n)". Keep prose words out of math, or wrap them in\text{…}. - Only standard LaTeX, no
\newcommandor custom macros (they break one of the two renderers). Multi-letter names are upright:\mathrm{FIRST}(\alpha). - Literal dollar signs outside math must be code (
`$`) or escaped (\$); a bare$starts a formula on the website. In math, the end marker is\$. - In tables, a
|inside math breaks the row: use\mid,\lvert … \rvert,\Vert. - Math inside a box is indented with the box body like any other text.
Numbered statements¶
Formal content goes in boxes numbered N.k.m (chapter, lesson, one shared counter per lesson): definitions, theorems/propositions/lemmas/corollaries, algorithms. Every result is followed by a proof box. ./course validate checks the numbering and that each result has a proof.
!!! definition "Definition 15.1.1 (Dominance)"
Let $G = (N, E, r)$ be a flowgraph. A node $d$ **dominates** $n$, written
$d \mathrel{\mathrm{dom}} n$, if every path $r \leadsto n$ contains $d$.
$\mathrm{Dom}(n) \triangleq \{\, d \in N \mid d \mathrel{\mathrm{dom}} n \,\}$.
!!! theorem "Theorem 15.1.2 (Dominator tree)"
For every reachable $n \neq r$, the strict dominators of $n$ are totally ordered by
$\mathrel{\mathrm{dom}}$; hence $\mathrm{idom}(n)$ is unique and the edges
$\mathrm{idom}(n) \to n$ form a tree rooted at $r$.
!!! proof
Let $a, b$ strictly dominate $n$ and take any path $P : r \leadsto n$. …
Hence $a \mathrel{\mathrm{dom}} b$ or $b \mathrel{\mathrm{dom}} a$.
!!! proof "Proof sketch (full proof: [LT79, §2])"
… (a sketch must name where the complete proof is)
!!! algorithm "Algorithm 15.1.3 (Cooper–Harvey–Kennedy)"
- **Input:** a flowgraph $G = (N, E, r)$, every node reachable.
- **Output:** $\mathrm{idom}(n)$ for every $n \in N$ (with $\mathrm{idom}(r) = r$).
- **Precondition:** nodes numbered in postorder of one DFS from $r$.
- **Postcondition:** `doms[n]` $= \mathrm{idom}(n)$ for all $n$.
- **Invariant:** after every pass, for every processed $n$, the path `doms`$^{*}(n)$ is a
superset of $\mathrm{Dom}(n)$ (Lemma 15.1.4).
```text
function ComputeIdoms(G = (N, E, r)):
...complete pseudo-code, §6...
```
!!! example "Dominance on the running example"
…a small instance of the definition or theorem…
Proofs are checked line by line by the reviewer: each step must follow from a definition, an earlier numbered result, or a cited fact. Name the proof technique ("by induction on the length of the path", "by contradiction", "exchange argument"). A lesson that genuinely has no theory (a survey of tools) marks itself <!-- theory: none — <reason> -->.
5a. Real-world examples¶
Theory comes with evidence. Every technique gets at least one !!! real-world box (DEPTH_CONTRACT §2a R6), under that technique's ### heading, showing the concept in an actual system: real clang -S -emit-llvm output, opt -passes='print<domtree>' or print<loops>, -debug-pass-manager, Bison/ANTLR conflict reports, rustc -Z unpretty=mir, swiftc -emit-sil, GCC -fdump-tree-* dumps, V8/Cranelift flags. Format:
!!! real-world "LLVM's dominator tree for a loop nest"
**Reproduce** (clang 23.1.2, opt 23.1.2; any OS):
```sh
cat > nest.c <<'EOF'
int f(int n) { int s = 0; for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) s += j; return s; }
EOF
clang -O0 -Xclang -disable-O0-optnone -fno-discard-value-names -S -emit-llvm nest.c -o nest.ll
opt -passes='sroa,print<domtree>' -disable-output nest.ll
```
**Output** (complete):
```text
DominatorTree for function: f
=============================--------------------------------
Inorder Dominator Tree:
[1] %entry {0,9} [0]
[2] %for.cond {1,9} [1]
[3] %for.body {2,8} [2]
[4] %for.cond1 {3,8} [3]
[5] %for.body3 {4,6} [4]
[6] %for.inc {5,6} [5]
[5] %for.end {6,8} [4]
[6] %for.inc4 {7,8} [5]
[3] %for.end6 {8,9} [2]
Roots: %entry
```
**What to notice:** each `[d]` is the depth in the tree and `{in,out}` the DFS interval used for
O(1) dominance queries; the inner header `%for.cond1` is dominated by the outer body, and
neither latch (`%for.inc`, `%for.inc4`) dominates anything: Theorem 15.1.2 on a real CFG.
- Exact commands in an
shblock, runnable as pasted, deterministic (-fno-discard-value-names, fixed flags, no timestamps). Create input files inline with a heredoc. - Tool versions in the Reproduce line; LLVM is always the pinned 23.1.2 (STYLE §8). Other tools: the version you actually ran (
bison 3.8.2,antlr4 4.13.2,rustc 1.9x.y,swiftc 6.x). - Captured output in a
textblock, pasted from a real run. Mark any abridgment ("abridged: …",…lines). Never hand-write output. - What to notice connects the output back to the numbered definitions/theorems.
- Use
??? real-world(collapsed) when the output is longer than ~30 lines. - The reviewer re-runs every box (REVIEW_CHECKLIST.md step 5).
6. Pseudo-code¶
Put pseudo-code in a ```text fenced block with these conventions:
function ComputeIdoms(G = (N, E, entry)):
order ← ReversePostorder(G) # comments start with #
doms[entry] ← entry
changed ← true
while changed:
changed ← false
for b in order, b ≠ entry:
new ← first processed predecessor of b
for p in preds(b), p ≠ new:
if doms[p] is defined:
new ← Intersect(p, new)
if doms[b] ≠ new:
doms[b] ← new
changed ← true
return doms
- Use
←for assignment and=/≠for comparison. Keywords are lowercase (function,for,while,if,return), and indentation means nesting. - Name every helper and define it in the same lesson, or link where it's defined.
- Line-number long algorithms (
1:…n:) when the correctness proof refers to lines.
7. Citations¶
- The chapter's bibliography lives in
references.yaml(format, kinds, minimums and annotations: REFERENCES.md);./course refs build NNgeneratesreferences.md. - Cite inline with the key in brackets:
[CHK01],[Geo05, GTW06],[LT79, §3],[EaC3 §9.2]../course validatechecks that every citation resolves; the website links each one to its entry. - Keys: author initials or first surname + two-digit year for papers (
CHK01,Tar74), a mnemonic for books (EaC3) andPROJECT-Topicfor source/docs entries (LLVM-GDTC). - Prefer the original paper, then a DOI, then the authors' PDF. Books always name the chapter/section/pages to read.
- Never cite a blog post as the origin of a technique. Blog posts are fine as extra reading, marked as such (
kind: blog). - Lessons don't keep their own reference lists; they may end with
## References→See the [chapter references](../references.md).
8. Pointing into real compilers¶
The pinned versions are:
- LLVM 23.1.2 (tag llvmorg-23.1.2)
- GCC 15
- rustc stable 1.9x at the time of writing. Say which.
- Swift 6.x
- V8 and Cranelift at a named release
Write each pointer as repository path + symbol, relative to the repository root:
LLVM
llvm/include/llvm/Support/GenericDomTreeConstruction.h—SemiNCAInfo::runSemiNCAbuilds the tree;llvm/lib/IR/Dominators.cppinstantiates it for IR (LLVM 23.1.2).
- Don't use line numbers, because they rot. When linking, link the tag rather than
main:https://github.com/llvm/llvm-project/blob/llvmorg-23.1.2/<path>. - Other repositories:
- GCC
gcc/tree-ssa-*.cc(gcc-15 branch) - rustc
compiler/rustc_*/src/... - Swift
lib/SILOptimizer/... - V8
src/compiler/... - Cranelift
cranelift/codegen/src/... - tree-sitter
lib/src/... - Every chapter has ≥ 3 "Find where LLVM does X" tasks. Each gives the task, the starting file, and a question about what the reader finds. That question appears in the quiz with type
text,singleorset.
9. Diagrams (Mermaid)¶
Use Mermaid for every graph. GitHub and the course website both render it, and it stays diffable.
flowchart TD
A([A]) --> B[B]
B --> C[C]
C --> B
B --> D[D]
classDef hl fill:#fde68a,stroke:#b45309;
class B hl;
- CFGs use
flowchart TD. The entry node is a stadium([A]), other blocks are[B], and edges are drawn in the successor order used by the text. That order matters for DFS and RPO. - Node names in diagrams must match the text and the trace tables exactly. Generate the diagram from the same data where possible:
CFG.to_mermaid()or./course drill <name> --seed N --mermaid. - Highlight with one
classDef hl(as above). Name back edges or special edges in the text too, because colour alone isn't accessible. - Grammars, lattices and trees use
flowchart TDorgraph BTfor lattices (⊥ at the bottom). Pipelines useflowchart LR. - Keep each diagram to ≤ 15 nodes. Split larger ones.
10. Code¶
Course C++ (skeletons, solutions, labs, lesson excerpts)¶
- C++23. Use
std::expected<T, pebble::Error>orllvm::Expected<T>for errors and never throw. Also usestd::print/std::format,std::span,std::string_viewand ranges where they make code clearer. No C++20 modules. RTTI matches LLVM's setting. - Style: follow the LLVM Coding Standards and the repository's
.clang-formatwhen present. UpperCamelCasetypes and variables,lowerCamelCasefunctions.- Early returns.
autoonly when the type is obvious from the right-hand side.- LLVM idioms in LLVM-facing code:
isa<>/cast<>/dyn_cast<>, notdynamic_castSmallVector,DenseMap,SetVector,StringRef/ArrayRefat API boundariesraw_ostreamfor IR printing- the new pass manager with
PassInfoMixin PreservedAnalysesreturned precisely- no legacy pass manager
- Pure algorithm code (for example a grammar toolkit) uses the standard library. Say why when you pick one over the other.
- Excerpts in lessons must compile as shown, or be labelled
// excerptwith the file they come from. Keep them under 40 lines. Longer code belongs in the repository, with a link. - Skeleton TODOs:
PEBBLE_TODO("chNN", "what to implement, in one line");. The message names the exercise number fromexercises.md.
Other code¶
- Shell commands go in
shblocks without a$prompt, so they copy-paste cleanly. Show expected output in a separatetextblock. - LLVM IR uses
llvmfences. It is opaque-pointer IR only, withptrand neveri32*. The IR must passopt -passes=verify. - Pebble examples use
pebblefences and followdocs/language/pebble-spec.md.
11. Spoilers and solutions¶
- Lessons teach the algorithms in full, pseudo-code included. They never contain the course's solution code for an exercise.
exercises.mdgives graded hints in<details>blocks: where to start → the key idea → a near-complete sketch. Never give the finished code.- Quiz answers live only in
solutions/quizzes/chNN.yaml. The builtquiz.yamlis hashed. Don't repeat a quiz answer verbatim in the lesson right next to the question's topic, and write the quiz so it tests understanding rather than recall of one sentence. solutions/is a spoiler zone. Lessons may say "if you are stuck, compare withsolutions/..." only at the end of an exercise.
12. Before you submit a chapter¶
./course refs build NN # references.yaml -> references.md
./course validate --strict # structure, rigor boxes, citations, labs, drills self-test, quizzes, flashcards
./course quiz build NN && ./course quiz verify --all
./course test NN # skeleton: must compile, tests fail with TODO(chNN)
./course test NN --solution # solutions: all tests pass
./course site build # the website builds in strict mode
./course serve # then read every page of the chapter in the browser
uv run python -m unittest discover tools/course/tests
Then fill in the rubric table from DEPTH_CONTRACT §5 and hand the chapter to the reviewer (REVIEW_CHECKLIST.md).
13. The course website¶
The course is read as a local website: ./course serve (http://127.0.0.1:8000, reloads on every save) or ./course site build (static files in build/site/, strict). It is MkDocs + Material, built in place from the repository's Markdown by tools/site/hooks.py and tools/course/site.py:
- Pages:
README.md(home),chapters/**,docs/**(except the chapter template),labs/**.md. Navigation comes fromtools/course/chapters.yaml; planned chapters get a "coming soon" page with their syllabus entry. - Generated per chapter: Flashcards (from
flashcards.tsv), Quiz (questions only — answers are never rendered), References (fromreferences.yaml, with "cited in" links). Generated globally: the Bibliography. - Links: write ordinary relative links to
.mdfiles (they work on GitHub and on the site). Links to anything that isn't a page (C++ sources, YAML, directories) are rewritten to GitHub automatically. Heading anchors are GitHub-compatible. - Rendering:
$…$/$$…$$math (KaTeX),mermaidfences, the box types of §3 and §5, syntax highlighting forcpp,llvm,sh,text,ebnf/grammar, andpebble/pir(Rust-like highlighting). - The strict build fails on broken links between pages. It does not catch math that failed to render or a swallowed
$: look at the page (REVIEW_CHECKLIST step 9).