Files
AILang/docs/plans/0016-floats-1-schema.md
T
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

720 lines
25 KiB
Markdown

# Floats Iteration 1 — Schema Layer (Implementation Plan)
> **Parent spec:** `docs/specs/0005-floats.md` (committed
> `e37366f`, approved 2026-05-10)
>
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Add the `Literal::Float { bits: u64 }` variant to the AST,
serialise it as `{"bits":"<16-lowercase-hex>","kind":"float"}` in
canonical JSON, register `"Float"` in the primitive-name set, and
keep the workspace compiling by adding exhaustive `Literal::Float`
arms across all downstream `match Literal { … }` sites with
named-iteration `unimplemented!` placeholders where later iterations
own the semantics.
**Architecture:** The variant carries a `u64` for the IEEE-754
binary64 bit pattern. A private `hex_u64` serde helper module on
`ast.rs` routes the field through the JSON *string* path
(16-lowercase-hex), bypassing `serde_json::Number` so canonical
bytes stay bit-stable across `serde_json` versions and so NaN / ±Inf
are representable (they are not, as JSON numbers). Adding the
variant breaks Rust's enum exhaustiveness at eight downstream
`match Literal` sites — each gets either the right semantic arm
(typecheck → `Type::float()`) or a named-iteration
`unimplemented!("Floats iter N: <subsystem>")` arm.
**Tech Stack:** `ailang-core` (ast.rs, canonical.rs, primitives.rs,
desugar.rs, pretty.rs); compile-completeness arms in `ailang-check`,
`ailang-codegen`, `ailang-surface`, `ailang-prose`.
---
## Files this plan creates or modifies
- Modify: `crates/ailang-core/src/ast.rs` — add private `hex_u64`
serde helper module, `Literal::Float { #[serde(with = "hex_u64")]
bits: u64 }` variant, `pub fn Type::float() -> Type` constructor.
- Modify: `crates/ailang-core/src/canonical.rs` — refresh module-doc
"no floats" comment; add canonical-bytes RED test plus
bit-stability tests (-0/+0 distinct, NaN/Inf preserved, serde
round-trip) in the `tests` mod.
- Modify: `crates/ailang-core/src/primitives.rs` — append `"Float"`
to both `is_primitive_name` and `primitive_surface_name`; extend
the `predicate_and_surface_name_agree` lockstep test name list to
include `"Float"`.
- Modify: `crates/ailang-core/src/desugar.rs` — extend the
`build_eq` OR-pattern at line 1061 to include `Literal::Float`.
- Modify: `crates/ailang-core/src/pretty.rs` — add `Literal::Float
{ bits } => format!("(float-bits 0x{:016x})", bits)` arm at
`lit_to_string` line 109.
- Modify: `crates/ailang-check/src/lib.rs` — add `Literal::Float
{ .. } => Type::float()` arm at the two `Literal` match sites
(lines 1697-1700 and 2307-2310).
- Modify: `crates/ailang-codegen/src/lib.rs` — add `Literal::Float
{ .. } => unimplemented!("Floats milestone iter 4: codegen")` at
the three `Literal` match sites (lines 918-928, 1215-1227,
2357-2361).
- Modify: `crates/ailang-surface/src/print.rs` — add `Literal::Float
{ .. } => unimplemented!("Floats milestone iter 2: surface print")`
at the two `Literal` match sites (lines 564-567 and 580-588).
- Modify: `crates/ailang-prose/src/lib.rs` — add `Literal::Float
{ .. } => unimplemented!("Floats milestone iter 5: prose")` at
line 913-916.
- Modify: `docs/JOURNAL.md` — append iteration-close entry.
---
## Task 1: `Literal::Float` variant + canonical hex serialization
**Files:**
- Modify: `crates/ailang-core/src/ast.rs:540-551` (Literal enum) and
near `Type::str_()` at line 632 (Type::float() constructor)
- Modify: `crates/ailang-core/src/canonical.rs:105-135` (tests mod)
- Modify: 8 downstream `Literal` match sites for compile-completeness
(paths listed above)
- [ ] **Step 1: Write the RED canonical-bytes test**
In `crates/ailang-core/src/canonical.rs`, inside `mod tests`, append:
```rust
/// Iter 22-floats.1 RED: a `Literal::Float` carries the IEEE-754
/// binary64 bit pattern as a `u64`; canonical JSON encodes it as
/// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path,
/// NOT through `serde_json::Number` (which is not bit-stable for
/// floats and cannot represent NaN / ±Inf at all).
///
/// Pinned literal: `1.5_f64` → bit pattern `0x3FF8000000000000` →
/// hex string `"3ff8000000000000"`. Sorted-key order is
/// `bits` < `kind` lexicographically.
#[test]
fn float_literal_canonical_bytes() {
use crate::ast::Literal;
let lit = Literal::Float {
bits: 0x3ff8_0000_0000_0000u64,
};
let bytes = to_bytes(&lit);
let s = std::str::from_utf8(&bytes).unwrap();
assert_eq!(s, r#"{"bits":"3ff8000000000000","kind":"float"}"#);
}
```
- [ ] **Step 2: Run test to verify it fails (RED)**
Run: `cargo test -p ailang-core float_literal_canonical_bytes`
Expected: COMPILE FAIL with rustc error E0599 / E0432 / similar
naming `Literal::Float` (the variant does not yet exist).
- [ ] **Step 3: Add the `hex_u64` serde helper module to `ast.rs`**
In `crates/ailang-core/src/ast.rs`, before the `Literal` enum
declaration (around line 536), add:
```rust
/// Private serde helper: emits a `u64` as a 16-character lowercase
/// hex JSON string and parses the same shape back. The
/// 16-character invariant covers the full `u64` range zero-padded
/// (`format!("{:016x}", 0u64) == "0000000000000000"`,
/// `format!("{:016x}", u64::MAX) == "ffffffffffffffff"`). Used by
/// [`Literal::Float`] so float bit patterns flow through the
/// canonical-JSON *string* path — guaranteeing bit stability across
/// `serde_json` versions and surfacing NaN / ±Inf, which JSON
/// numbers cannot represent.
mod hex_u64 {
use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &u64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{:016x}", value))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
let s = <&str>::deserialize(d)?;
if s.len() != 16 {
return Err(D::Error::custom(format!(
"Float bits: expected 16 hex chars, got {}",
s.len()
)));
}
u64::from_str_radix(s, 16).map_err(D::Error::custom)
}
}
```
- [ ] **Step 4: Add the `Literal::Float` variant**
In `crates/ailang-core/src/ast.rs`, replace the `Literal` enum
declaration:
```rust
/// A literal value.
///
/// The JSON discriminator is the `kind` field. `Unit` carries no
/// payload and serializes as `{"kind":"unit"}`. `Float` carries a
/// `u64` IEEE-754 binary64 bit pattern, serialized via the
/// [`hex_u64`] helper as a 16-character lowercase hex string —
/// canonical-JSON bytes therefore stay bit-stable for hashing and
/// can represent NaN / ±Inf (which JSON numbers cannot).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Literal {
/// Signed 64-bit integer.
Int { value: i64 },
/// Boolean.
Bool { value: bool },
/// UTF-8 string.
Str { value: String },
/// The unit value `()`.
Unit,
/// IEEE-754 binary64 (LLVM `double`) carried as its bit pattern.
/// See [`hex_u64`] for the serialization rationale.
Float {
#[serde(with = "hex_u64")]
bits: u64,
},
}
```
- [ ] **Step 5: Add the `Type::float()` convenience constructor**
In `crates/ailang-core/src/ast.rs`, in the `impl Type { … }` block
(immediately after `pub fn str_()` at line 632), append:
```rust
/// Convenience constructor for `Float` (no args). IEEE-754
/// binary64. Parallel to [`Type::int`] / [`Type::bool_`].
pub fn float() -> Type {
Type::Con { name: "Float".into(), args: vec![] }
}
```
- [ ] **Step 6: Add compile-completeness arm in `desugar.rs`**
In `crates/ailang-core/src/desugar.rs`, line 1061, replace the
existing OR-pattern:
```rust
Literal::Int { .. } | Literal::Bool { .. } | Literal::Str { .. } => Term::App {
```
with:
```rust
Literal::Int { .. }
| Literal::Bool { .. }
| Literal::Str { .. }
| Literal::Float { .. } => Term::App {
```
- [ ] **Step 7: Add compile-completeness arm in `pretty.rs`**
In `crates/ailang-core/src/pretty.rs`, line 109's `lit_to_string`,
add the `Literal::Float` arm before the closing brace:
```rust
fn lit_to_string(l: &Literal) -> String {
match l {
Literal::Int { value } => value.to_string(),
Literal::Bool { value } => value.to_string(),
Literal::Str { value } => {
// serde_json escapes for us; the result is a valid
// JSON string literal, which is enough as our canonical form.
serde_json::to_string(value).unwrap()
}
Literal::Unit => "()".to_string(),
Literal::Float { bits } => format!("(float-bits 0x{:016x})", bits),
}
}
```
- [ ] **Step 8: Add compile-completeness arms in `crates/ailang-check/src/lib.rs`**
Two sites. At line 1697-1700:
```rust
Literal::Int { .. } => Type::int(),
Literal::Bool { .. } => Type::bool_(),
Literal::Str { .. } => Type::str_(),
Literal::Unit => Type::unit(),
Literal::Float { .. } => Type::float(),
```
At line 2307-2310 (same shape — typecheck literal → type):
```rust
Literal::Int { .. } => Type::int(),
Literal::Bool { .. } => Type::bool_(),
Literal::Str { .. } => Type::str_(),
Literal::Unit => Type::unit(),
Literal::Float { .. } => Type::float(),
```
- [ ] **Step 9: Add compile-completeness arms in `crates/ailang-codegen/src/lib.rs`**
Three sites. At line 918-928 (constant lowering):
```rust
let (val_ty, val) = match lit {
Literal::Int { value } => ("i64".to_string(), value.to_string()),
Literal::Bool { value } => (
"i1".to_string(),
if *value { "true".into() } else { "false".into() },
),
Literal::Unit => ("i8".to_string(), "0".to_string()),
Literal::Str { value } => {
let g = self.intern_string("str", value);
("ptr".to_string(), format!("@{g}"))
}
Literal::Float { .. } => unimplemented!("Floats milestone iter 4: codegen"),
};
```
At line 1215-1227 (term lowering):
```rust
Term::Lit { lit } => Ok(match lit {
Literal::Int { value } => (value.to_string(), "i64".into()),
Literal::Bool { value } => (
if *value { "true".into() } else { "false".into() },
"i1".into(),
),
Literal::Str { value } => {
// Create global constant; in opaque-pointer LLVM,
// `@name` is directly a valid `ptr`.
let g = self.intern_string("str", value);
(format!("@{g}"), "ptr".into())
}
Literal::Unit => ("0".into(), "i8".into()),
Literal::Float { .. } => unimplemented!("Floats milestone iter 4: codegen"),
}),
```
At line 2357-2361 (synth_with_extras → Type — semantic arm, not
placeholder):
```rust
Term::Lit { lit } => Ok(match lit {
Literal::Int { .. } => Type::int(),
Literal::Bool { .. } => Type::bool_(),
Literal::Str { .. } => Type::str_(),
Literal::Unit => Type::unit(),
Literal::Float { .. } => Type::float(),
}),
```
- [ ] **Step 10: Add compile-completeness arms in `crates/ailang-surface/src/print.rs`**
Two sites. At line 564-567:
```rust
fn write_lit(out: &mut String, lit: &Literal) {
match lit {
Literal::Int { value } => out.push_str(&value.to_string()),
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
Literal::Str { value } => write_string_lit(out, value),
Literal::Unit => out.push_str("(lit-unit)"),
Literal::Float { .. } => unimplemented!("Floats milestone iter 2: surface print"),
}
}
```
At line 580-588 (inside `write_pattern` Pattern::Lit arm):
```rust
match lit {
Literal::Int { value } => out.push_str(&value.to_string()),
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
Literal::Str { value } => write_string_lit(out, value),
Literal::Unit => {
// Unit is not a valid pat-lit; fall back to a bare unit-lit
// form. This is unreachable for any well-formed AST that
// came through the typechecker.
out.push_str("(lit-unit)");
}
Literal::Float { .. } => unimplemented!("Floats milestone iter 2: surface print"),
}
```
- [ ] **Step 11: Add compile-completeness arm in `crates/ailang-prose/src/lib.rs`**
At line 913-916:
```rust
Literal::Int { value } => out.push_str(&value.to_string()),
Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }),
Literal::Str { value } => write_string_lit(out, value),
Literal::Unit => out.push_str("()"),
Literal::Float { .. } => unimplemented!("Floats milestone iter 5: prose"),
```
- [ ] **Step 12: Verify the workspace builds**
Run: `cargo build --workspace`
Expected: clean build, zero new warnings.
- [ ] **Step 13: Verify the RED test now passes (GREEN)**
Run: `cargo test -p ailang-core float_literal_canonical_bytes`
Expected: PASS — exactly one test reported, GREEN.
- [ ] **Step 14: Verify no regression across the workspace**
Run: `cargo test --workspace`
Expected: all existing tests PASS. The pre-existing `def_hash`
regression tests in `crates/ailang-core/src/hash.rs` (which pin
`db33f57cb329935e` for `sum.sum` and `b082192bd0c99202` for
`IntList`) MUST stay GREEN — adding `Literal::Float` to the enum
must not change the canonical bytes of any pre-existing fixture
(neither `sum.ail.json` nor `list.ail.json` contains a Float
literal).
- [ ] **Step 15: Commit**
```bash
git add crates/ailang-core/src/ast.rs \
crates/ailang-core/src/canonical.rs \
crates/ailang-core/src/desugar.rs \
crates/ailang-core/src/pretty.rs \
crates/ailang-check/src/lib.rs \
crates/ailang-codegen/src/lib.rs \
crates/ailang-surface/src/print.rs \
crates/ailang-prose/src/lib.rs
git commit -m "floats iter 1.1: Literal::Float variant + canonical hex serialization"
```
---
## Task 2: Register `"Float"` as a primitive type name
**Files:**
- Modify: `crates/ailang-core/src/primitives.rs` (entire file is 52
LoC; both functions plus the lockstep test).
- [ ] **Step 1: Extend the lockstep test (RED)**
In `crates/ailang-core/src/primitives.rs`, modify the test name list
at line 44 to include `"Float"`:
```rust
#[test]
fn predicate_and_surface_name_agree() {
for name in ["Int", "Bool", "Str", "Unit", "Float", "List", "Foo", "", "int"] {
assert_eq!(
is_primitive_name(name),
primitive_surface_name(name).is_some(),
"lockstep violation for {name:?}"
);
}
}
```
- [ ] **Step 2: Run test to verify it fails (RED)**
Run: `cargo test -p ailang-core predicate_and_surface_name_agree`
Expected: FAIL with `lockstep violation for "Float"` —
`is_primitive_name("Float") == false` but the test now expects it
to be `true` AND requires `primitive_surface_name("Float")` to be
`Some(_)`. The exact assertion that fires: the `assert_eq!` line
(`false != true.is_some()` evaluates `false != false = false` —
wait, both are `false`, so this would not actually fail until we
also flip the test expectation; instead, the test design is "they
must AGREE", and since both are currently `false` for `"Float"`,
agreement still holds.
Therefore the RED test must be tightened: append a line that
*explicitly asserts* `"Float"` is recognised. Append after the
loop:
```rust
// Float is a primitive (Floats milestone, iter 1).
assert!(is_primitive_name("Float"), "Float must be a primitive");
assert_eq!(
primitive_surface_name("Float"),
Some("Float"),
"Float surface name must be \"Float\""
);
```
Re-run: `cargo test -p ailang-core predicate_and_surface_name_agree`
Expected: FAIL on `assert!(is_primitive_name("Float"))`.
- [ ] **Step 3: Add `"Float"` to both functions (GREEN)**
In `crates/ailang-core/src/primitives.rs`, line 17:
```rust
pub fn is_primitive_name(name: &str) -> bool {
matches!(name, "Int" | "Bool" | "Str" | "Unit" | "Float")
}
```
In `crates/ailang-core/src/primitives.rs`, line 24:
```rust
pub fn primitive_surface_name(name: &str) -> Option<&'static str> {
match name {
"Int" => Some("Int"),
"Bool" => Some("Bool"),
"Str" => Some("Str"),
"Unit" => Some("Unit"),
"Float" => Some("Float"),
_ => None,
}
}
```
- [ ] **Step 4: Verify the test now passes (GREEN)**
Run: `cargo test -p ailang-core predicate_and_surface_name_agree`
Expected: PASS.
Run also: `cargo test --workspace`
Expected: all green. (Adding a primitive name should not change any
existing fixture — no existing fixture references `"Float"` as a
type constructor.)
- [ ] **Step 5: Commit**
```bash
git add crates/ailang-core/src/primitives.rs
git commit -m "floats iter 1.2: register Float as a primitive type name"
```
---
## Task 3: Bit-stability tests for `Literal::Float`
**Files:**
- Modify: `crates/ailang-core/src/canonical.rs` `tests` mod —
append four tests pinning the spec's IEEE-bit-stability
guarantees (A1 + A5).
- [ ] **Step 1: Add the four bit-stability tests**
In `crates/ailang-core/src/canonical.rs`, inside `mod tests`,
append:
```rust
/// A5 guarantee: -0.0 and +0.0 are distinct bit patterns and
/// therefore distinct Form-A literals (they hash distinctly even
/// though IEEE-`==` will report them equal at the value level
/// when arithmetic comparison ships).
#[test]
fn negative_zero_and_positive_zero_serialise_distinctly() {
use crate::ast::Literal;
let pos = Literal::Float { bits: 0x0u64 };
let neg = Literal::Float { bits: 0x8000_0000_0000_0000u64 };
let pos_bytes = to_bytes(&pos);
let neg_bytes = to_bytes(&neg);
assert_ne!(pos_bytes, neg_bytes, "+0 and -0 must hash distinctly");
assert_eq!(
std::str::from_utf8(&pos_bytes).unwrap(),
r#"{"bits":"0000000000000000","kind":"float"}"#
);
assert_eq!(
std::str::from_utf8(&neg_bytes).unwrap(),
r#"{"bits":"8000000000000000","kind":"float"}"#
);
}
/// A1 guarantee: a NaN bit pattern survives canonicalisation
/// without collapse to JSON `null` (which is what the `serde_json`
/// number path does for non-finite floats — and the reason
/// `Literal::Float` routes through the *string* path).
#[test]
fn nan_bits_preserved() {
use crate::ast::Literal;
let qnan = Literal::Float { bits: 0x7ff8_0000_0000_0000u64 };
let bytes = to_bytes(&qnan);
assert_eq!(
std::str::from_utf8(&bytes).unwrap(),
r#"{"bits":"7ff8000000000000","kind":"float"}"#
);
}
/// A1 guarantee: ±Inf bit patterns survive canonicalisation —
/// JSON numbers cannot represent infinity at all, the string path
/// must.
#[test]
fn inf_bits_preserved() {
use crate::ast::Literal;
let pos_inf = Literal::Float { bits: 0x7ff0_0000_0000_0000u64 };
let neg_inf = Literal::Float { bits: 0xfff0_0000_0000_0000u64 };
assert_eq!(
std::str::from_utf8(&to_bytes(&pos_inf)).unwrap(),
r#"{"bits":"7ff0000000000000","kind":"float"}"#
);
assert_eq!(
std::str::from_utf8(&to_bytes(&neg_inf)).unwrap(),
r#"{"bits":"fff0000000000000","kind":"float"}"#
);
}
/// Round-trip: serialise then deserialise via `serde_json` — bits
/// preserved bit-for-bit.
#[test]
fn float_literal_serde_roundtrip() {
use crate::ast::Literal;
let cases = [
0x0u64,
0x8000_0000_0000_0000u64,
0x3ff8_0000_0000_0000u64, // 1.5
0x7ff8_0000_0000_0000u64, // qNaN
0x7ff0_0000_0000_0000u64, // +Inf
0xfff0_0000_0000_0000u64, // -Inf
0xffff_ffff_ffff_ffffu64, // saturated
];
for &bits in &cases {
let lit = Literal::Float { bits };
let json = serde_json::to_string(&lit).unwrap();
let back: Literal = serde_json::from_str(&json).unwrap();
match back {
Literal::Float { bits: got } => assert_eq!(
got, bits,
"round-trip lost bits for {:#018x}: json={}",
bits, json
),
other => panic!("expected Literal::Float, got {:?}", other),
}
}
}
```
- [ ] **Step 2: Verify all four tests PASS (no impl change needed)**
Run: `cargo test -p ailang-core --test '*' canonical -- \
negative_zero_and_positive_zero_serialise_distinctly \
nan_bits_preserved inf_bits_preserved \
float_literal_serde_roundtrip`
Or simpler:
Run: `cargo test -p ailang-core canonical::tests`
Expected: all canonical tests GREEN, including the four new ones.
- [ ] **Step 3: Commit**
```bash
git add crates/ailang-core/src/canonical.rs
git commit -m "floats iter 1.3: bit-stability tests for Literal::Float"
```
---
## Task 4: Refresh `canonical.rs` module-doc comment
**Files:**
- Modify: `crates/ailang-core/src/canonical.rs:13-14` (module-level
doc comment).
- [ ] **Step 1: Replace the "no floats" sentence**
In `crates/ailang-core/src/canonical.rs`, lines 12-14, replace:
```
//! number-formatting follows whatever `serde_json::Number::to_string`
//! does, which is fine for the AILang AST (integers and short strings;
//! no floats).
```
with:
```
//! Number formatting follows whatever `serde_json::Number::to_string`
//! does — used only for the AST integer literals. Float literals do
//! not go through the number path; [`crate::ast::Literal::Float`]
//! carries its IEEE-754 bit pattern as a `u64` and serialises via a
//! 16-character lowercase hex *string*, which the canonical writer
//! treats like any other string. The string path is what makes float
//! canonical bytes bit-stable across `serde_json` versions and what
//! lets NaN / ±Inf round-trip at all (they cannot, as JSON numbers).
```
- [ ] **Step 2: Verify rustdoc still builds**
Run: `cargo doc --no-deps -p ailang-core 2>&1 | head -40`
Expected: no NEW warnings on `canonical.rs` (the module's
pre-existing warning baseline must not increase).
- [ ] **Step 3: Commit**
```bash
git add crates/ailang-core/src/canonical.rs
git commit -m "floats iter 1.4: refresh canonical.rs 'no floats' doc comment"
```
---
## Task 5: Iteration close — JOURNAL entry
**Files:**
- Modify: `docs/JOURNAL.md` — append entry at top under today's
date heading.
- [ ] **Step 1: Append the iteration-close entry**
In `docs/JOURNAL.md`, append a new section under today's date:
```markdown
### Floats milestone — iter 1: schema layer
Added `Literal::Float { bits: u64 }` as the fifth `Literal` variant.
Bits are an IEEE-754 binary64 bit pattern, serialised as a
16-character lowercase hex *string* (`{"bits":"<hex>","kind":"float"}`)
via a private `hex_u64` serde helper on `ast.rs`. Routing through
the JSON string path bypasses `serde_json::Number::to_string` (not
bit-stable across `serde_json` versions for floats) and lets NaN /
±Inf survive canonicalisation at all (they collapse to `null` as
JSON numbers).
Adding the variant broke Rust enum exhaustiveness at eight
downstream `match Literal { … }` sites. Each got either the
permanent semantic arm (`ailang-check` typecheck:
`Literal::Float { .. } => Type::float()`) or a named-iteration
`unimplemented!("Floats milestone iter N: <subsystem>")` arm
(`ailang-codegen` → iter 4, `ailang-surface` print → iter 2,
`ailang-prose` → iter 5). The arms are honest about which
iteration owns the semantics.
`Float` is now registered as a primitive type name in
`primitives.rs`; the lockstep test is extended to cover it.
The four bit-stability tests pin the A1 / A5 spec guarantees:
`-0` ≠ `+0` at the canonical-bytes level, NaN bits preserved,
±Inf bits preserved, serde round-trip is bit-exact for the
saturating boundary values.
Pre-existing `def_hash` regression hashes (`db33f57cb329935e` /
`b082192bd0c99202`) stay GREEN — adding a Literal variant does
not perturb any pre-existing canonical bytes.
```
- [ ] **Step 2: Commit**
```bash
git add docs/JOURNAL.md
git commit -m "floats iter 1: JOURNAL — iteration close"
```
---
## Iteration acceptance
- [ ] `cargo build --workspace` is clean.
- [ ] `cargo test --workspace` is GREEN (no pre-existing test
regressed; the pre-existing `def_hash` regression hashes
stay bit-identical).
- [ ] `cargo test -p ailang-core float_literal_canonical_bytes` and
the four bit-stability tests are GREEN.
- [ ] `cargo test -p ailang-core predicate_and_surface_name_agree`
is GREEN with `"Float"` in the recognised set.
- [ ] `crates/ailang-core/src/canonical.rs:14` no longer says
"no floats".
- [ ] JOURNAL entry committed.
- [ ] No file touched outside the nine listed in the file map above.
When all five tasks are committed and the acceptance checklist is
green, hand back to the orchestrator. Iteration 2 (Surface lex /
parse / print round-trip) is the next dispatch.