Skip to content

Milestone 11 — Language gaps (plan and report) ​

Status: complete (2026-10-25 to 2026-10-26). G1–G6, G7a–G7d (floats, ADR 0011) and G8a–G8c (closures, ADR 0012) are done; the acceptance is the last section. The owner chose these gaps before M10 (tooling), which is next.

Why ​

M9's suite and the README's tour were written in Nova next to the same programs in C, Rust and the others. Each place where Nova needed a workaround, or could not say something at all, is recorded in milestone-9.md (findings F1–F8) and in the README ("What Nova does not have yet"). This milestone takes them in order of use and cost. Each gap was first reproduced against the compiler of M9 (2026-10-25); the probe is quoted.

Plan ​

#GapProbe on M9's compilerSpecStatus
G1Nested patterns: Ok(E.A(x)), Some(3), Err(IoError.NotFound)expected ')', found '.' (N0100)type-system §15.2, §18.4done
G2for over an array, slice or vector of a non-Copy element: the variable is a referencecannot move a 'S' out of the value 'shapes' refers to (N0251)§16.4done
G3Borrow-checker precision: a reference local reassigned while a loan of its referent is live (a cursor walking a list), and a temporary reassigned in a loop while its earlier value is still borrowed (F1)cannot assign to 'node' while 'node.next' is borrowed (N0240); cannot assign to a temporary value while a temporary value is borrowed (N0240)ownership.md B-rules, lifetimes.mddone
G4Small conveniences: byte and character literals ('k' as u8), wrapping*, checked* and saturating* arithmetic (promised by OD-6), number formatting into a String'char' values are not supported in M1 (N0216); type 'u8' has no methods (N0234); 'String' has no method 'appendNumber'§4, §6, §23.2done
G5if and match as expressionsexpected expression, found 'if' (N0102)§5, §9, §15.3done
G6A reference to an optional as a type (F2: borrow T? is an optional reference)— (no syntax)§13.5, §22.1, grammar.md §2done
G7Floating-point types (OD-23)unknown type 'f64' (N0204)a design and an ADR firstADR 0011 accepted; G7a–G7d done
G8Capturing closures (OD-9)expected ')', found ':' (N0100)a design and an ADR firstADR 0012 accepted; G8a, G8b, G8b′, G8c done

Not a gap any more: F3 (Some(b) with b: borrow Box<T> as a borrow T?) is accepted and runs (probe, 2026-10-25). The contents helper in benchmarks/suite/linked-list/main.nova is no longer needed.

G7 and G8 change the language beyond its v0 design: each got a design and an ADR for the owner before code (both accepted 2026-10-25). Their increments are G7a–G7c then the first math functions (ADR 0011 §5 Q5, §6), and G8a–G8c (ADR 0012 §6).

Results ​

Run results are in testing.md's run log; this section says what each increment changed.

G1: nested patterns (1b949a7) ​

  • A variant pattern's payload holds sub-patterns: _, a binding, an integer or bool literal, or another variant pattern, to any depth (type-system §15.2, §18.4). The HIR keeps the pattern tree (HirPattern).
  • Exhaustiveness and unreachable arms use Maranget's usefulness algorithm (Semantics/PatternUsefulness.cs), and a missing case is reported with a witness, e.g. Ok(Shape.Square(_)).
  • Lowering tests and binds through the tree (NirBuilder.LowerNestedMatch); flat matches lower as before.
  • Restriction: a sub-pattern below a reference payload is N0205 for now.
  • The fuzzer generates nested Err patterns and literal sub-patterns.

G2: for over non-Copy elements (8e2c65b) ​

  • Over a sequence of a non-Copy element, the loop variable is a borrow T to the element in place, the rule of a match on a reference; the sequence cannot change while it lives. Copy elements are still copied (§16.4).
  • Fixed on the way: a shared slice of pointers is declared T *const * in C, not const T **.

G3: borrow-checker precision (b5b359f) ​

  • A Write or Dead of a reference local does not conflict with a loan whose place goes through it, such as (*r).next: overwriting a reference changes what it points to, not the object (lifetimes.md, "Overwriting a reference is not a use of its referent"). This accepts a cursor walking a list and a temporary reassigned in each loop iteration (F1). A box is not a reference: a loan through *b still conflicts when b is overwritten.

