ADR 0011 — M11 G7: floating-point types
- Status: Accepted (project owner, 2026-10-25, taking every recommendation in §5). The wording is the agent's; the owner may revise it. Not yet implemented: the decisions are written into
type-system.md(a new §35),grammar.md,c-backend.mdandcosmoos/profiles.mdbefore any code (§6). - Scope: M11 G7 (
milestone-11.md), closing OD-23 (open-decisions.md). It amendstype-system.md§1 (N0216 for floats), §5 (flexible literals) and §7 (conversions).
1. Context
Facts this design rests on, from the code and the docs as they are today:
- What already exists. The lexer reads float literals with
f32/f64suffixes (Syntax/Lexer.cs:236-239,grammar.md§1:1.and.5are not floats). The checker rejects them: "floating-point numbers are not supported in M1" (N0216,TypeChecker.csSynth,FloatLiteralExpr), andf64as a type is unknown (N0204).language-spec.md§2 and Ex2/Ex13 already usef64(IEEE 754, Cfloat/double), andc-backend.md's type table mapsf32/f64tofloat/double. - Profiles.
cosmoos/profiles.md§1: floats are yes in hosted and CosmoOS user programs, forbidden in the CosmoOS kernel, and target-defined in the generic freestanding profile. CosmoOS builds its kernel and loader with-mgeneral-regs-onlyon both arches (CosmoOSbuild/arch/x86_64.mk:6,14,aarch64.mk:19,28), andscripts/check-fpregs.shrejects objects that touch FP registers. Nova runs that script inc4.pyandc5.py(c4.py:204-206,c5.py:104);c6.pyandc7.pyreuse C4's other checks but not this one. - No libm, no exact libc formatting. CosmoOS's libc has no
<math.h>(libc/include/), and itsprintffloat conversions are "deliberately small … not exactly rounded in the last digit for every input" (libc/src/printf.c:9-14). Formatting therefore cannot go through libc if output is to be deterministic and equal to the interpreter's. - No compiler-rt (
c-backend.md§1). On the two 64-bit targets Nova supports, float arithmetic andint64 ↔ doubleconversions compile to instructions.fmod,sqrtand the like are libm calls unless they become builtins that lower to an instruction. - The interpreter is an oracle (M7c). Every program must print the same output compiled and interpreted, and the fuzzer compares them (
tests/Nova.Fuzz). .NET'sdoubleandfloatare IEEE binary64/binary32 with round-to-nearest-even. The JIT does not fusea * b + cinto an FMA unless asked (Math.FusedMultiplyAdd) (inferred, to verify in G7). - C compilers may contract. clang contracts
a * b + cinto an FMA within an expression by default (-ffp-contract=on). gcc does in GNU modes (-ffp-contract=fast), but not under-std=c11. An FMA rounds once instead of twice, so the C result could differ from the interpreter's and between compilers (inferred from compiler documentation, to verify in G7). - Undefined behaviour. In C, converting a float to an integer type that cannot hold its truncated value is undefined (C11 6.3.1.4). Generated C must have none (
c-backend.md).
2. Options
| A. IEEE floats, deterministic (proposed) | B. Floats as in C | C. No floats; fixed-point library | |
|---|---|---|---|
| Semantics | IEEE 754 binary32/64, round-to-nearest-even, no contraction, no traps; same result compiled and interpreted | whatever the C compiler does (contraction, -ffast-math allowed) | integers only |
| Float → int | checked: panics on NaN and out of range | C's cast (UB out of range) | — |
| Printing | Nova's own shortest round-trip formatter | libc printf("%g") | — |
| Cost | a formatter in the runtime (~1–2k lines with tables, inferred) | small | a library |
| Safety | no UB; deterministic | UB on conversion; results vary by compiler | — |
B breaks two rules (no UB; the interpreter as oracle). C leaves Ex2/Ex13 and every numeric program out. A is proposed.
3. Proposed decision
Types.
f32andf64are IEEE 754 binary32 and binary64, laid out perTargetInfo(4/4 and 8/8 on both targets). They are Copy, need no drop, hold no loans, and are FFI-safe (Cfloat/double). The FP environment is the default one, never changed by Nova code: round to nearest even, no traps, and no flags read.Literals.
1.5,1e9,2.5e-3, with an optionalf32/f64suffix. An unsuffixed float literal is flexible like an integer one (§5): it takes an expectedf32orf64, and defaults tof64. The literal's value is the decimal rounded to nearest (even) in its type, exactly once:0.1f32is thef32nearest 0.1, not thef64nearest 0.1 rounded again. An integer literal is not a float (let x: f64 = 1;is N0205; write1.0), as Nova has no implicit conversions. A literal that rounds to infinity is an error (N0206).Operators.
+ - * /and unary-on two operands of the same float type give that type. They never panic: overflow gives ±infinity,0.0 / 0.0gives NaN, as IEEE says. This is the one exception to "arithmetic panics on failure" (OD-6), and the spec states it.%is not defined on floats in G7 (N0207), since C'sfmodis a libm call. Comparisons== != < <= > >=are IEEE's: NaN is unequal to everything, itself included, and-0.0 == 0.0. Bitwise and shift operators are N0207.No contraction, no excess precision. Each operation rounds to its type. The generated C enforces this:
rt.hchecksFLT_EVAL_METHOD == 0(<float.h>is a freestanding header) and emits#pragma STDC FP_CONTRACT OFF;- novac's builds pass
-ffp-contract=off, andc-backend.md's checklist requires it of integrations, since gcc ignores the pragma; -ffast-mathand its parts are unsupported, as-fwrapvalready is not relied on.
The interpreter computes each operation in .NET's
floatordouble. Constant folding in the compiler uses the same .NET arithmetic.Conversions (
type-system.md§7).asstays lossless only:f32 as f64;i8,i16,u8andu16 as f32; andi8–i32,u8–u32 as f64. Every value of the source type is exactly representable in the target type.round<T>(e)(new, free-standing liketruncate): any integer or float to a floatT, rounded to nearest even. It is always defined:round<f64>(x)forx: i64,round<f32>(d)ford: f64(overflow gives infinity).truncate<T>(e)extended: a float to an integerT, rounding toward zero. It panics ifeis NaN or the truncated value does not fit inT("float out of range", a new panic kind). The C code compareseagainst the exactly representable bounds before converting, so there is no UB.checkedTruncate<T>(e)returnsT?instead of panicking.saturatingTruncate<T>(e)clamps, with NaN giving 0, as Rust'sasdoes.f.bits()andf64.fromBits(u)(f32likewise) reinterpret the representation, for hashing and serialising. Literals are emitted to C by their bits, so their values never depend on the C compiler's decimal parsing.
Printing.
Console.write,writeLineandString.appendNumberacceptf32andf64. The text is the shortest decimal that rounds back to the same value, laid out as ECMAScript'sNumber::toString(ECMA-262 §6.1.6.1.20):- plain digits for 1e-6 ≤ |x| < 1e21 (
0.1,1.5,100,123456.789); - an exponent otherwise (
1e21,1.5e-7); NaN,Infinityand-Infinity, and0for both zeros.
ECMAScript's rule is fully specified and widely implemented, so test vectors exist. The C runtime implements it with its own shortest-digits algorithm (Ryu or Schubfach, §5 Q4), using 64×64→128-bit multiplication in portable 32-bit halves (no
__int128, which MSVC lacks). The interpreter gets the shortest digits from .NET'sToString("R")(shortest round-trip since .NET Core 3.0) and lays them out by the same rule.- plain digits for 1e-6 ≤ |x| < 1e21 (
Profiles. Floats are allowed in hosted programs and CosmoOS user programs. In the freestanding profile, a build option
--no-float(CosmoOS kernel builds pass it, fromc4.py–c8.py) makes any float type, literal or operation an error, with a new code.check-fpregs.shstays the backstop, and G7 adds it toc6.py–c8.py. Without the option, freestanding code may use floats, asprofiles.md§1's "target-defined" says. Printing floats is hosted-only (it uses the console).Not in G7.
%, a math library (sqrt,floor,sin, …), parsing floats from text,f16/f128, SIMD, rounding modes and FP exceptions, floats asmatchpatterns, and floats inconstexpressions beyond literals and the four operators. The first two come next (§5 Q5).
4. Consequences
- Ex2 and Ex13 of
language-spec.mdbecome runnable, and numeric code can be written. The README's "What Nova does not have yet" loses "floating-point types". - A float result is the same compiled with clang, gcc or MSVC and interpreted, so the fuzzer can generate float code and compare outputs. The cost is the forbidden contraction: FMA-heavy kernels are slower than C compiled with contraction on. A future explicit
f.mulAdd(a, b)(an FMA builtin) would restore it where wanted. Any performance claim needs a benchmark first (a float workload inbenchmarks/suite, e.g. n-body). - The C runtime grows by a formatter and its tables (inferred: about 10 KiB of tables for Ryu's
doublepath; less for Schubfach). It is linked only into programs that print floats. truncateon floats can panic, unliketruncateon integers. The name says what happens to the fraction, and the panic is the out-of-range case. Q3 offers a different name.- The CosmoOS kernel profile is unchanged in effect: floats were forbidden, and stay so through
--no-float.
5. Questions for the owner (decided 2026-10-25: each as recommended)
- Approach A (IEEE, deterministic, no contraction)? Recommended: yes.
- Never-panicking float arithmetic (infinity and NaN) as an explicit exception to OD-6? Recommended: yes. The alternative, panicking on non-finite results, is expensive (a check after every operation) and unlike every other language.
- Conversion names.
round<T>for to-float, andtruncate<T>(panicking),checkedTruncate<T>andsaturatingTruncate<T>for to-integer? Alternative: Rust's saturatingasfor float → int. Recommended: the proposal, since an unexpected NaN or huge value then fails loudly. - Formatter. A port of Ryu (Apache-2.0 or Boost licence; its licence would sit in
runtime/), or a new implementation of Schubfach from its paper (no licence question; more of our own code to test)? Recommended: Schubfach, tested against .NET's shortest output on several million random and boundary values. - Math library scope after G7. Recommended: next,
sqrt,abs,floor,ceil,minandmax, which lower to single instructions on both targets (__builtin_sqrtwith-fno-math-errno,__builtin_floor, …). Without libm, everything else would be a Nova implementation with its own accuracy tests. --no-floatas an option rather than a separate profile? Recommended: an option. Profiles stay asprofiles.mddefines them, and a Linux kernel build would use the same option.
5a. Amendments during implementation (2026-10-25)
- G7a.
rt.h's IEEE checks, contraction pragmas and float helpers apply only in programs that use floats (NV_FLOATS).- Without that guard, the check
FLT_EVAL_METHOD == 0broke every CosmoOS kernel object on x86-64. - CosmoOS builds those objects with
-mgeneral-regs-only, which leaves no SSE there (c4.py).
- Without that guard, the check
- G7b.
- Exponent sign. §3.6's examples wrote
1e21. ECMAScript writes1e+21, and Nova follows ECMAScript exactly, so its test vectors apply (type-system.md §35.7). - The interpreter's digits come from an exact search over rationals, not from .NET's
ToString("R"). .NET 10 prints 2^-25 as2.980232238769531E-08, which does not read back as 2^-25. - One-digit results. Schubfach is used without the Java code's two-digit minimum, so
5e-324prints as5e-324.
- Exponent sign. §3.6's examples wrote
6. Implementation plan (once accepted)
Spec first (type-system.md §35, grammar.md, c-backend.md, profiles.md), then:
- G7a. Types, literals, operators, comparisons,
as,round,truncateand its variants,bits/fromBits, C emission and the interpreter. - G7b. The formatter and printing, in the C runtime and the interpreter.
- G7c.
--no-float, the fuzzer generating float expressions, an MSVC run, and a float benchmark.
Tests to add (none run yet):
- run-pass for every operator at ±0, ±infinity, NaN, subnormals and the boundaries of each conversion;
- run-fail for
truncateof NaN and out-of-range values; - check-fail for mixed types,
%, bitwise operators and integer literals where a float is expected; - a formatter test against .NET on random bits and on ECMAScript's test cases;
- C output compiled with clang, gcc and MSVC, and compared with the interpreter.