Week 3 · Lesson 6
The week 3 lab
Add loops to your compiler: range and end in the type checker, loop-carried memory in the interpreter, and nested for loops in the renderer. You're done when every week 3 test passes, weeks 1 and 2 still pass, and you've written 10 new tests of your own.
What you're building
- Two new ops.
range nis a loop counter named by its statement, andend x1 ... xk rrepeats its effects for every value ofr. See spec/uir-week3.md. - Open ranges on every node, and the checks that use them. Asserts, calls and range bounds have none. An
endmust close a range its effects use. Each range is closed by exactly oneend, which is what makes the loop nesting well defined. Every range a store uses is closed before its output. A buffer's output version sits outside every loop. - Bounds through ranges. A range's interval is
[0, n-1], so your week 1 analysis provesi*K + kin range with no new code beyond that one case. - Placement and linearization. Each value at the depth of its deepest range, loads by the placement rule, each
endas a block. One structure that bothrunandrenderwalk. - Loop-carried memory in
run. Week 2's "a version is a value" interpreter can't express an accumulator. Executing the linearized blocks against mutable arrays can. - Nested
forloops inrender, with loop-invariant values hoisted.
Warm up
The smallest loop there is.
Your first loop
Slot 0 is x (4 f32), slot 1 is y (4 f32). Make the final contents of slot 1 equal 2 * x[i] for every i in 0..3, using one range. The starter does only i = 0, with no loop.
The command-line contract
Nothing new on the command line. run, simplify and render keep their contracts from weeks 1 and 2; what changes is which programs they accept and what render may print.
| command | week 3 changes |
|---|---|
your.exe run FILE | Programs may contain range and end. Loop rule violations (an end closing an unused range, a range closed twice, a range never closed, an assert or call depending on an open range) are static errors: exit 2 with an invalid: line. |
your.exe render FILE | For a program that contains a range, the C may use for loops. while, do, goto and recursion are still out. For a program with no range, the C must stay loopless. |
How render tests are graded
Exactly as in week 2, with one change to the loop check. For each # render test the grader:
- runs
your.exe render FILEand fails the test on a nonzero exit; - parses the program with the oracle and checks whether it contains a
range. It then strips comments and string literals from your C and fails the test if it findswhile,doorgoto, or, when the program has no range,for; - writes its own
main: one array per buffer filled with thedatavalues (f32 from exact bits), a call tokernelin slot order, then one line per element; - compiles both files with
clang -std=c11 -O2 -ffp-contract=off -fsanitize=undefined -fsanitize-trap=undefined ... -lm; - runs it and compares every element with the oracle: i32 and bool exactly, f32 bit for bit, any NaN matching any NaN.
Two new ways to fail. A loop bound or offset that overflows int32_t in your C is undefined behavior, so keep loop counters int32_t and do index arithmetic through the same uint32_t wrap as every other add and mul. And an accumulator loaded once outside its loop gives the wrong sum, not a crash: it shows up as a wrong value in the buffer.
Running the suite
From the course repo root, and keep the earlier weeks green while you work:
python tools/test.py --compiler "C:/path/to/your.exe" --week 3
python tools/test.py --compiler "C:/path/to/your.exe" --week 3 -k render -v # just the render tests, with details
python tools/test.py --compiler "C:/path/to/your.exe" --week 2
python tools/test.py --compiler "C:/path/to/your.exe" --week 1
Pass the built executable, not dune exec: the grader runs tests in parallel. Tests live in tests/week3/run/ and tests/week3/render/.
Write your 10 tests
Ten tests in <your compiler repo>/tests/week3/, each aimed at a rule someone will get wrong. Check them against the oracle:
python tools/test.py --check-tests <your compiler repo>/tests/week3
Good targets: an accumulator whose load has to run inside the loop, a reset store indexed by the outer ranges, and a range bound of 0 (the loop runs zero times). Invalid cases make good targets too, such as an end that closes a range its effect doesn't use, or a range closed by two ends. Also try one end with two effects writing two buffers, and a padded convolution whose raw index is invalid but whose masked index passes.
A build order that works
Parse and type the new ops
rangetakes one i32 operand and is named by its statement, so two ranges with the same bound are still different loops.endtakes one or more effects and a range last. Effects arevoid, and onlyendandaftermay consume them.Checkpoint: every file in
tests/week3/run/parses or is rejected with exit 2.Open ranges
One memoized pass: a range is open in itself,
endremoves its range, everything else takes the union of its sources. Then the static checks from the spec.Checkpoint: the loop-rule invalid tests exit 2.
Bounds through ranges
Add one case to your interval analysis: a range's interval is
[0, hi(n) - 1].Checkpoint: looped matmul and convolution offsets type check, and an unmasked padded index exits 2.
Loop order and placement
Find each range's outer loops from its
end. Give every node a path of loops, outermost first, and move accumulator loads to their uses. Keep this a separate pass: it's the part the syllabus says wants a rewrite engine.Checkpoint: print each node with its path and check the convolution by eye.
Linearize
Build nested blocks: each block lists its nodes and its loops in an order where sources come first and loads of a version come before the store that overwrites it.
Checkpoint: the "old version needed after it was overwritten" tests still exit 2.
Execute the blocks in
runMutable arrays per storage, a value table, and a loop that sets each range's counter and runs its body.
Checkpoint: every week 3 run test passes, and weeks 1 and 2 still do.
Render the blocks
The same walk, printing
forheaders, indented bodies and closing braces. Your week 2 expression printer doesn't change.Checkpoint: every render test passes, bit for bit.
Your 10 tests
Write them, check them with
--check-tests, and use them to break your compiler before the suite does.
Checklist
Your repo's AGENTS.md hasn't changed: an AI assistant may explain concepts, draw diagrams, and help you debug by asking questions, but must never write or rewrite compiler code. That includes your placement pass and your loop printer. "Why does my accumulator end up with only the last product?" is a fine question. "Write my linearizer" is not.
The oracle in _reference/ stays closed. Running python _reference/uirc.py render FILE on your own test to see where it puts each line is fine: it's asking the oracle a question.
Week 4's rangeify creates ranges and ends for you, from shapes. If placement and printing are tangled together now, you'll be untangling them under deadline next week. Keep three passes: open ranges, placement and linearization, printing.
Check yourself
Your renderer prints a for loop to zero a scratch alloc at the top of every kernel. Which render tests fail?
Look at step 2 of how render tests are graded.
for is allowed only for programs that contain a range. Zero an alloc with = {0} and the problem disappears.
What exit code should run give?
%b = buffer 4 dtype=i32 slot=0
%c = buffer 4 dtype=i32 slot=1
%i = range 4
%v = shrink %b %i shape=[]
%s = store %v 7
%e = end %s %i
%b1 = after %b %e
%w = shrink %c %i shape=[]
%s2 = store %w 8
%e2 = end %s2 %i
%c1 = after %c %e2
Count the ends that close %i.
2: %i is closed twice. A range is one loop and gets one end. To write both buffers in it, use end %s %s2 %i.
Your matmul render test fails with a wrong value: every output equals just the last product. What's the most likely cause?
Which value should change from one k iteration to the next?
If the load of the accumulator runs once before the loop, each iteration computes start + product and stores it, and the last store wins. The placement rule puts that load inside the loop. Overflow would trap, and the flags are the grader's, not yours.
Which of these may you do this week?
Running the oracle is asking it a question.
Running the oracle on your own tests is fine. Reading its source spoils the puzzle, and the AI policy rules out having your compiler code written for you.