Milestone 7 — Production C backend (plan and acceptance)
Status: complete 2026-10-24 (started 2026-10-23). Normative: c-backend.md (§3 control flow, §7 debugging, §8 validation), intermediate-representation.md §5 (the NIR interpreter), testing.md (fuzzing). Each increment writes its spec first, then the code and tests.
Increments
| # | Content | Spec | Status |
|---|---|---|---|
| 7a | Source maps: #line directives to Nova source (--line-directives, on with -g), source paths that never contain host paths, the C compiler run where the paths resolve; tests that the debug line table names Nova lines, and that the whole run corpus builds and runs with directives | c-backend §7 | done 2026-10-23 |
| 7b | Structured C: if/else, loops, break/continue reconstructed from the NIR graph (dominator-based, for reducible graphs; goto only where no structured form exists, and the flat form as a fallback and test oracle). The run corpus passes in both forms | c-backend §3 | done 2026-10-23 |
| 7c | NIR reference interpreter (novac run --interp): executes checked NIR with a model of memory (objects, places, liveness, initialisation), the runtime's allocation accounting and fault injection, and the same output, panics and exit statuses as native code. It reports any use of a dead or uninitialised object as an interpreter fault (a dynamic safety oracle). The whole run corpus and the fault sweep agree with native output | IR §5 | done 2026-10-23 |
| 7d | Random program generator (Csmith-style, tests/fuzz): well-typed, terminating Nova programs over integers of every width, checked arithmetic, structs, enums, match, arrays, slices, references, boxes, vectors, optionals and results, printing a checksum. Oracle: the checker accepts every program, and the interpreter, clang and GCC at -O0/-O2 agree (output and exit status) | testing §5 | done 2026-10-23 |
| 7e | Campaign and portability: a 24-hour fuzz campaign (clang/GCC × O0/O2 × interpreter), with every finding fixed and kept as a corpus test; MSVC best-effort for the hosted profile (C11 mode) in CI | c-backend §1.1, §8 | done 2026-10-24 (MSVC 2026-10-23) |
Acceptance criteria (roadmap.md)
- A 24-hour fuzz campaign with no miscompile across clang/GCC × O0/O2.
- The interpreter agrees with native output on the whole run corpus (run-pass and run-fail), and with the fault-injection sweep's outcomes.
- Source maps: a debugger's line table maps generated code to Nova lines; generated C contains no host paths, with or without directives.
- Structured C: the run corpus passes with structured and flat C under every C configuration; reviewed snapshots show the structured form.
- MSVC best-effort: the hosted run corpus compiles with MSVC (C11) in CI, and runs where the runtime supports Windows; unsupported parts are listed.
- Docs:
c-backend.md,intermediate-representation.md,testing.md,roadmap.md,risks.md(R-1, R-3).
Results by increment
| Increment | Evidence (run 2026-10-23) |
|---|---|
| 7a | SourceMapTests: clang's line table (-g -S, .file/.loc) for a two-function program names demo/gcd.nova and every statement line (1–7, 9, 12–15), and no line outside the file or on the blank line; the main.c hand-back numbers are exact; directives are deterministic and contain no host path for all run-pass programs; SourceText.SourcePath cases. The run corpus gained a configuration clang -O0 -g line-directives (every run-pass and run-fail program builds warning-clean and agrees). Snapshot emit-c/line-directives.c reviewed. Manually: novac build -g run-pass/gcd.nova, then lldb -o "breakpoint set --file gcd.nova --line 5" stops in nv_gcd at gcd.nova:5, lists the Nova source, shows v_x_2 = 1071, and the backtrace names gcd.nova:13 for the caller. Default output (no directives) is byte-identical to before: the 17 existing C snapshots are unchanged, so the guest runs (c2.py, c3.py) were not repeated. Not done: columns (C's columns, not Nova's, appear in the line table; #line cannot carry columns) |
| 7b | Structured form is the default; --flat-c (and CEmitOptions.Structured = false) keeps the flat form. The run corpus (run-pass and run-fail) passes under every configuration in the structured form and in a clang -O2 flat oracle configuration; -Wimplicit-fallthrough joined the strict warning set. StructuredCTests: for every run-pass program, every NIR statement and exit appears exactly once in the tree and every remaining goto names a label in the tree; a nested-loop function needs no goto (3 breaks); a diamond becomes if/else; an irreducible graph (built by hand, since Nova source cannot produce one) falls back to flat. Over the 137 run programs, gotos inside functions drop from 1,188 (flat) to 28, in 15 programs: the rest are exits that C cannot express structurally (e.g. leaving a loop from inside a switch). The flat snapshot (emit-c/flat.c) equals the earlier output; the 14 structured snapshots were reviewed. Guest runs (structured C is now what runs in CosmoOS): c2.py (137/137 programs, 228/228 sweep runs) and c3.py (26/26 BusyBox cases, c3-io) pass in QEMU on x86-64 and on the CosmoOS VM on AArch64. The AArch64 runs were repeated on the final code; the x86-64 runs may have used the build from just before the elimination fixpoint, which changes one goto into a break in one corpus program |
| 7c | novac run --interp (src/Nova.Compiler/Interpreter/): byte-addressed memory with the target layout, runtime algorithms step for step, host C library for files and sockets. InterpreterTests: all 137 run-pass and run-fail programs give the corpus expectations (stdout, exit status, the exact panic line, empty stderr otherwise). The fault-injection sweep compares the interpreter with the native ASan build at the baseline and at every failure point (programs with ≤ 200 attempts; 30 spread points above that): all 103 sweep tests agree, so attempt counts, outcomes and the NOVA_ALLOC_REPORT line are equal. The collection property tests (24 programs) agree with their models in the interpreter. c3-io (files, loopback TCP, Process.run) and nwc on the 17 recorded BusyBox cases agree in the interpreter. The safety oracle catches three unsafe NIR mutations: a duplicated box drop ("double free"), a read after StorageDead ("use of a dead local") and, without its bounds check, a read past a vector's length ("use of uninitialised memory", where native C prints garbage). Found while building it: nothing in the compiler; the interpreter's own first bugs were a stable vector's length offset (256, after the chunk pointers) and descriptor-sized results. Speed: deep-structures (600,000 allocations) takes 2.3 s in a Release build. Not done: borrow tracking (Stacked Borrows); Windows I/O |
| 7d | tests/Nova.Fuzz (generator, oracle, campaign tool; testing.md §5). Programs of 200–750 lines that call every function and print 20–100 checksum lines; about 8% panic through a raw operation, so panic paths are compared too. The checker accepts 5,000 of 5,000 generated programs. FuzzTests runs 32 seeds through the whole oracle in the suite. Findings, all fixed, each with a corpus test: (1) C name collision: a Nova function add_i32 became nv_add_i32, the runtime's checked add; top-level functions are now nvF_<name> and the C namespaces are specified (c-backend §2.1; run-pass/runtime-names.nova). (2) Checker false positive: v.push(p.a) through a borrow mut parameter was N0272, because a write's carriers were taken from the operand's base local whatever its type; only values whose type may hold loans carry loans now (run-pass/copies-through-parameters.nova; every compile-fail test still reports its error). (3)–(6) Generated C that was not warning-free for valid Nova: x = x (-Wself-assign), a == a (-Wtautological-compare), a constant index past an array's end after its panicking check (-Warray-bounds), and in the structured form a label before statements that print nothing (-Wc23-extensions) (c-backend §5; run-pass/warning-free.nova, run-fail/constant-index-past-end.nova; the label case is covered by fuzz seed 1). (7) Found by FuzzTests in Linux CI (GCC is not on the development Mac): GCC's -Wtype-limits on comparisons that the type's own limits decide (u < 0, x <= MAX, and constant against constant); these are now constants (warning-free.nova). Campaigns on macOS (Apple clang; no GCC on this host): 2,000 programs (seeds 1–2000, 7 min 23 s, 8 jobs, raw-operation rate 3%) with 0 findings after the fixes, 34% of them panicking; with the rate lowered to 1%, 500 programs (seeds 5000–5499) with 0 findings and 9% panicking. On Linux (Debian 12 x86-64, GCC 12.2, clang 18.1.8, 12 jobs, after finding 7): 1,500 programs (seeds 1–1500) in 5 min 18 s through clang -O0/-O2/flat, GCC -O0/-O2 and the interpreter, 0 findings, 14% panicking |
| 7e (MSVC) | cl /std:c11 /W3 /WX with an 8 MiB stack (c-backend §1.1). MsvcTests: all 141 run-pass and run-fail programs give the corpus expectations; nwc on 3 recorded BusyBox cases (files, standard input, a missing file); Process.run of cmd /c exit 7, a missing program, and TCP reported as unsupported. 145/145 on Windows 11 ARM64 (Parallels VM, MSVC 19.51) and 145/145 on x64 in CI (test-windows, run 39944590259). Found on the way: MSVC's <stddef.h> has no max_align_t in C mode (now nv_max_align_t); Windows' 1 MiB default stack overflows deep-recursion.nova (hence /STACK:8388608); the test harness decoded child output with the console code page (now UTF-8). Not supported on Windows: TCP |
| 7e (campaign) | 24-hour campaign on the Debian x86-64 box (GCC 12.2, clang 18.1.8; clang -O0/-O2/flat, GCC -O0/-O2, interpreter), seeds from 1,000,000, 12 jobs at nice 19, on commit 973ea0e, started 2026-10-23 10:56 UTC. Commits after it change clang and GCC builds only by name (nv_clz64 is the same builtin, nv_max_align_t is max_align_t); the rest is under _WIN32. Interim (after 1 h 27 min, 25,600 programs): 3 findings, one kind: clang -Warray-bounds on a constant slice index too large for any object (seeds 1009815, 1014252, 1016777); fixed in the next commit (nv_opaque_index; run-fail/constant-index-past-end.nova), and the three seeds pass. The campaign kept running on 973ea0e, so it reported the same kind again. Result (ended 2026-10-24 10:57 UTC, 24 h): 423,773 programs (seeds 1,000,000–1,423,772), each through all six configurations; 53,536 (12.6%) panic and 303,160 end with another exit status, all agreeing. No miscompile: no program gave different output, exit status or panic in any configuration. 24 findings, all of the one kind above (clang rejected the generated C; no build produced a wrong answer). All 24 seeds pass on the fixed code (01aa4e0), each kept by run-fail/constant-index-past-end.nova. The campaign predates M5, so it has no clang -O2 placement configuration; that configuration is covered by the run corpus and 500 fuzz programs (milestone-5.md). Found afterwards in the campaign tool: the elapsed time printed hh (hours modulo 24), so the 24-hour summary read 00:00:01; it now prints total hours |
Acceptance results (2026-10-24)
| # | Criterion | Evidence | Result |
|---|---|---|---|
| 1 | 24-hour campaign, no miscompile across clang/GCC × O0/O2 | 7e campaign: 423,773 programs through clang -O0/-O2/flat, GCC 12.2 -O0/-O2 and the interpreter; no disagreement in output, exit status or panic. Its one finding kind (a warning that rejected valid generated C) was fixed and has a corpus test | met |
| 2 | Interpreter agrees with native output; fault sweep | 7c: InterpreterTests on all 137 run-pass and run-fail programs; all 103 sweep tests agree with the native ASan build, allocation report included. The interpreter is also a configuration in every fuzz run | met |
| 3 | Source maps; no host paths | 7a: SourceMapTests (clang's line table names exactly the Nova statement lines; no host path in any run-pass program, with or without directives); lldb breakpoints on Nova lines | met |
| 4 | Structured C on the whole run corpus, structured and flat | 7b: every run configuration in structured form plus clang -O2 flat; StructuredCTests; 14 reviewed structured snapshots and the flat snapshot | met |
| 5 | MSVC best-effort in CI | 7e (MSVC): MsvcTests 145/145 on x64 in CI (test-windows) and on Windows 11 ARM64; unsupported: TCP | met |
| 6 | Docs | c-backend.md §1.1, §2.1, §3, §5, §7; intermediate-representation.md §5; testing.md §5; this file; roadmap.md; risks.md R-1, R-3 | met |
Not done (recorded): Nova columns in debug info (#line carries lines only); borrow tracking in the interpreter (Stacked-Borrows-style); TCP and interpreter I/O on Windows; a campaign with the placement configuration (M5 came after the campaign started).