# Floats Iteration 1 — Schema Layer (Implementation Plan)

> **Parent spec:** `docs/specs/2026-05-10-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.