G4a: character literals (4704317) ​

  • 'k', '\n' and '\u{1F600}' are integer literals of their Unicode scalar value: u32 by default, any integer type that holds the value, ASCII only in 8-bit types (N0206), and usable as patterns (§4).

G4b: explicit overflow behaviour ​

  • wrappingAdd/Sub/Mul, checkedAdd/Sub/Mul (returning T?) and saturatingAdd/Sub/Mul on every integer type; none of them panics (§6). The names are reserved on integer types: an interface method of that name cannot be implemented for one (N0228).
  • C: rt.h helpers nv_wrapping_*, nv_try_* (for the checked methods) and nv_saturating_*, each in both of rt.h's variants: the compiler builtins and the portable fallback MSVC uses. Signed wrapping goes through the unsigned twin and nv_as_iN, so it has no implementation-defined conversion. The checked operators' helpers did not change.
  • The interpreter computes the exact result in Int128 and then wraps, tests or clamps it.
  • Tests: run-pass/overflow-methods.nova (every width, at and past both bounds, compiled and interpreted) and check-fail/overflow-methods.nova. The fuzzer now generates wrapping* and saturating* calls.

G4c: numbers into a String ​

  • s.appendNumber(n) appends any integer in decimal (§23.2): Result<void, AllocError>, two-phase and unchanged on failure, like append. A non-integer argument is N0207.
  • C: rt.h's nv_format_i64/nv_format_u64 write the digits into a 20-byte compound literal, which lives to the end of the enclosing block, and the string's existing nvVA_ helper appends them. INT64_MIN is negated in unsigned arithmetic. The interpreter shares one append routine (AppendText) between append and appendNumber.
  • Tests: run-pass/append-number.nova (every type at its bounds, a reference, a literal, a loop with ?; also run by the fault-injection sweep) and check-fail/append-number.nova.
  • The suite's parsing and string-building workloads keep their hand-written digit loops: they mirror the other languages' versions, and M9's figures were measured on them.

G5: if and match as expressions ​

  • if (c) a else b and match (e) { p => v, ... } in expression position (type-system §15.3, grammar.md). A branch is an expression, or a block that cannot complete ({ return; }, { continue; }); a block that can complete has no value (N0235, new). An if or match that begins a statement is still the statement form, and an if expression needs its else.
  • Design choices (for the owner to revisit): branches are expressions, as in Kotlin, rather than blocks that end in a value, as in Rust. This keeps C#'s and TypeScript's statement blocks unchanged and needs no new rule for "the last expression of a block"; a branch that needs statements is a statement if assigning a var. ?: is not available, since ? is already postfix (Result propagation). Without an expected type, the first branch that is neither flexible nor null gives the type, so x + if (c) 1 else 2 with x: u8 is u8 arithmetic.
  • HIR: an HirStmtValue holds the statement form, whose value branches end in an HirYield; NIR lowering assigns each yield to one temporary of the enclosing statement, so moves, drop flags, loans and temporaries follow from the existing NIR analyses. A branch's temporaries end with the branch (a reference to one is N0243). The missing-return analysis looks into branches, so a break in a branch counts as an exit of its loop.
  • Tests: run-pass/branch-expressions.nova (strings, references with two sources, a consuming match, nested patterns, generics, drops of the branch taken only, continue and break from a branch, evaluation order; compiled and interpreted), parse-pass and parse-fail/branch-expressions.nova, check-fail/branch-expressions.nova and check-fail/branch-expression-ownership.nova. The fuzzer generates if and match expressions.

G6: references to optionals (F2) ​

  • A type may be parenthesised (grammar.md §2), so a reference to an optional is borrow (T?) or borrow mut (Box<Node>?); borrow T? is still an optional reference, and (borrow T)? spells it out (type-system §22.1). After borrow, a ( begins a source list only when it holds names and a type follows the ) (§13.5): borrow(a) (T?) names a source.
  • Diagnostics print the two as written, borrow i32? and borrow (i32?); before, both printed as borrow i32?.
  • Nothing else changed: the type existed (a borrow of an optional place had it) and behaves as any reference, so a list can be appended at its tail through a borrow mut (Box<Node>?) slot. Parenthesised types are not part of the generic-argument lookahead in expressions: g(a < (b), c > (d)) stays two comparisons.
  • Fixed on the way: an assignment whose target fails (N0211) no longer adds N0260 for a null value.
  • Tests: run-pass/optional-references.nova (compiled and interpreted), check-fail/optional-references.nova, parse-pass/parenthesised-types.nova (new .ast snapshot, reviewed).

