Files
AILang/docs/plans/0020-floats-5-prose-design.md
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
2026-05-28 13:31:31 +02:00

29 KiB

Floats Iteration 5 — Prose + DESIGN.md + Milestone Close (Implementation Plan)

Parent spec: docs/specs/0005-floats.md (committed e37366f, approved 2026-05-10) — section A5 (determinism contract) + the crates/ailang-prose/ Components subsection.

For agentic workers: REQUIRED SUB-SKILL: use skills/implement to 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 the Literal::Float { .. } => unimplemented!(...) arm at line 917 with a real renderer; add mod tests Float 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]; drop depends on: Floats from 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 helper write_float_lit(out, bits); add a mod tests block (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_lit helper + 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 → drop depends on: Floats line.

  • 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 --workspace is clean.
  • cargo test --workspace is GREEN — pre-existing 402 tests stay GREEN; the 3 new prose tests PASS (= 405+).
  • No unimplemented!("Floats milestone iter 5: prose") arm remains anywhere in crates/.
  • DESIGN.md §"Float semantics" present.
  • DESIGN.md line 2034 lists Float as a supported primitive.
  • crates/ailang-core/tests/design_schema_drift.rs and tests/spec_drift.rs GREEN (the iter-1 anchors are unchanged).
  • Roadmap shows Floats [x]; Post-22 Prelude has no depends on: Floats line.
  • 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).