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.
25 KiB
Floats Iteration 1 — Schema Layer (Implementation Plan)
Parent spec:
docs/specs/0005-floats.md(committede37366f, approved 2026-05-10)For agentic workers: REQUIRED SUB-SKILL: use
skills/implementto 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 privatehex_u64serde helper module,Literal::Float { #[serde(with = "hex_u64")] bits: u64 }variant,pub fn Type::float() -> Typeconstructor. - 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 thetestsmod. - Modify:
crates/ailang-core/src/primitives.rs— append"Float"to bothis_primitive_nameandprimitive_surface_name; extend thepredicate_and_surface_name_agreelockstep test name list to include"Float". - Modify:
crates/ailang-core/src/desugar.rs— extend thebuild_eqOR-pattern at line 1061 to includeLiteral::Float. - Modify:
crates/ailang-core/src/pretty.rs— addLiteral::Float { bits } => format!("(float-bits 0x{:016x})", bits)arm atlit_to_stringline 109. - Modify:
crates/ailang-check/src/lib.rs— addLiteral::Float { .. } => Type::float()arm at the twoLiteralmatch sites (lines 1697-1700 and 2307-2310). - Modify:
crates/ailang-codegen/src/lib.rs— addLiteral::Float { .. } => unimplemented!("Floats milestone iter 4: codegen")at the threeLiteralmatch sites (lines 918-928, 1215-1227, 2357-2361). - Modify:
crates/ailang-surface/src/print.rs— addLiteral::Float { .. } => unimplemented!("Floats milestone iter 2: surface print")at the twoLiteralmatch sites (lines 564-567 and 580-588). - Modify:
crates/ailang-prose/src/lib.rs— addLiteral::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 nearType::str_()at line 632 (Type::float() constructor) -
Modify:
crates/ailang-core/src/canonical.rs:105-135(tests mod) -
Modify: 8 downstream
Literalmatch 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:
/// 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_u64serde helper module toast.rs
In crates/ailang-core/src/ast.rs, before the Literal enum
declaration (around line 536), add:
/// 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::Floatvariant
In crates/ailang-core/src/ast.rs, replace the Literal enum
declaration:
/// 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:
/// 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:
Literal::Int { .. } | Literal::Bool { .. } | Literal::Str { .. } => Term::App {
with:
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:
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:
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):
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):
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):
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):
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:
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):
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:
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
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":
#[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:
// 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:
pub fn is_primitive_name(name: &str) -> bool {
matches!(name, "Int" | "Bool" | "Str" | "Unit" | "Float")
}
In crates/ailang-core/src/primitives.rs, line 24:
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
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.rstestsmod — 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:
/// 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
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
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:
### 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
git add docs/JOURNAL.md
git commit -m "floats iter 1: JOURNAL — iteration close"
Iteration acceptance
cargo build --workspaceis clean.cargo test --workspaceis GREEN (no pre-existing test regressed; the pre-existingdef_hashregression hashes stay bit-identical).cargo test -p ailang-core float_literal_canonical_bytesand the four bit-stability tests are GREEN.cargo test -p ailang-core predicate_and_surface_name_agreeis GREEN with"Float"in the recognised set.crates/ailang-core/src/canonical.rs:14no 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.