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
| # | Gap | Probe on M9's compiler | Spec | Status |
|---|---|---|---|---|
| G1 | Nested patterns: Ok(E.A(x)), Some(3), Err(IoError.NotFound) | expected ')', found '.' (N0100) | type-system §15.2, §18.4 | done |
| G2 | for over an array, slice or vector of a non-Copy element: the variable is a reference | cannot move a 'S' out of the value 'shapes' refers to (N0251) | §16.4 | done |
| G3 | Borrow-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.md | done |
| G4 | Small 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.2 | done |
| G5 | if and match as expressions | expected expression, found 'if' (N0102) | §5, §9, §15.3 | done |
| G6 | A reference to an optional as a type (F2: borrow T? is an optional reference) | — (no syntax) | §13.5, §22.1, grammar.md §2 | done |
| G7 | Floating-point types (OD-23) | unknown type 'f64' (N0204) | a design and an ADR first | ADR 0011 accepted; G7a–G7d done |
| G8 | Capturing closures (OD-9) | expected ')', found ':' (N0100) | a design and an ADR first | ADR 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 orboolliteral, 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
Errpatterns and literal sub-patterns.
G2: for over non-Copy elements (8e2c65b)
- Over a sequence of a non-
Copyelement, the loop variable is aborrow Tto the element in place, the rule of amatchon a reference; the sequence cannot change while it lives.Copyelements are still copied (§16.4). - Fixed on the way: a shared slice of pointers is declared
T *const *in C, notconst 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*bstill conflicts whenbis overwritten.
G4a: character literals (4704317)
'k','\n'and'\u{1F600}'are integer literals of their Unicode scalar value:u32by 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(returningT?) andsaturatingAdd/Sub/Mulon 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.hhelpersnv_wrapping_*,nv_try_*(for the checked methods) andnv_saturating_*, each in both ofrt.h's variants: the compiler builtins and the portable fallback MSVC uses. Signed wrapping goes through the unsigned twin andnv_as_iN, so it has no implementation-defined conversion. The checked operators' helpers did not change. - The interpreter computes the exact result in
Int128and then wraps, tests or clamps it. - Tests:
run-pass/overflow-methods.nova(every width, at and past both bounds, compiled and interpreted) andcheck-fail/overflow-methods.nova. The fuzzer now generateswrapping*andsaturating*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, likeappend. A non-integer argument is N0207.- C:
rt.h'snv_format_i64/nv_format_u64write the digits into a 20-byte compound literal, which lives to the end of the enclosing block, and the string's existingnvVA_helper appends them.INT64_MINis negated in unsigned arithmetic. The interpreter shares one append routine (AppendText) betweenappendandappendNumber. - 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) andcheck-fail/append-number.nova. - The suite's
parsingandstring-buildingworkloads 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 bandmatch (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). Aniformatchthat begins a statement is still the statement form, and anifexpression needs itselse.- 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
ifassigning avar.?:is not available, since?is already postfix (Resultpropagation). Without an expected type, the first branch that is neither flexible nornullgives the type, sox + if (c) 1 else 2withx: u8isu8arithmetic. - HIR: an
HirStmtValueholds the statement form, whose value branches end in anHirYield; 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 abreakin 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,continueandbreakfrom a branch, evaluation order; compiled and interpreted),parse-passandparse-fail/branch-expressions.nova,check-fail/branch-expressions.novaandcheck-fail/branch-expression-ownership.nova. The fuzzer generatesifandmatchexpressions.
G6: references to optionals (F2)
- A type may be parenthesised (grammar.md §2), so a reference to an optional is
borrow (T?)orborrow mut (Box<Node>?);borrow T?is still an optional reference, and(borrow T)?spells it out (type-system §22.1). Afterborrow, 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?andborrow (i32?); before, both printed asborrow 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
nullvalue. - Tests:
run-pass/optional-references.nova(compiled and interpreted),check-fail/optional-references.nova,parse-pass/parenthesised-types.nova(new.astsnapshot, reviewed).
G7a: floating-point types (ADR 0011)
f32andf64(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>, andtruncate<T>with itscheckedandsaturatingforms; bits()andfromBits;- constants folded with the run-time rounding.
No new diagnostic codes.
- literals rounded once in their own type, and flexible: an unsuffixed literal takes an expected
C: literals by their bits (
nv_f64_from_bits), plain operators, andrt.hhelpers.- The helpers keep conversions free of C's undefined behaviour.
nv_ftoi_*compares with exact exclusive bounds first.nv_f64_to_f32rounds finite values beyondFLT_MAXitself: toFLT_MAXbelow the midpoint 2^128 − 2^103, to infinity from it. rt.hchecks the IEEE formats andFLT_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").
- The helpers keep conversions free of C's undefined behaviour.
Interpreter and constant folding share
FloatSemantics:- each operation in .NET's
floatordouble; - integer-to-float conversion rounded in one step from the exact integer, so a
u64tof32is never rounded twice through a double.
- each operation in .NET's
Two emitter shortcuts excluded floats: folding
x == xtotrue(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.novaandfloat-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'sfloat-cast-overflowcheck.
Fix (
d112351). C4 on x86-64 showed thatrt.h'sFLT_EVAL_METHODcheck broke every kernel object: CosmoOS's-mgeneral-regs-onlyleaves no SSE there. The float parts ofrt.hnow apply only underNV_FLOATS, which a program with floats defines.FreestandingTestsbuilds its cross objects with that flag too.
G7b: printing floats
Console.write,writeLineandString.appendNumberprintf32andf64(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
reprand Schubfach agree with each other, and with the search.
- Finding: .NET 10's own shortest formatting is wrong at some powers of two. 2^-25 prints as
- 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 Pythonrepr. 0 failures.
G7c: --no-float, math methods, floats in the fuzzer
--no-float(buildandcheck; type-system §35.9) makes every float type, literal andfromBitsN0297, a new code.c4.pytoc8.pypass it, and the corpus directive// option: no-floattests it.- Math methods (§35.8):
sqrt,abs,floor,ceil,minandmax, by IEEE.- None calls libm. ADR 0011 Q5 expected single instructions, but baseline x86-64 has none for
floorandceil(roundsdneeds SSE4.1). - So
floor,ceil,abs,minandmaxare exact portable C inrt.h.sqrtis the hardware instruction:__builtin_elementwise_sqrtwith clang, and__builtin_sqrtunder-fno-math-errnowith gcc (Toolchain.SemanticFlagsnow passes it).
- None calls libm. ADR 0011 Q5 expected single instructions, but baseline x86-64 has none for
- 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 throughtruncate, which may panic.
- Its first run found a bug:
ifandmatchexpressions whose branches are all flexible float literals typed asi32(G5's default, from before float literals were flexible). They now default tof64(§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 incheck-fail/floats.nova, and the regression inrun-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…) -> Rtypes (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'sextern "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):
externandunsafefunctions, 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
sleepandallocuntil 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 (
HasLayoutignored 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-effectsandarray-of-broken-struct. - Two older tests changed with the rules:
let fnValue = two;is now valid (calls.nova), andmatch (f)on a function is N0207 (enums.nova).
G8b: closures (ADR 0012, amended in its §5a)
- Literals
(x: i32, y) => expressionor=> { 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 aborrow mut fn(…) -> Rif 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
IsScopedcases (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 fnbecomes aborrow mut fn. The reverse is N0299 (new), and so is a capturing closure where a plainfnis expected. An uninferable parameter or result is N0236 (new). - C:
nvCE_<id>environments,nvCB_<id>bodies takingconst void *env,nvCK_…code-pointer typedefs, and a call(*f)(f, args…). - Found on the way: G6's source-list lookahead did not count
fnas a type start, soborrow(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, aString; 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) andclosure-effects.
G8b′: effect lists on function and closure types (ADR 0012 §3.7, amended in §5a)
@effects(…)beforefnin 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
@effectsbound; - any other value converts only to a type whose list contains its own (N0299 otherwise).
- Inference does not narrow types:
let f = isEven;is afn(i32) -> bool. That keepsvarand branch expressions as they were, at the cost of writing the type where a list is wanted. - C: names gain
e<bits>afterN/Qonly 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)withsquare: fn(i64) -> i64reportsfn(i32) -> i32expected (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) andeffect-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,allon slices, arrays (a[..]) and vectors, andretainon vectors (type-system §36.4). They are written in Nova, inlib/core/slices.nova, as static functions ofSliceAlgorithms<T>; the method calls name them. Each closure is aborrow mut fnoverborrow T, so elements need not be Copy.sortByis 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).retainlowers 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:
clearhad no effects in the effect checker although it runs the elements' drops (§32.1). It andtruncatenow 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 writesT *const *, andrun-pass/closures.novacovers 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.
- G8b: a closure capturing a reference parameter emitted
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) andretainon its arrays and vectors.Tests:
run-pass/closure-methods.nova: every method on arrays, sub-slices and vectors; Copy,Stringand boxed elements; empty and one-element slices; random, duplicate, all-equal (heapsort), sorted and reversed inputs; an inconsistent comparator;swapandtruncate. Compiled and interpreted, the same;run-fail/slice-swap-bounds.nova;- check-fail
closure-methods,closure-method-loans(N0241) andclosure-method-effects(N0295, includingclearandtruncate).
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):Compiler Nova sortByC qsortthe same introsort in C the same, comparator opaque clang 18.1.8 1.07 1.17 0.63 0.91 gcc 12.2.0 1.13 1.20 0.67 0.94 sortByis 6–9% faster thanqsortwith 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-bcechanges 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 constant1i32 + 2147483647i32there, and clang-Werrorrejected 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
f64structs 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.
- The same algorithm everywhere: an array of five
C's build gains
-lm(forsqrtunder glibc); nothing else in the suite's builds changed.Ryzen, median of 5 (ms):
C compiler nova c cpp rust rust-checked zig zig-safe goose clang 18.1.8 624 614 619 818 824 361 412 612 gcc 12.2.0 593 970 977 800 831 348 412 710 - 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'ssqrtfor 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).
- gcc's C goes from 0.98–1.04 s to 0.58–0.59 s with
- 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:
| # | Probe | Result |
|---|---|---|
| G1 | match on Result<E, IoError> with Ok(E.A(x)), Err(IoError.NotFound); Some(3) | runs, same output; found a backend bug (below) |
| G2 | for (s in shapes) over a Vector of a struct holding a String | runs |
| G3 | a cursor walking a boxed list, reassigned while a reference made through it lives | runs |
| G4 | 'k' as a u8, wrappingAdd, saturatingAdd, checkedAdd, String.appendNumber | runs |
| G5 | let parity = if (…) "even" else "odd", a match expression | runs |
| G6 | a parameter of type borrow (i32?) | runs |
| G7 | f64, sqrt, printing | runs |
| G8 | retain with a capturing closure, sortBy, a mutating forEach | runs |
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:
parsingnow writes'k','=','\n'and'0'instead of their numbers; its C is unchanged apart from one source column.string-buildingstill appends each digit as a one-byte view,digitText[d..d + 1]: there is no method that appends a single character to aString. 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.pyon 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:Compiler Nova/C (sixteen workloads) median 9f (fifteen) clang 18.1.8 0.89 ( large-objects) to 2.45 (string-building)1.19 0.93 to 2.44, median 1.17 gcc 12.2.0 0.61 ( nbody) to 3.50 (string-building)1.46 1.02 to 3.54, median 1.40 The gcc row leaves out
recursion(0.03 of C), as 9f's did. Withoutnbody, 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:numericunder clang (+16%) andbinary-treesunder gcc (+21%). Theirmain.cis 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-treesunder gcc 545–549 ms against 545–548 ms;numericunder 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 (numericunder 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:
clang gcc LZ4 compress, Nova/C speed (text, random, records) 0.76, 0.53, 0.59 0.69, 0.60, 0.56 LZ4 decompress, Nova/C speed 0.99, 0.96, 0.76 0.84, 0.91, 0.70 fbvalid, Nova/C time 1.00 1.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.