G7a: floating-point types (ADR 0011) ​

  • f32 and f64 (type-system §35):

    • literals rounded once in their own type, and flexible: an unsuffixed literal takes an expected f32;
    • + - * / and comparisons by IEEE, never panicking;
    • lossless as, round<T>, and truncate<T> with its checked and saturating forms;
    • bits() and fromBits;
    • constants folded with the run-time rounding.

    No new diagnostic codes.

  • C: literals by their bits (nv_f64_from_bits), plain operators, and rt.h helpers.

    • The helpers keep conversions free of C's undefined behaviour. nv_ftoi_* compares with exact exclusive bounds first. nv_f64_to_f32 rounds finite values beyond FLT_MAX itself: to FLT_MAX below the midpoint 2^128 − 2^103, to infinity from it.
    • rt.h checks the IEEE formats and FLT_EVAL_METHOD == 0, and turns contraction off by pragma (clang, MSVC). novac passes -ffp-contract=off (Toolchain.SemanticFlags).
    • A new panic kind, NV_PANIC_FLOAT_RANGE ("float out of range").
  • Interpreter and constant folding share FloatSemantics:

    • each operation in .NET's float or double;
    • integer-to-float conversion rounded in one step from the exact integer, so a u64 to f32 is never rounded twice through a double.
  • Two emitter shortcuts excluded floats: folding x == x to true (false for NaN), and comparing two constants as integers.

  • Tests:

    • run-pass/floats.nova: literals, arithmetic, infinities, NaN, signed zeros, every conversion at its boundaries, bits, constants, structs, generics and branch expressions. The output is identical compiled and interpreted, and every value was checked against Python's IEEE arithmetic.
    • run-fail/float-truncate-nan.nova and float-truncate-range.nova;
    • check-fail/floats.nova;
    • emit-c/floats.nova (snapshot);
    • rt.h's helpers in a C program under -Wconversion -Werror, with UBSan's float-cast-overflow check.
  • Fix (d112351). C4 on x86-64 showed that rt.h's FLT_EVAL_METHOD check broke every kernel object: CosmoOS's -mgeneral-regs-only leaves no SSE there. The float parts of rt.h now apply only under NV_FLOATS, which a program with floats defines. FreestandingTests builds its cross objects with that flag too.

G7b: printing floats ​

  • Console.write, writeLine and String.appendNumber print f32 and f64 (type-system §35.7). The text is the shortest decimal that reads back as the value, the nearest on a tie of length, laid out as ECMAScript's Number::toString (1e+21, 1.5e-7, 0.000001).
  • C: Schubfach (runtime/hosted/nova_rt_float.c).
    • It uses integers only, and a 617-entry table of 126-bit powers of ten, generated exactly by a Python script. The script also checked the three floor-log approximations over every input they get.
    • Unlike the paper's Java code, it allows one-digit results (5e-324).
    • The file is linked only into programs that print a float.
  • Interpreter: an exact search over rationals (FloatSemantics.ShortestDigits).
    • Finding: .NET 10's own shortest formatting is wrong at some powers of two. 2^-25 prints as 2.980232238769531E-08, which reads back as a different double.
    • So .NET serves as neither formatter nor oracle. Python's repr and Schubfach agree with each other, and with the search.
  • ADR 0011 gained a §5a with these deviations, and the exponent sign +.
  • Tests:
    • FloatFormatTests: C against the interpreter on 75,000 values in every C configuration, plus ECMAScript layout cases;
    • run-pass/float-printing.nova: the expected text from Python and numpy, not from either implementation;
    • offline: Python and numpy on 412,042 values. On the shared box, every f32: 4,278,190,082 values, all but the NaNs, each reading back and none with a shorter decimal next to it. Also 5,010,501 doubles against Python repr. 0 failures.

