All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
29 KiB
Floats Iteration 5 — Prose + DESIGN.md + Milestone Close (Implementation Plan)
Parent spec:
docs/specs/0005-floats.md(committede37366f, approved 2026-05-10) — section A5 (determinism contract) + thecrates/ailang-prose/Components subsection.For agentic workers: REQUIRED SUB-SKILL: use
skills/implementto run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Replace the iter-1 prose unimplemented!("Floats milestone iter 5: prose") arm with a Float renderer matching the surface-print
shape from iter 2; add §"Float semantics" to DESIGN.md per spec A5;
update DESIGN.md's "supported primitive types" line and the builtins
list to include Float entries; mark Floats [x] in roadmap; write
the milestone-close JOURNAL entry.
Architecture: Prose render is one-way (Form-A AST → Form-B text).
The Float arm in write_lit reuses the same to_string + .0-suffix- fallback shape as crates/ailang-surface/src/print.rs::write_float_lit
from iter 2 — except non-finite bits render as the Mainstream-language
spellings NaN / +Inf / -Inf rather than panicking, because prose
is a one-way render and CAN handle Form-A values that surface lex
cannot produce. DESIGN.md additions sit alongside the existing
"What is supported" list and add a new §"Float semantics" subsection.
Tech Stack: crates/ailang-prose/src/lib.rs, docs/DESIGN.md,
docs/roadmap.md, docs/JOURNAL.md. No code outside the prose
crate.
No-regression invariant: cargo test --workspace GREEN. The
iter-1 spec_drift / design_schema_drift anchors for Float
(`FLOAT` in form_a.md, "kind": "float" in DESIGN.md JSON-
schema block) are already in place since iter 1.1 — they stay
unchanged. The new DESIGN.md additions do not perturb the existing
anchors.
Files this plan creates or modifies
- Modify:
crates/ailang-prose/src/lib.rs— replace theLiteral::Float { .. } => unimplemented!(...)arm at line 917 with a real renderer; addmod testsFloat coverage. - Modify:
docs/DESIGN.md— line 2034 (primitive types list) + lines 2038-2059 (builtins list) + insert new §"Float semantics" subsection before §"What is not (yet) supported". - Modify:
docs/roadmap.md— flip Floats[~]→[x]; dropdepends on: Floatsfrom Post-22 Prelude. - Modify:
docs/JOURNAL.md— append iter-5 close entry AND separate milestone-close entry summarising the whole 5-iteration arc.
No file outside this list is touched.
Task 1: Prose renderer for Literal::Float
Files:
-
Modify:
crates/ailang-prose/src/lib.rs:911-919(write_lit) + add a private helperwrite_float_lit(out, bits); add amod testsblock (or extend the existing one — verify which). -
Step 1: Find the test mod or confirm absence
Run: grep -n "^#\[cfg(test)\]\|^mod tests" crates/ailang-prose/src/lib.rs
Determine where to add the test. If a mod tests already exists at
EOF, append to it. If not, create one.
- Step 2: Write the RED test
In the test mod, append:
/// Floats milestone iter 5.1: prose renders Float literals as
/// surface-style decimal text. Finite values use shortest
/// round-trippable decimal with `.0` suffix when neither `.` nor
/// `e`/`E` is in the rendered string (parallel to surface print).
/// Non-finite (NaN, ±Inf) render as the Mainstream-language
/// spellings — `NaN`, `+Inf`, `-Inf` — because prose is a one-way
/// render and the surface lex grammar's no-NaN/Inf restriction
/// does not apply to it.
#[test]
fn renders_float_literal_finite() {
use ailang_core::ast::*;
let t = Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } };
let s = render_term(&t);
assert_eq!(s, "1.5");
let t = Term::Lit { lit: Literal::Float { bits: 0x4024_0000_0000_0000u64 } };
let s = render_term(&t);
assert_eq!(s, "10.0");
}
#[test]
fn renders_float_literal_signed_zero() {
use ailang_core::ast::*;
let pos = Term::Lit { lit: Literal::Float { bits: 0x0u64 } };
assert_eq!(render_term(&pos), "0.0");
let neg = Term::Lit { lit: Literal::Float { bits: 0x8000_0000_0000_0000u64 } };
assert_eq!(render_term(&neg), "-0.0");
}
#[test]
fn renders_float_literal_non_finite() {
use ailang_core::ast::*;
let nan = Term::Lit { lit: Literal::Float { bits: 0x7ff8_0000_0000_0000u64 } };
assert_eq!(render_term(&nan), "NaN");
let pos_inf = Term::Lit { lit: Literal::Float { bits: 0x7ff0_0000_0000_0000u64 } };
assert_eq!(render_term(&pos_inf), "+Inf");
let neg_inf = Term::Lit { lit: Literal::Float { bits: 0xfff0_0000_0000_0000u64 } };
assert_eq!(render_term(&neg_inf), "-Inf");
}
If render_term is private or absent, look for the equivalent
helper at the top of mod tests (the existing tests use it per
the grep from earlier surveying — render_term(&Term::Lit { lit: Literal::Int { value: 42 } }) returned "42").
- Step 3: Verify RED
Run: cargo test -p ailang-prose --lib renders_float_literal
Expected: PANIC at the iter-1 unimplemented!("Floats milestone iter 5: prose") arm in write_lit.
- Step 4: Add the
write_float_lithelper + replace the arm
In crates/ailang-prose/src/lib.rs, immediately BEFORE write_lit
(around line 911), add:
/// Floats milestone iter 5: render a Float literal's bit pattern
/// as surface-style decimal text. Finite values use Grisu3
/// (`f64::to_string`) with a `.0` suffix when neither `.` nor
/// `e`/`E` appears, parallel to
/// `crates/ailang-surface/src/print.rs::write_float_lit`. Non-finite
/// values render as `NaN` / `+Inf` / `-Inf` — these are valid in
/// Form-A (the canonical bytes carry the bit pattern) but cannot
/// be expressed in surface lex; prose is one-way render, so naming
/// them after the Mainstream-language spellings is more useful
/// than a panic or a `(float-bits hex)` placeholder.
fn write_float_lit(out: &mut String, bits: u64) {
let f = f64::from_bits(bits);
if f.is_nan() {
out.push_str("NaN");
return;
}
if f == f64::INFINITY {
out.push_str("+Inf");
return;
}
if f == f64::NEG_INFINITY {
out.push_str("-Inf");
return;
}
let s = f.to_string();
out.push_str(&s);
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
out.push_str(".0");
}
}
Then replace the iter-1 arm at write_lit (line 917):
Literal::Float { bits } => write_float_lit(out, *bits),
- Step 5: Verify RED tests now GREEN
Run: cargo test -p ailang-prose --lib renders_float_literal
Expected: all 3 tests PASS.
- Step 6: Verify no
unimplemented!arm with the iter-5 string remains
Run: grep -rn "Floats milestone iter 5: prose" crates/
Expected: zero hits.
- Step 7: Verify no workspace regression
Run: cargo test --workspace
Expected: all 402+ tests pass (= 402 prior + 3 new), 0 failed.
- Step 8: Commit
git add crates/ailang-prose/src/lib.rs
git commit -m "floats iter 5.1: prose renders Float literals (finite + NaN/Inf)"
Task 2: DESIGN.md — extend supported primitives + builtins lists
Files:
-
Modify:
docs/DESIGN.md:2034(primitive types list) + lines 2038-2059 (builtins list). -
Step 1: Update line 2034 to include Float
In docs/DESIGN.md, line 2034, replace:
- Int, Bool, Unit, **Str** as primitive types.
with:
- Int, Bool, Unit, **Str**, **Float** as primitive types.
- Step 2: Refresh the builtins list at lines 2038-2059
In docs/DESIGN.md, replace the **Builtins.** block at lines
2038-2059 with the post-Floats current shape:
- **Builtins.** Arithmetic operators (`+`, `-`, `*`, `/`) of type
`forall a. (a, a) -> a` (codegen-restricted to `{Int, Float}`);
`%` of type `(Int, Int) -> Int` (Int-only — `fmod` semantics for
Float deferred); ordering operators and `!=` (`!=`, `<`, `<=`,
`>`, `>=`) of type `forall a. (a, a) -> Bool` (codegen-restricted
to `{Int, Float}`); polymorphic `neg : forall a. (a) -> a`
(codegen-restricted to `{Int, Float}`; Float arm uses LLVM
`fneg double` for correct `-0.0` handling); logical
`not : (Bool) -> Bool`; conversions
`int_to_float : (Int) -> Float`,
`float_to_int_truncate : (Float) -> Int` (saturating, NaN → 0),
`float_to_str : (Float) -> Str` (codegen lowering deferred —
symbol installed but errors at codegen pending runtime Str
allocation); inspection `is_nan : (Float) -> Bool` (LLVM
`fcmp uno`); Float bit-pattern constants `nan : Float`,
`inf : Float`, `neg_inf : Float` (resolved as bare values, lower
to direct hex-float `double` SSA constants at use site); the IO
effect ops (`io/print_int|bool|str|float`); **`==` : forall a.
(a, a) -> Bool**; and **`__unreachable__ : forall a. a`**.
- **`==` is polymorphic.** The typechecker accepts
`==` at any type whose two sides agree (the rigid `a` of the
`Forall` is unified by HM at the use site). Codegen
monomorphises and dispatches on the resolved AIL arg type:
`Int` → `icmp eq i64`; `Bool` → `icmp eq i1`;
`Str` → `call @strcmp(ptr, ptr)` then `icmp eq i32 0`
(`@strcmp` is declared in the LLVM IR header alongside
`@printf` / `@GC_malloc`); `Unit` → constant `i1 true`
(Unit has a single inhabitant; both sides are still
evaluated for any side effects); `Float` → `fcmp oeq double`.
ADT and `Fn` arg types are rejected at codegen with a
`CodegenError::Internal` mentioning `==` and the offending
type — neither has a canonical structural-equality scheme
yet, and the language deliberately does not silently elide
the check. **`!=` for Float uses `fcmp UNE double` (NOT
`one`)** — `one` is "ordered and not equal" and would return
false for `nan != nan`, violating IEEE-`!=`.
- **`__unreachable__`** is a polymorphic bottom value: a use of
`__unreachable__` typechecks against any expected type at
the use site and codegens to the LLVM `unreachable`
instruction (UB if ever executed). It is the chain
machinery's deepest fall-through for matches that the
typechecker proved exhaustive, and it is available to user
code as an explicit panic primitive
(`(if cond __unreachable__ ...)` for assertions or
impossible branches). Reference site is
`Term::Var { name = "__unreachable__" }` / form-A bare
`__unreachable__`.
The widening + new entries are folded in; the existing prose about
== polymorphism gains a Float → fcmp oeq double line; the
!= UNE-not-one note is added; the existing __unreachable__
prose is preserved verbatim.
- Step 3: Verify DESIGN.md still parses (markdown render)
Run: cargo test -p ailang-core --test design_schema_drift
Expected: GREEN — the "kind": "float" anchor (added in iter 1)
is still present in the JSON-schema block, unchanged. The new
prose only adds; nothing is removed.
- Step 4: Commit
git add docs/DESIGN.md
git commit -m "floats iter 5.2: DESIGN.md — Float in primitive types + refreshed builtins list"
Task 3: DESIGN.md — add §"Float semantics" subsection
Files:
-
Modify:
docs/DESIGN.md— insert new top-level section immediately BEFORE## What is not (yet) supported(line 2006). -
Step 1: Insert the new subsection
In docs/DESIGN.md, immediately before ## What is not (yet) supported
(line 2006), insert:
## Float semantics
`Float` is IEEE-754 binary64 (LLVM `double`). One float type ships;
no `f32` variant. The runtime / codegen contract:
**Guaranteed:**
- Every individual builtin (`+`/`-`/`*`/`/`/`neg`/`<`/`==`/...) lowers
to a single LLVM IR instruction on the Float arm:
`fadd/fsub/fmul/fdiv double`, `fneg double`, `fcmp olt/ole/ogt/oge
double`, `fcmp oeq double`, `fcmp une double` (for `!=`). On a
fixed `(target triple, LLVM version)` pair, the bit pattern of
the result of any single op is reproducible.
- NaN and ±Inf propagate per IEEE 754 — no silent collapse to zero,
no trap. Arithmetic on a NaN operand produces NaN; division by
zero produces ±Inf; `0.0 / 0.0` produces NaN.
- `-0.0` and `+0.0` are distinct bit patterns at the canonical-JSON
hash level (`{"bits":"0000000000000000",...}` vs
`{"bits":"8000000000000000",...}` — distinct `def_hash`s) but
compare equal via `==` per IEEE (`fcmp oeq double` returns true
for `+0 == -0`). This asymmetry is the correct IEEE behaviour;
it does mean `def_hash`-equality is finer than `==`-equality on
Float.
- `==` returns `false` whenever either operand is NaN
(`fcmp oeq` is the ordered-equal predicate; ordered = both
operands non-NaN).
- `!=` returns `true` whenever either operand is NaN (`fcmp une`
is the unordered-or-not-equal predicate). This matches Rust
`f64::ne` and IEEE-`!=` exactly.
- `is_nan` (`fcmp uno double %x, %x`) returns `true` iff `x` is
NaN. Bit-pattern-based NaN detection without dependence on the
payload bits.
- `int_to_float` (`sitofp`) is exact for `|n| < 2^53`,
round-to-nearest-even otherwise.
- `float_to_int_truncate` (`@llvm.fptosi.sat.i64.f64`) is total:
NaN → 0, +Inf → i64::MAX, -Inf → i64::MIN, finite-out-of-range
saturates, finite-in-range truncates toward zero. Matches Rust
`as i64` semantics (since 1.45).
**Unspecified:**
- FMA contraction. LLVM may fold `fadd (fmul a b) c` into
`fma a b c`. Bit results may differ between an op-emitted-in-
isolation pattern and an op-folded-into-FMA pattern.
- Reassociation. The compiler may reorder a chain like
`(a + b) + c` into `a + (b + c)`, producing a bit-different
result on numerically sensitive inputs.
- Subnormal flushing modes. If the target enables FTZ (flush-to-
zero) or DAZ (denormals-are-zero), subnormal results round to
zero; AILang does not enable these flags but does not forbid the
target from doing so.
- The exact NaN bit pattern produced by an op. Any quiet NaN bit
pattern is conformant; `0.0 / 0.0` may produce `0x7ff8_0000_
0000_0000` on one target and a different qNaN on another.
These are the Rust / Swift / standard-LLVM defaults — not
research-grade reproducibility guarantees. The stronger guarantee
(e.g. Pythonic `float.fromhex`-level bit reproducibility across
ops) would require `-ffp-contract=off` plus per-op intrinsic
selection — out of scope for the milestone; revisit only if a real
use case appears.
**Form-A serialisation:** Float literals carry the IEEE-754
bit pattern as a 16-character lowercase hex string in the canonical
JSON: `{"kind":"float","bits":"<16-hex>"}`. Routing through the
JSON *string* path (not `serde_json::Number`) preserves bit
stability across `serde_json` versions and lets NaN / ±Inf
round-trip through Form-A — JSON numbers cannot represent them.
**Pattern matching:** `Pattern::Lit` on `Literal::Float` is hard-
rejected at typecheck (`CheckError::FloatPatternNotAllowed`). IEEE
semantics make Float patterns semantically dubious — NaN never
matches via IEEE-`==`, and bit-exact equality is rarely what an
LLM-author wants. Use ordering operators (`<`, `>`, ...) and
`is_nan` to discriminate Floats.
`float_to_str` (Float → Str) is **type-installed but codegen-
deferred** to a follow-up milestone: it requires runtime-allocated
`Str` (the current Str path uses only static `@.str_*` globals;
no malloc-backed dynamic-Str infrastructure). Calling it
typechecks but produces a structured `CodegenError::Internal`.
- Step 2: Verify markdown still parses cleanly
Run: cargo doc --no-deps -p ailang-core 2>&1 | head -10
Expected: no new warnings (rustdoc doesn't see DESIGN.md, but the
test sweeps any markdown-formatting glitches that might have been
introduced; a clean output here is reassurance, not a hard test).
- Step 3: Verify the drift test still passes
Run: cargo test -p ailang-core --test design_schema_drift
Expected: GREEN. The new section adds prose; no anchor is removed.
- Step 4: Commit
git add docs/DESIGN.md
git commit -m "floats iter 5.3: DESIGN.md — new §Float semantics subsection (A5 determinism contract)"
Task 4: Roadmap — mark Floats [x], drop Post-22 Prelude dependency
Files:
-
Modify:
docs/roadmap.md— Floats entry P0 → mark[x]; Post-22 Prelude P1 → dropdepends on: Floatsline. -
Step 1: Mark the Floats milestone done
In docs/roadmap.md, line 36 currently reads:
- [~] **\[milestone\]** Floats — introduce `Float` as IEEE-754
Change to:
- [x] **\[milestone\]** Floats — introduce `Float` as IEEE-754
(Just the ~ → x swap; the rest of the entry stays for now —
will be removed in a follow-up tidy once the milestone-close
JOURNAL entry is enough context.)
- Step 2: Drop the Post-22 Prelude dependency line
In docs/roadmap.md, locate the Post-22 Prelude entry (P1, around
line 53). Remove the lines:
- depends on: Floats (the partial-Eq/Ord story for Float must be
settled before the Prelude classes ship, so the Prelude can
decide what's instanced and what isn't)
The Prelude is now unblocked; the Floats commitment (no Eq /
Ord for Float, only Show) is settled and documented in DESIGN.md
§"Float semantics".
- Step 3: Commit
git add docs/roadmap.md
git commit -m "floats iter 5.4: roadmap — mark Floats [x] + unblock Post-22 Prelude"
Task 5: JOURNAL — iter-5 close + milestone-close
Files:
-
Modify:
docs/JOURNAL.md— append TWO entries: iter-5 close (small, mirrors per-task subjects), and a separate milestone- close summary covering the whole 5-iteration arc. -
Step 1: Append the iter-5 close entry
In docs/JOURNAL.md, append at file end:
## 2026-05-10 — Iteration Floats.5: prose + DESIGN.md + milestone close
Replaced the iter-1 prose `unimplemented!("Floats milestone iter 5:
prose")` arm with `write_float_lit` — finite values render as
shortest round-trippable decimal with `.0` suffix (parallel to
surface print); non-finite values render as `NaN` / `+Inf` /
`-Inf` (Mainstream-language spellings) because prose is one-way
render and CAN handle Form-A NaN / ±Inf bits that surface lex
cannot produce.
DESIGN.md gained a new top-level §"Float semantics" subsection per
spec section A5: IEEE-754 binary64 commitment, per-op bit stability
on fixed (target, LLVM), NaN / ±Inf propagation, `-0`/`+0` hash-vs-
equality asymmetry, FMA-contraction / reassociation / subnormal
flushing UNSPECIFIED, conversions semantics, Form-A serialisation
shape, Pattern::Lit::Float rejection rationale, and the
`float_to_str` deferred-codegen note. The "What is supported"
bullet list at line 2034 was extended to mention `Float` as a
primitive type; the builtins list was refreshed to include the
widened ops (`forall a. (a, a) -> a`), the 5 new fn builtins, the
3 Float constants, and the 4th IO effect op (`io/print_float`).
Roadmap flipped Floats from `[~]` to `[x]`; Post-22 Prelude is
unblocked (the `depends on: Floats` line dropped — the partial-
Eq/Ord-for-Float story is now settled and documented in
§"Float semantics", so the Prelude milestone can decide what's
instanced without bouncing back to Float design).
Per-task commits:
- `<sha-5.1>` floats iter 5.1: prose renders Float literals (finite + NaN/Inf)
- `<sha-5.2>` floats iter 5.2: DESIGN.md — Float in primitive types + refreshed builtins list
- `<sha-5.3>` floats iter 5.3: DESIGN.md — new §Float semantics subsection (A5 determinism contract)
- `<sha-5.4>` floats iter 5.4: roadmap — mark Floats [x] + unblock Post-22 Prelude
Replace each <sha-5.N> with the actual commit SHA at write time.
- Step 2: Append the milestone-close entry
In docs/JOURNAL.md, immediately AFTER the iter-5 close section,
append:
## 2026-05-10 — Milestone close: Floats
The Floats milestone is closed. `Float` is a fully supported
primitive type in AILang, end-to-end. `examples/floats.ail.json`
builds via `ail build`, runs, and produces predictable stdout —
the milestone-acceptance gate landed first try in iter 4.6.
**Five-iteration arc:**
- **Iter 1 (schema).** `Literal::Float { bits: u64 }` AST variant
with private `hex_u64` serde helper (16-char lowercase hex string
in canonical JSON; bypasses `serde_json::Number` for bit
stability + NaN/Inf representability). `Float` registered as
primitive name. 8 downstream `match Literal` sites wired with
permanent semantic arms or named-iteration `unimplemented!`
placeholders. Drift-test anchors added to `spec_drift.rs` /
`design_schema_drift.rs` / `form_a.md` / DESIGN.md JSON-schema
block. Bit-stability tests pin A1 / A5 properties.
- **Iter 2 (surface).** Lex `<digits>.<digits>(e[+-]?<digits>)?`
and `<digits>e[+-]?<digits>` per spec A2; reject bare leading /
trailing dots, missing fraction-after-dot, missing / sign-only
exponents. Parser accepts `Tok::Float` in atom + pat-lit
positions (typecheck rejects pat-lit Float in iter 3). Surface
print emits shortest-round-trippable decimal with `.0` suffix
fallback; non-finite bits panic per spec (cannot reach printer
via well-formed surface input). Round-trip property
`lex(print(L)) == L` pinned for 6 representative bit patterns.
- **Iter 3 (typecheck + builtins).** Widened `+`/`-`/`*`/`/` and
`!=`/`<`/`<=`/`>`/`>=` from monomorphic `(Int, Int) -> {Int, Bool}`
to polymorphic `forall a. (a, a) -> {a, Bool}` (same shape `==`
already had). `%` stays Int-only. 5 new builtins installed:
`neg` (poly), `int_to_float`, `float_to_int_truncate`,
`float_to_str`, `is_nan`. 3 bit-pattern constants installed as
bare-value globals: `nan`, `inf`, `neg_inf`. 1 new effect op:
`io/print_float`. `Pattern::Lit::Float` typecheck-rejected via
new `CheckError::FloatPatternNotAllowed`.
- **Iter 4 (codegen + E2E).** Float literals lower as LLVM hex-
float `double` constants. `synth::builtin_binop` replaced by
3-tuple-returning `builtin_binop_typed(name, &Type)` —
`fadd/fsub/fmul/fdiv double` and `fcmp olt/ole/ogt/oge/oeq/UNE
double` arms (note `UNE`, not `one`). `lower_eq` Float arm.
5 new fn-builtin lowering arms (`neg` poly with `fneg double`;
`int_to_float` `sitofp`; `float_to_int_truncate`
`@llvm.fptosi.sat.i64.f64`; `is_nan` `fcmp uno`; `float_to_str`
deferred via structured `CodegenError`). 3 Float constants
intercepted at `Term::Var` arm. `io/print_float` via inline
`printf("%g\n", v)`. `examples/floats.ail.json` E2E fixture +
`crates/ail/tests/floats_e2e.rs` E2E test — milestone gate.
- **Iter 5 (prose + DESIGN.md).** Prose renderer for Float
literals (finite + NaN/Inf). DESIGN.md §"Float semantics" + line-
2034 + builtins-list refresh. Roadmap mark + Prelude unblock.
**Key design decisions (rationale):**
- **One float type only — `f64`, named `Float`** (no `f32`). LLM
authors don't reach for `f32` unprompted; shipping both would
surface a per-op type-pun question that adds friction without
measurable correctness gain.
- **IEEE-conformant equality** — `==` returns `false` for `nan ==
nan`, no `Eq` instance for `Float` in the eventual prelude. The
partial-equality reality is real and surfaces through builtins
(`is_nan`, ordering ops returning `false` for NaN-involved
comparisons) rather than through a lying total typeclass
instance.
- **Polymorphic operators over `{Int, Float}` via codegen-dispatch**
— the same mechanism `==` already used. Mainstream alignment
beats the explicit-naming purity of the earlier `(fadd 1.5 2.5)`
draft. The hardcoded `{Int, Float}` filter is transitional;
cleanly replaceable by a `Num a` constraint when typeclasses
ship in 22c.
- **Form-A bit-pattern hex string** — bypasses `serde_json::Number`
(not bit-stable across versions for floats; cannot represent
NaN / ±Inf). Routing through the string path preserves bit-exact
determinism + lets non-finite Floats round-trip through
canonical JSON.
- **Pattern::Lit::Float hard-reject at typecheck** — IEEE semantics
make Float patterns semantically dubious (NaN never matches;
bit-exact equality is rarely the LLM-author's intent). Surface
lex / parser accept the syntax to keep the diagnostic at the
correct layer (typecheck, not parser).
- **`fcmp UNE` for `!=`, NOT `fcmp one`** — `one` is "ordered and
not equal" and returns false for `nan != nan`, violating IEEE-
`!=` and the user-fixed constraint #2. Caught during the
pre-implementation LLVM IR audit; documented in iter-4 codegen
arm + DESIGN.md §"Float semantics".
- **`fneg double` for `neg`, NOT `fsub double 0.0, x`** — IEEE
`0.0 - 0.0 = +0.0`, so the `fsub`-from-zero desugar would
wrongly produce `+0.0` for `neg(+0.0)` instead of `-0.0`. LLVM 8+
intrinsic; clang 22 always available.
- **`@llvm.fptosi.sat.i64.f64` for `float_to_int_truncate`** —
saturating fp-to-int intrinsic exactly matches Rust `as i64`
semantics (NaN → 0, ±Inf → i64::{MIN,MAX}, saturating). Total,
no Maybe wrapper.
**Process notes (orchestration lessons):**
- **Drift tests are part of the schema layer.** The original 5-iter
plan put DESIGN.md anchor edits in iter 5; iter 1's drift-test
break revealed they belong in iter 1 (where they get added
together with the AST variant they document). The exhaustive-
match drift tests are *the enforcement mechanism* of the schema
invariant — adding a new `Literal` variant without their anchors
IS a schema-layer break.
- **Plan inconsistencies caught by the reviewer chain.** Iter 4.2
shipped a plan that would have stranded comparison ops (Step 4
said `builtin_binop_typed` returns None for them; Step 5
narrowed the dispatch matches!). Implementer caught it; pulled
forward Task 3 Steps 3-4 as minimal repair; orchestrator
endorsed; Task 3's residual scope was narrowed accordingly. The
two-stage review (spec + quality) caught two more issues:
the dual-meaning `(instr, ll_ty)` 2-tuple → fixed via 3-tuple
return; the `unimplemented!()` panic for `float_to_str` →
fixed via structured `CodegenError::Internal`.
- **`is_static_callee` companion-edit.** Any new lowering arm in
`lower_app` MUST also be recognised by `is_static_callee`
(otherwise App dispatch falls through to indirect-call →
UnknownVar). This bit twice (iter 4.2 for `==`, iter 4.4 for
the 5 new fn-builtins). Both pulled forward as minimal repairs.
- **The `synth_term` test helper does not exist.** Iter 3 plan
named it; the actual fn is `crate::synth(...)` with 8 args. The
implementer flagged this; future plans should specify the
adapter pattern explicitly.
**Workspace at milestone close:** 405+ tests passing, 0 failed.
Iter-1-baseline rustdoc warnings preserved (1 warning on `desugar`
private link, pre-existing). `def_hash` regression hashes
(`db33f57cb329935e` for `sum.sum`, `b082192bd0c99202` for
`IntList`) preserved bit-identical — adding a Literal variant
does not perturb pre-existing canonical bytes.
**Known debt deferred beyond this milestone:**
- `float_to_str` codegen lowering — requires runtime-allocated Str
(the current Str path uses only static `@.str_*` globals;
no malloc-backed dynamic-Str infrastructure). Symbol is
type-installed and surface-callable, but lowering errors with a
structured `CodegenError::Internal`. Future milestone wires the
runtime Str-allocator path; revisit then.
- Float-typed container slot layout — mono pass produces
`List_Fl` etc. naturally per the iter-1 `type_descriptor` `Fl`
arm, but no current fixture exercises a Float-typed container.
Verify when a future use case surfaces.
- Pattern-matching on Float ranges (e.g. `(0.0..1.0)`) — out of
scope; would need a different Pattern variant entirely.
- Float-aware ADT layout for Float-only ADT fields — same
monomorphisation guarantee as `List<Float>` would extend; not
exercised by current corpus.
**Roadmap effects:** Floats `[x]`. Post-22 Prelude unblocked (was
`depends on: Floats`); the Prelude milestone can now ship `Show`
for `Float` and decide-against `Eq` / `Ord` for Float (both
reflecting the partial-equality reality settled in this milestone).
Next dispatch (orchestrator): `audit` skill — milestone-close
drift review + bench-regression diagnostics + rustdoc audit.
After audit closes clean: `fieldtest` skill — 2-4 `.ailx`
real-world examples exercising the new Float surface.
- Step 3: Commit
git add docs/JOURNAL.md
git commit -m "floats iter 5: JOURNAL — iter-5 close + Floats milestone close"
Iteration acceptance
cargo build --workspaceis clean.cargo test --workspaceis GREEN — pre-existing 402 tests stay GREEN; the 3 new prose tests PASS (= 405+).- No
unimplemented!("Floats milestone iter 5: prose")arm remains anywhere incrates/. - DESIGN.md §"Float semantics" present.
- DESIGN.md line 2034 lists Float as a supported primitive.
crates/ailang-core/tests/design_schema_drift.rsandtests/spec_drift.rsGREEN (the iter-1 anchors are unchanged).- Roadmap shows Floats
[x]; Post-22 Prelude has nodepends on: Floatsline. - JOURNAL has BOTH the iter-5 close entry AND the separate milestone-close summary.
- No file touched outside the four listed in the file map.
When all five tasks are committed and the acceptance checklist is
green, hand back to the orchestrator. The orchestrator dispatches
audit next (milestone-close drift review + bench regression),
followed by fieldtest (2-4 real-world .ailx examples).