G7c: --no-float, math methods, floats in the fuzzer ​

  • --no-float (build and check; type-system §35.9) makes every float type, literal and fromBits N0297, a new code. c4.py to c8.py pass it, and the corpus directive // option: no-float tests it.
  • Math methods (§35.8): sqrt, abs, floor, ceil, min and max, by IEEE.
    • None calls libm. ADR 0011 Q5 expected single instructions, but baseline x86-64 has none for floor and ceil (roundsd needs SSE4.1).
    • So floor, ceil, abs, min and max are exact portable C in rt.h. sqrt is the hardware instruction: __builtin_elementwise_sqrt with clang, and __builtin_sqrt under -fno-math-errno with gcc (Toolchain.SemanticFlags now passes it).
  • The fuzzer generates float expressions:
    • literals at the edges, arithmetic, the math methods, conversions both ways, and branch expressions;
    • in print statements, in comparisons, and in integer expressions through saturatingTruncate, and rarely through truncate, which may panic.
  • Its first run found a bug: if and match expressions whose branches are all flexible float literals typed as i32 (G5's default, from before float literals were flexible). They now default to f64 (§15.3).
  • Tests: run-pass/float-math.nova (signed zeros, halves, 2^52 + 1, NaN, f32; compiled and interpreted, the same), check-fail/no-float.nova, new cases in check-fail/floats.nova, and the regression in run-pass/float-printing.nova.
  • Not done here: a float workload in benchmarks/suite, in every language, run on the Ryzen box (G7d). MSVC builds every float corpus program in CI's Windows job.

G8a: function values (ADR 0012) ​

  • fn(T…) -> R types (grammar.md §2, type-system §36.1): a new type kind, Function. It is Copy, holds no loans and is not FFI-safe, separate from C's extern "C" fn.
  • Values:
    • a package function's name is a value of its type, and so is a generic instance named with its type arguments;
    • not values (N0298, new): extern and unsafe functions, since calls through values are safe; functions with region parameters, whose signatures can store loans through an argument; main; and functions whose reference result has several possible sources.
  • Calls through a local, a parameter or a struct field are safe.
    • They lower to a builtin. The borrow checker's existing rule for builtins gives the result the loans of every reference argument, which is sound because the function held cannot have region parameters.
    • Effects were sleep and alloc until G8b′'s effect lists.
  • C: a typedef nvN_… per type, the function's address as the value, and an indirect call. The interpreter reuses its function addresses.
  • Fixed on the way, older than G8: an array of a struct with an unknown field type crashed the checker (HasLayout ignored error types). It now reports the unknown type.
  • Tests:
    • run-pass/function-values.nova: arguments, branch expressions, generic instances, struct fields, a higher-order helper, scoped parameters, borrow mut, no result; compiled and interpreted, the same;
    • parse-pass/function-types.nova (new snapshot, reviewed);
    • check-fail function-values, function-value-loans, function-value-effects and array-of-broken-struct.
    • Two older tests changed with the rules: let fnValue = two; is now valid (calls.nova), and match (f) on a function is N0207 (enums.nova).

G8b: closures (ADR 0012, amended in its §5a) ​

  • Literals (x: i32, y) => expression or => { block } (grammar.md §2, type-system §36.2).
    • A capture-free literal is a plain function value.
    • One that captures is a borrow fn(…) -> R, or a borrow mut fn(…) -> R if it changes what it captures.
  • Representation: one pointer. The closure points at its environment, a hidden local of the enclosing block whose first member is the code pointer, followed by a reference to each capture (C11 6.7.2.1p15). So a closure type is an ordinary reference type, and the existing borrow checker enforces every capture rule. G8b adds two IsScoped cases (the environment and the callable object) and no checker rule.
  • The body is a synthetic function. It is checked where the literal is, with the outer scopes visible. Captures are recorded at name lookup (shared), and become mutable at a mutable borrow or an assignment rooted at them.
    • A reflection-based scan of the body's syntax decides beforehand whether the literal names an outer local; locals cannot be shadowed, so the answer is exact.
    • NIR maps each captured local to its place through the environment parameter.
  • Conversions: a function value becomes a closure through an adapter (an environment holding a copy of it), and a borrow fn becomes a borrow mut fn. The reverse is N0299 (new), and so is a capturing closure where a plain fn is expected. An uninferable parameter or result is N0236 (new).
  • C: nvCE_<id> environments, nvCB_<id> bodies taking const void *env, nvCK_… code-pointer typedefs, and a call (*f)(f, args…).
  • Found on the way: G6's source-list lookahead did not count fn as a type start, so borrow(r) fn(…) misparsed. Fixed.
  • Tests:
    • run-pass/closures.nova: shared and mutable captures, capture-free literals, an adapter, nested closures, generics, this, a closure in a struct with a region parameter, a loop, a reference parameter, a String; compiled and interpreted, the same;
    • parse-pass/closures.nova (new snapshot, reviewed);
    • check-fail closures, closure-loans (N0240, N0242, N0244, N0251 from the existing rules) and closure-effects.

G8b′: effect lists on function and closure types (ADR 0012 §3.7, amended in §5a) ​

  • @effects(…) before fn in a type (grammar.md §2, type-system §36.3): @effects() fn(i32) -> bool, borrow @effects() fn(i32). The list is part of the type; no list means both effects. A call through a value has its type's effects, so @context(atomic) and @context(irq) code can call closures and function values.
  • How values are held to a list:
    • a named function where a type with a list is expected takes that type. After effect inference, the effect checker compares the function's effects with the list (N0295 where it is named, with the call chain);
    • a closure literal, or an adapter closure, gets the list as its body's @effects bound;
    • any other value converts only to a type whose list contains its own (N0299 otherwise).
  • Inference does not narrow types: let f = isEven; is a fn(i32) -> bool. That keeps var and branch expressions as they were, at the cost of writing the type where a list is wanted.
  • C: names gain e<bits> after N/Q only for types with a list, so every existing name is unchanged. Typedefs differing only in the list name the same C type.
  • Found on the way (older than G8b′, not fixed): an integer literal fixes a type parameter before a function argument is considered, so apply(square, 7) with square: fn(i64) -> i64 reports fn(i32) -> i32 expected (N0205).
  • Tests:
    • run-pass/effect-lists.nova: atomic and irq functions calling closures and function values, a generic higher-order function, an @effects() function calling through a value, adapters, a field, conversions to wider lists; compiled and interpreted, the same;
    • parse-pass/effect-lists.nova (new snapshot, reviewed), parse-fail/effect-list-placement.nova;
    • check-fail effect-lists (N0290, N0299) and effect-list-violations (N0295: named functions directly and through a call, an adapter, a literal's body, an assignment, a field, calls in irq and atomic contexts).

G8c: library methods taking closures (ADR 0012 §3.9, amended in §5a) ​

  • sortBy, forEach, any, all on slices, arrays (a[..]) and vectors, and retain on vectors (type-system §36.4). They are written in Nova, in lib/core/slices.nova, as static functions of SliceAlgorithms<T>; the method calls name them. Each closure is a borrow mut fn over borrow T, so elements need not be Copy.

  • sortBy is an unstable introsort: median-of-three quicksort, insertion sort below 16 elements, heapsort past a depth of 2·log₂ n. It allocates nothing, and with an inconsistent comparator still leaves a permutation.

  • Two new primitives the library needs, also public:

    • s.swap(i, j) (§16.5): both indices checked, any element type;
    • v.truncate(n) (§23.1). retain lowers to the library's retain over the elements, then a truncation, so the vector is evaluated once.
  • Effects: a call through a closure without an effect list, so not in atomic or irq code; effect-polymorphic functions are OD-9c. Found on the way: clear had no effects in the effect checker although it runs the elements' drops (§32.1). It and truncate now have their drops' effects.

  • Also found and fixed:

    • G8b: a closure capturing a reference parameter emitted const const T * *, which clang rejects. Found by the fuzzer. C now writes T *const *, and run-pass/closures.nova covers it;
    • older than G8: an array literal of string literals, ["kiwi", "fig"], was typed [str; 2] and crashed NIR lowering. Array elements are no longer read through references to unsized values, so it is N0246, like any array of string views;
    • G8b: a rejected call with an untyped closure argument also reported N0236 for each parameter. Such closures are now checked quietly.
  • The fuzzer generates closures: local ones, shared and mutating, called once; function values; and forEach, any, all, sortBy (a key comparison, so equal keys test the sort's determinism) and retain on its arrays and vectors.

  • Tests:

    • run-pass/closure-methods.nova: every method on arrays, sub-slices and vectors; Copy, String and boxed elements; empty and one-element slices; random, duplicate, all-equal (heapsort), sorted and reversed inputs; an inconsistent comparator; swap and truncate. Compiled and interpreted, the same;
    • run-fail/slice-swap-bounds.nova;
    • check-fail closure-methods, closure-method-loans (N0241) and closure-method-effects (N0295, including clear and truncate).
  • The sort benchmark, benchmarks/sort/ (README there), sorts 10 × 1,000,000 values on the Ryzen. Sort times in seconds (median of 9, minus the generate-only baseline; run after the campaign, on an otherwise quiet box):

    CompilerNova sortByC qsortthe same introsort in Cthe same, comparator opaque
    clang 18.1.81.071.170.630.91
    gcc 12.2.01.131.200.670.94
    • sortBy is 6–9% faster than qsort with both compilers.
    • Against the same algorithm in C, the gap has two parts:
      • about 0.28 s is the indirect call: C slows from 0.63 s to 0.91 s when its comparator is opaque, as Nova's is;
      • the remaining 0.16–0.19 s (Nova over same-opaque, about 1.2×) is Nova's checks and closure environment. --no-bce changes nothing (1.08 s and 1.10 s in the first run, against 1.10 s and 1.11 s with elimination), so it is not the index checks that elimination can remove. Not analysed further.
    • Inlining the comparator, as C can when it sees the function, would need monomorphised closures (OD-9a).
  • The G8c campaign on the shared Debian box: 60 minutes, seeds from 9200000, 10 jobs, closures generated.

    • 8,517 programs, 1 finding (seed 9202817), older than G8.
    • Under a condition that is never true (if (1u16 < 0u16)), the overflow-check analysis's facts contradict. So it removed the check of a constant 1i32 + 2147483647i32 there, and clang -Werror rejected the overflowing constant expression in the C.
    • Fixed: operations on constants are decided by their exact result (optimisations.md §3.3), with run-pass/dead-constant-overflow.nova (reduced from the finding by line deletion).

G7d: a float workload in the suite (ADR 0011) ​

  • benchmarks/suite/nbody/: the Benchmarks Game's n-body, the sixteenth workload, in Nova, C, C++, Rust, Zig and Goose (suite README).

    • The same algorithm everywhere: an array of five f64 structs and 10,000,000 steps.
    • The checksum combines the energy before and after, to 10⁻⁹. All eight builds print 169075164169077842, with and without FMA contraction.
  • C's build gains -lm (for sqrt under glibc); nothing else in the suite's builds changed.

  • Ryzen, median of 5 (ms):

    C compilernovaccpprustrust-checkedzigzig-safegoose
    clang 18.1.8624614619818824361412612
    gcc 12.2.0593970977800831348412710
    • Nova and C are within 2% under clang.
    • gcc: C and C++ take 1.6× Nova's time because of errno. Nova's C is compiled with -fno-math-errno (§35.8), and C's is not, so gcc keeps a call to libm's sqrt for the errno case. Checked on the same box:
      • gcc's C goes from 0.98–1.04 s to 0.58–0.59 s with -fno-math-errno;
      • clang's changes little (0.63–0.68 s to 0.61 s).
    • Rust takes 1.3× C's time under clang. Not investigated.
    • Zig takes 0.58× C's time. Not investigated (its LLVM, not --cc, compiles it).
    • Checks: Nova's bounds and overflow checks cost nothing visible here. The only integer operations are loop counters over constant bounds, which elimination removes.

Acceptance (2026-10-26) ​

The criterion (roadmap.md): each gap has its spec, positive and negative tests, and the probe that showed it now passing. Each increment above records its spec and tests. The close-out added a re-run of the probes, the README's own examples of the new features, and a regression pass against M9's acceptance.

The probes. Each gap's probe, written again against the close-out compiler (fc below), built with clang and run under --interp:

#ProbeResult
G1match on Result<E, IoError> with Ok(E.A(x)), Err(IoError.NotFound); Some(3)runs, same output; found a backend bug (below)
G2for (s in shapes) over a Vector of a struct holding a Stringruns
G3a cursor walking a boxed list, reassigned while a reference made through it livesruns
G4'k' as a u8, wrappingAdd, saturatingAdd, checkedAdd, String.appendNumberruns
G5let parity = if (…) "even" else "odd", a match expressionruns
G6a parameter of type borrow (i32?)runs
G7f64, sqrt, printingruns
G8retain with a capturing closure, sortBy, a mutating forEachruns

Found by the probes: a program's own Result<S, IoError>, with S a struct or an enum, did not compile to C. The backend wrote its I/O conversion helper (nvIOR_…, c-backend.md §2) for every Result<_, IoError>, casting the runtime's integer to S, which clang rejects. It is older than M11 (C3); the interpreter was not affected. Fixed: the helpers are written only for the results I/O built-ins return (void, an integer or a descriptor), with run-pass/io-error-results.nova.

Where the gaps showed.

  • F1 (arena) and F3 (linked-list): fixed in G3 and earlier; the suite keeps its helper functions so that timings stay comparable (commented there).
  • F5: parsing now writes 'k', '=', '\n' and '0' instead of their numbers; its C is unchanged apart from one source column. string-building still appends each digit as a one-byte view, digitText[d..d + 1]: there is no method that appends a single character to a String. Not one of this milestone's gaps; recorded here.
  • The README gained a feature entry for floats and closures and a tour section, "Closures and floats" (run-pass/readme-closures.nova).

Regression.

  • Local suite on macOS (clang): 2,401 passed.

  • CosmoOS, no boots: c4.py on both arches, c6.py, c7.py --no-boot: every check passes; CosmoOS's tree is unchanged.

  • The whole suite in every language on the Ryzen (as 9f's A; m11.sh), sixteen workloads. Nova/C, median of five:

    CompilerNova/C (sixteen workloads)median9f (fifteen)
    clang 18.1.80.89 (large-objects) to 2.45 (string-building)1.190.93 to 2.44, median 1.17
    gcc 12.2.00.61 (nbody) to 3.50 (string-building)1.461.02 to 3.54, median 1.40

    The gcc row leaves out recursion (0.03 of C), as 9f's did. Without nbody, gcc's range is 0.96 to 3.50. Two Nova rows moved by more than 9f's 15% noise bound against 9f's single run: numeric under clang (+16%) and binary-trees under gcc (+21%). Their main.c is byte-identical to 9f's compiler's (7510bd2), and the runtime only gained code they do not call. Re-run interleaved on the same box (m11-ab.sh: 9f's compiler, then the close-out's, three rounds each, clang and gcc, Nova and C): the fastest of each round's five runs is the same for both compilers (binary-trees under gcc 545–549 ms against 545–548 ms; numeric under clang 332–342 ms against 333–365 ms). Only the medians spread, and C's spread as much between the two trees with identical code (numeric under gcc 223 ms against 267–284 ms). The two rows are this shared machine's noise, not a change.

  • LZ4 and fbvalid, once per compiler, against their C originals:

    clanggcc
    LZ4 compress, Nova/C speed (text, random, records)0.76, 0.53, 0.590.69, 0.60, 0.56
    LZ4 decompress, Nova/C speed0.99, 0.96, 0.760.84, 0.91, 0.70
    fbvalid, Nova/C time1.001.06

    Random-data compression under clang, at 0.53, is below the README's "compresses at 0.59–0.75". That range was wrong already at M9: 9f's own runs measured 142–144 MB/s against C's 261–272 (0.53–0.55), as benchmarks/lz4's earlier tables do (0.54–0.57). Interleaving 9f's compiler with the close-out's (m11-ab2.sh, three rounds per compiler) gives the same speeds (random compression 141–144 MB/s against 135–144), and the generated C of both ports is byte-identical to 9f's. The README now gives the medians of four clang runs per data set: compression 0.52–0.76 of C, decompression 0.77–1.02.

  • Fuzz campaign on the shared Debian box at the close-out code: 60 minutes, seeds from 9300000, 10 jobs. 8,502 programs, 0 findings (1,748 panicked, 5,570 other exit statuses, as generated).

Result. Every gap of the plan is closed, each with its spec, tests and passing probe; the probes found one older backend bug, fixed. Nothing regressed against M9's acceptance. M11 is complete; M10 (tooling) is next.

Pre-1.0: the language and its tools are still changing.