# Floats Iteration 4 — Codegen + Runtime (Implementation Plan) > **Parent spec:** `docs/specs/0005-floats.md` (committed > `e37366f`, approved 2026-05-10) — sections A3 (codegen-side > dispatch), A4 (conversions), A5 (determinism), and the > `crates/ailang-codegen/` + `runtime/` Components subsections. > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Lower every Float-related typecheck primitive that iter 3 installed into LLVM IR. Float literals → hex-float `double` SSA constants. Arithmetic and comparison ops dispatch on the resolved arg type — Int → existing `add i64` / `icmp slt`, Float → new `fadd double` / `fcmp olt double` / **`fcmp une double` for `!=`**. Conversions (`int_to_float`, `float_to_int_truncate`) lower via `sitofp` and the `@llvm.fptosi.sat.i64.f64` intrinsic. `is_nan` lowers via `fcmp uno double %x, %x`. Float constants `nan` / `inf` / `neg_inf` resolve to direct hex-float `double` SSA values at use site. `io/print_float` lowers via `printf("%g\n", v)` inline, parallel to `io/print_int`. The end-to-end fixture `examples/floats.ail.json` exercises literal, arithmetic, comparison, conversion, and `io/print_float` in a single program that builds via `ail build` and produces predicted stdout. `float_to_str` is deferred to iter 5+ because it requires runtime-allocated Str — AILang's Str is currently only static `@.str_*` globals (no malloc-backed dynamic str path). The codegen arm gets a named-iteration `unimplemented!("Floats milestone iter 5+: float_to_str needs dynamic Str allocation")`. **Architecture:** `synth::builtin_binop` becomes type-dispatched — either replaced by a new helper `builtin_binop_typed(name, &Type)` returning the right `(instruction, llvm_type)` pair, or extended in-place. The `lower_app` site at lib.rs:1737 calls `synth_arg_type` (parallel to how `==` dispatch already works at line 1729-1733) to resolve the arg type, then dispatches. Float constant-`Var` resolution intercepts at `lower_term`'s `Term::Var` arm (lib.rs:1228) before the global-lookup fallback. The runtime glue is **inline `printf` + libc `puts`-style** — no new C file needed for `io/print_float`. **Tech Stack:** `crates/ailang-codegen/` (`lib.rs`, `synth.rs`), `examples/floats.ail.json` (new fixture), `crates/ail/tests/` (new E2E test). No runtime C file changes. **No-regression invariant:** every existing Int-using `examples/*.ail.json` end-to-end test stays GREEN. The `add i64` regression test at lib.rs:2627-2633 stays GREEN. The 395 workspace tests at iter-3 close stay GREEN. --- ## Files this plan creates or modifies - Modify: `crates/ailang-codegen/src/synth.rs` — `llvm_type` add `"Float" => "double"`, `type_descriptor` add `"Float" => "Fl"`, `builtin_binop` either becomes type-dispatched or is replaced by `builtin_binop_typed(name, &Type)` (Task 2 picks the shape). - Modify: `crates/ailang-codegen/src/lib.rs` — Float literal lowering (2 sites at line 918/1215 currently `unimplemented!`), type-dispatched arithmetic / comparison (line 1737), `lower_eq` Float arm (line 2263), Float constant `Var`-intercept (line ~1228), 4 new fn-builtin lowering arms, `io/print_float` effect-op arm (line 2152 region), `float_to_str` named-iteration `unimplemented!`. - Create: `examples/floats.ail.json` — E2E fixture exercising literal + arithmetic + comparison + conversion + io/print_float. - Create: `crates/ail/tests/floats_e2e.rs` — `ail build` + run + stdout-check for the new fixture. - Modify: `docs/JOURNAL.md` — append iteration-close entry. No file outside this list is touched. --- ## Task 1: Codegen primitive registration + Float literal lowering **Files:** - Modify: `crates/ailang-codegen/src/synth.rs` (`llvm_type` line ~14, `type_descriptor` line ~129). - Modify: `crates/ailang-codegen/src/lib.rs` lines 918 + 1215 (replace 2 `unimplemented!("Floats milestone iter 4: codegen")` arms). - [ ] **Step 1: Write the RED codegen test for Float literal lowering** In `crates/ailang-codegen/src/lib.rs`, inside `mod tests` (around line 2576), append: ```rust /// Floats iter 4.1 RED: a `Literal::Float { bits: 0x3ff8_0000_0000_0000 }` /// (= `1.5_f64`) lowers in a `Const` definition as an LLVM hex-float /// `double` SSA constant. The exact IR snippet pinned: `@ail_t_k = /// constant double 0x3FF8000000000000`. #[test] fn lowers_float_const_to_hex_double() { use ailang_core::ast::*; let m = Module { schema: ailang_core::SCHEMA.to_string(), name: "t".into(), imports: vec![], defs: vec![ Def::Const(ConstDef { name: "k".into(), ty: Type::float(), value: Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }, doc: None, }), Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::unit()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, }), ], }; let ir = emit_ir(&m).unwrap(); assert!( ir.contains("@ail_t_k = constant double 0x3FF8000000000000"), "ir missing the Float literal lowering: {ir}" ); } ``` - [ ] **Step 2: Run test to verify RED** Run: `cargo test -p ailang-codegen --lib lowers_float_const_to_hex_double` Expected: PANIC with the iter-1 `unimplemented!("Floats milestone iter 4: codegen")` message originating from `crates/ailang-codegen/ src/lib.rs:928`. - [ ] **Step 3: Add `"Float"` to `synth.rs::llvm_type`** In `crates/ailang-codegen/src/synth.rs`, find `fn llvm_type` (around line 14) and add a `"Float"` arm parallel to existing primitives: Read the function body first; the existing structure is `match name { "Int" => "i64", ... }`. Add `"Float" => "double"` to that match. - [ ] **Step 4: Add `"Float"` to `synth.rs::type_descriptor`** In `synth.rs::type_descriptor` (line ~129), add `"Float" => "Fl"` arm. Two letters because single-letter `F` collides with the ADT-name prefix convention (`F`); `Fl` is unambiguous. - [ ] **Step 5: Replace `unimplemented!` arm at lib.rs:918 (constant lowering)** In `crates/ailang-codegen/src/lib.rs`, line 928, replace: ```rust Literal::Float { .. } => unimplemented!("Floats milestone iter 4: codegen"), ``` with: ```rust Literal::Float { bits } => ("double".to_string(), format!("0x{:016X}", bits)), ``` (The match returns `(val_ty, val)` — `("double", "0x...")` is the right shape for the constant lowering site.) - [ ] **Step 6: Replace `unimplemented!` arm at lib.rs:1215 (term lowering)** In `crates/ailang-codegen/src/lib.rs`, line 1228, replace: ```rust Literal::Float { .. } => unimplemented!("Floats milestone iter 4: codegen"), ``` with: ```rust Literal::Float { bits } => (format!("0x{:016X}", bits), "double".into()), ``` (The match returns `(value, llvm_ty)` — note the field-order swap relative to the Step 5 site, parallel to how `Literal::Int` at line 1215 is `(value, "i64")` not `("i64", value)`.) - [ ] **Step 7: Verify GREEN** Run: `cargo test -p ailang-codegen --lib lowers_float_const_to_hex_double` Expected: PASS. Run: `cargo test --workspace` Expected: 396+ passed (= 395 prior + 1 new), 0 failed. - [ ] **Step 8: Commit** ```bash git add crates/ailang-codegen/src/synth.rs crates/ailang-codegen/src/lib.rs git commit -m "floats iter 4.1: codegen primitive registration + Float literal hex-double lowering" ``` --- ## Task 2: Codegen widening of arithmetic ops (`+`/`-`/`*`/`/`) **Files:** - Modify: `crates/ailang-codegen/src/synth.rs::builtin_binop` (line ~167) — convert from monomorphic-Int table to type-dispatched, OR add a new sibling fn `builtin_binop_typed` and route `lower_app` through it. - Modify: `crates/ailang-codegen/src/lib.rs::lower_app` (line 1737) — pass the resolved arg type to the new type-dispatched helper. - [ ] **Step 1: Inspect current `builtin_binop` shape** Run: `grep -A 20 "fn builtin_binop" crates/ailang-codegen/src/synth.rs | head -25` Expected: a match returning `Option<(&'static str, &'static str)>` (instruction + return-llvm-type) keyed on operator name. The current Int hardcoding is `"+" => Some(("add", "i64"))`, etc. - [ ] **Step 2: Write the RED test for Float arithmetic lowering** In `crates/ailang-codegen/src/lib.rs::tests`, append: ```rust /// Floats iter 4.2 RED: `(+ 1.5 2.5)` lowers as `fadd double`, /// not `add i64`. The Int regression `(+ 1 2)` still lowers as /// `add i64`. Both lowerings live in one IR for one workspace /// build. #[test] fn lowers_float_arithmetic_dispatched() { use ailang_core::ast::*; fn fn_def(name: &str, body: Term, ret_ty: Type) -> Def { Def::Fn(FnDef { name: name.into(), ty: Type::Fn { params: vec![], ret: Box::new(ret_ty), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, }) } let plus_int = Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Lit { lit: Literal::Int { value: 1 } }, Term::Lit { lit: Literal::Int { value: 2 } }, ], tail: false, }; let plus_float = Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }, Term::Lit { lit: Literal::Float { bits: 0x4004_0000_0000_0000u64 } }, ], tail: false, }; let m = Module { schema: ailang_core::SCHEMA.to_string(), name: "t".into(), imports: vec![], defs: vec![ fn_def("ai", plus_int, Type::int()), fn_def("af", plus_float, Type::float()), fn_def("main", Term::Lit { lit: Literal::Unit }, Type::unit()), ], }; let ir = emit_ir(&m).unwrap(); assert!( ir.contains("add i64"), "Int arithmetic regressed (no `add i64` in IR): {ir}" ); assert!( ir.contains("fadd double"), "Float arithmetic missing (no `fadd double` in IR): {ir}" ); } ``` - [ ] **Step 3: Verify RED** Run: `cargo test -p ailang-codegen --lib lowers_float_arithmetic_dispatched` Expected: FAIL — `(+ 1.5 2.5)` lowers as `add i64 0x3ff8..., 0x4004...` (wrong instruction, wrong type) because the current `builtin_binop` returns hardcoded `i64` regardless of arg type. The assertion on `fadd double` fires. - [ ] **Step 4: Convert `builtin_binop` to type-dispatched** In `crates/ailang-codegen/src/synth.rs`, replace `fn builtin_binop` (read the current body to find the exact match arms first). The new shape: ```rust /// Floats iter 4.2: arithmetic / comparison ops are type-dispatched /// over `{Int, Float}`. Caller (`lower_app`) resolves the arg type /// via `synth_arg_type` and passes it here. Returns the /// `(instruction, llvm_type)` pair to emit. `%` stays Int-only. pub(crate) fn builtin_binop_typed(name: &str, arg_ty: &Type) -> Option<(&'static str, &'static str)> { let is_int = matches!(arg_ty, Type::Con { name, .. } if name == "Int"); let is_float = matches!(arg_ty, Type::Con { name, .. } if name == "Float"); match (name, is_int, is_float) { ("+", true, _) => Some(("add", "i64")), ("+", _, true) => Some(("fadd", "double")), ("-", true, _) => Some(("sub", "i64")), ("-", _, true) => Some(("fsub", "double")), ("*", true, _) => Some(("mul", "i64")), ("*", _, true) => Some(("fmul", "double")), ("/", true, _) => Some(("sdiv", "i64")), ("/", _, true) => Some(("fdiv", "double")), ("%", true, _) => Some(("srem", "i64")), // Comparison ops are wired in Task 3 — return None here so // `lower_app`'s comparison branch handles them. (Or merge // both passes into a single switch in Task 3.) _ => None, } } ``` KEEP the original `builtin_binop(name)` fn IF other call sites depend on it (search via `grep -rn "builtin_binop" crates/`). If the only consumer is `lower_app` at lib.rs:1737, REMOVE the old fn and migrate the import. - [ ] **Step 5: Update `lower_app` at lib.rs:1737 to dispatch on arg type** In `crates/ailang-codegen/src/lib.rs`, line 1737-1755, replace: ```rust if let Some((instr, ret_ty)) = builtin_binop(name) { if args.len() != 2 { return Err(CodegenError::Internal(format!( "builtin `{name}` expected 2 args" ))); } let (a, _) = self.lower_term(&args[0])?; let (b, _) = self.lower_term(&args[1])?; let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = {instr} i64 {a}, {b}\n" )); let _ = tail; return Ok((dst, ret_ty.into())); } ``` with: ```rust // Floats iter 4.2: arithmetic / comparison are now polymorphic // over `{Int, Float}`. Resolve the arg type, then dispatch via // `builtin_binop_typed`. Same shape as the `==` dispatch above. if matches!(name, "+" | "-" | "*" | "/" | "%") { if args.len() != 2 { return Err(CodegenError::Internal(format!( "builtin `{name}` expected 2 args" ))); } let arg_ty = self.synth_arg_type(&args[0])?; let (instr, ll_ty) = builtin_binop_typed(name, &arg_ty) .ok_or_else(|| CodegenError::Internal(format!( "`{name}` not supported for type `{}`", ailang_core::pretty::type_to_string(&arg_ty) )))?; let (a, _) = self.lower_term(&args[0])?; let (b, _) = self.lower_term(&args[1])?; let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = {instr} {ll_ty} {a}, {b}\n" )); let _ = tail; return Ok((dst, ll_ty.into())); } ``` Also update the second `builtin_binop` reference at line 2076 (`if builtin_binop(name).is_some() || name == "not"`) to: ```rust if matches!(name, "+" | "-" | "*" | "/" | "%") || name == "not" { ``` Also update the imports at line 56 to drop `builtin_binop` (or keep both imports if the old fn is retained). - [ ] **Step 6: Verify the existing `add i64` regression test stays GREEN** Run: `cargo test -p ailang-codegen --lib` Expected: the existing `lib::tests::lowers_t_add_int` (or however the test at line 2627-2633 is named) stays GREEN — `add i64 %arg_a, %arg_b` is still emitted for the Int-arg `+`. - [ ] **Step 7: Verify the new RED test now GREEN** Run: `cargo test -p ailang-codegen --lib lowers_float_arithmetic_dispatched` Expected: PASS. - [ ] **Step 8: Verify no workspace regression** Run: `cargo test --workspace` Expected: 397+ passed, 0 failed. - [ ] **Step 9: Commit** ```bash git add crates/ailang-codegen/src/synth.rs crates/ailang-codegen/src/lib.rs git commit -m "floats iter 4.2: codegen arithmetic dispatch on arg type — fadd/fsub/fmul/fdiv double" ``` --- ## Task 3: Codegen widening of comparison ops + `==`/`!=` Float arms **Files:** - Modify: `crates/ailang-codegen/src/synth.rs::builtin_binop_typed` (extend with comparison arms). - Modify: `crates/ailang-codegen/src/lib.rs::lower_app` (extend the dispatch block from Task 2 to cover comparison ops). - Modify: `crates/ailang-codegen/src/lib.rs::lower_eq` (line 2263) — add `"Float"` arm. - [ ] **Step 1: Write the RED test for Float comparison + `!=`** In `crates/ailang-codegen/src/lib.rs::tests`, append: ```rust /// Floats iter 4.3 RED: `(< 1.5 2.5)` lowers as `fcmp olt double`; /// `(!= 1.5 1.5)` lowers as `fcmp UNE double` (NOT `one`); `(== 1.5 /// 1.5)` lowers as `fcmp oeq double`. Int regressions still emit /// `icmp slt i64` / `icmp ne i64` / `icmp eq i64`. #[test] fn lowers_float_comparison_dispatched() { use ailang_core::ast::*; fn fn_def(name: &str, body: Term) -> Def { Def::Fn(FnDef { name: name.into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::bool_()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, }) } fn cmp(op: &str, a: Term, b: Term) -> Term { Term::App { callee: Box::new(Term::Var { name: op.into() }), args: vec![a, b], tail: false, } } let f1 = Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }; let f2 = Term::Lit { lit: Literal::Float { bits: 0x4004_0000_0000_0000u64 } }; let i1 = Term::Lit { lit: Literal::Int { value: 1 } }; let i2 = Term::Lit { lit: Literal::Int { value: 2 } }; let main_def = Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::unit()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, }); let m = Module { schema: ailang_core::SCHEMA.to_string(), name: "t".into(), imports: vec![], defs: vec![ fn_def("flt_f", cmp("<", f1.clone(), f2.clone())), fn_def("fne_f", cmp("!=", f1.clone(), f1.clone())), fn_def("feq_f", cmp("==", f1.clone(), f1.clone())), fn_def("flt_i", cmp("<", i1.clone(), i2.clone())), fn_def("fne_i", cmp("!=", i1.clone(), i2.clone())), fn_def("feq_i", cmp("==", i1.clone(), i2.clone())), main_def, ], }; let ir = emit_ir(&m).unwrap(); assert!(ir.contains("fcmp olt double"), "missing `fcmp olt double`: {ir}"); assert!(ir.contains("fcmp une double"), "missing `fcmp une double` (note: `une` not `one`): {ir}"); assert!(ir.contains("fcmp oeq double"), "missing `fcmp oeq double`: {ir}"); assert!(ir.contains("icmp slt i64"), "Int `<` regressed: {ir}"); assert!(ir.contains("icmp ne i64"), "Int `!=` regressed: {ir}"); assert!(ir.contains("icmp eq i64"), "Int `==` regressed: {ir}"); } ``` - [ ] **Step 2: Verify RED** Run: `cargo test -p ailang-codegen --lib lowers_float_comparison_dispatched` Expected: FAIL — Float `<`/`!=` currently fall through (Task 2's arithmetic-only dispatch returned None for them, leading to a fallback path that doesn't recognise them). - [ ] **Step 3: Extend `builtin_binop_typed` with comparison arms** In `crates/ailang-codegen/src/synth.rs::builtin_binop_typed`, extend the match: ```rust // Comparison ops (Floats iter 4.3). Note: `!=` Float uses // `fcmp une` ("unordered or not equal") — NOT `one` ("ordered // and not equal"), which would return `false` for `nan != nan` // and violate IEEE / spec A5. ("!=", true, _) => Some(("icmp ne", "i64")), ("!=", _, true) => Some(("fcmp une", "double")), ("<", true, _) => Some(("icmp slt", "i64")), ("<", _, true) => Some(("fcmp olt", "double")), ("<=", true, _) => Some(("icmp sle", "i64")), ("<=", _, true) => Some(("fcmp ole", "double")), (">", true, _) => Some(("icmp sgt", "i64")), (">", _, true) => Some(("fcmp ogt", "double")), (">=", true, _) => Some(("icmp sge", "i64")), (">=", _, true) => Some(("fcmp oge", "double")), ``` For comparison ops, the "return type" in the emitted instruction is the LLVM type of the OPERANDS (not the result, which is always `i1`). The tuple `(instr, ll_ty)` therefore carries the operand type for the format-string; the result is always `i1`. The caller must override the return-llvm-type to `"i1"` for comparison ops. - [ ] **Step 4: Update `lower_app` dispatch to cover comparison ops** In `crates/ailang-codegen/src/lib.rs`, extend the match-pattern from Task 2's Step 5 to include comparison ops, AND fix the result type: ```rust if matches!(name, "+" | "-" | "*" | "/" | "%" | "!=" | "<" | "<=" | ">" | ">=") { if args.len() != 2 { return Err(CodegenError::Internal(format!( "builtin `{name}` expected 2 args" ))); } let arg_ty = self.synth_arg_type(&args[0])?; let (instr, ll_ty) = builtin_binop_typed(name, &arg_ty) .ok_or_else(|| CodegenError::Internal(format!( "`{name}` not supported for type `{}`", ailang_core::pretty::type_to_string(&arg_ty) )))?; let (a, _) = self.lower_term(&args[0])?; let (b, _) = self.lower_term(&args[1])?; let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = {instr} {ll_ty} {a}, {b}\n" )); let _ = tail; // Comparison ops return i1; arithmetic returns the operand type. let result_ll_ty = if matches!(name, "!=" | "<" | "<=" | ">" | ">=") { "i1".into() } else { ll_ty.into() }; return Ok((dst, result_ll_ty)); } ``` Also extend the line-2076 reference identically: ```rust if matches!(name, "+" | "-" | "*" | "/" | "%" | "!=" | "<" | "<=" | ">" | ">=") || name == "not" { ``` - [ ] **Step 5: Add Float arm in `lower_eq`** In `crates/ailang-codegen/src/lib.rs::lower_eq` (line 2263), add a `"Float"` arm to the existing `Type::Con { name }` match: ```rust "Float" => { let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = fcmp oeq double {a}, {b}\n" )); Ok((dst, "i1".into())) } ``` Place it immediately after the `"Unit"` arm and before the fallback `other => Err(...)`. - [ ] **Step 6: Verify GREEN** Run: `cargo test -p ailang-codegen --lib lowers_float_comparison_dispatched` Expected: PASS. Run: `cargo test --workspace` Expected: 398+ passed, 0 failed. - [ ] **Step 7: Commit** ```bash git add crates/ailang-codegen/src/synth.rs crates/ailang-codegen/src/lib.rs git commit -m "floats iter 4.3: codegen comparison dispatch — fcmp olt/ole/ogt/oge/oeq/UNE double" ``` --- ## Task 4: Codegen new fn builtins (`neg`, `int_to_float`, `float_to_int_truncate`, `is_nan`) + `float_to_str` deferral **Files:** - Modify: `crates/ailang-codegen/src/lib.rs::lower_app` — add 4 new arms (one per builtin) plus a `float_to_str => unimplemented!()` deferral marker. - Modify: `crates/ailang-codegen/src/lib.rs` IR-header section (line ~461 where `@strcmp` and `@printf` are declared) — declare `@llvm.fptosi.sat.i64.f64`. - [ ] **Step 1: Write the RED test for the four lowerings** In `crates/ailang-codegen/src/lib.rs::tests`, append: ```rust /// Floats iter 4.4 RED: four new fn-builtins lower to the spec'd /// LLVM ops. `neg` polymorphic dispatches to `sub i64 0, %x` for /// Int and `fneg double %x` for Float (NOT `fsub 0.0, %x`, which /// is wrong for `-0.0`). `int_to_float` → `sitofp`. `is_nan` → /// `fcmp uno double %x, %x`. `float_to_int_truncate` → /// `@llvm.fptosi.sat.i64.f64` intrinsic call. #[test] fn lowers_float_fn_builtins() { use ailang_core::ast::*; fn fn_def(name: &str, body: Term, ret_ty: Type) -> Def { Def::Fn(FnDef { name: name.into(), ty: Type::Fn { params: vec![], ret: Box::new(ret_ty), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, }) } fn app1(callee: &str, arg: Term) -> Term { Term::App { callee: Box::new(Term::Var { name: callee.into() }), args: vec![arg], tail: false, } } let neg_int = app1("neg", Term::Lit { lit: Literal::Int { value: 5 } }); let neg_float = app1("neg", Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }); let i2f = app1("int_to_float", Term::Lit { lit: Literal::Int { value: 5 } }); let f2i = app1("float_to_int_truncate", Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }); let isnan = app1("is_nan", Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }); let main_def = fn_def("main", Term::Lit { lit: Literal::Unit }, Type::unit()); let m = Module { schema: ailang_core::SCHEMA.to_string(), name: "t".into(), imports: vec![], defs: vec![ fn_def("ni", neg_int, Type::int()), fn_def("nf", neg_float, Type::float()), fn_def("c1", i2f, Type::float()), fn_def("c2", f2i, Type::int()), fn_def("isn", isnan, Type::bool_()), main_def, ], }; let ir = emit_ir(&m).unwrap(); assert!(ir.contains("sub i64 0,"), "neg Int missing: {ir}"); assert!(ir.contains("fneg double"), "neg Float missing (must use fneg, not fsub-from-zero): {ir}"); assert!(ir.contains("sitofp i64"), "int_to_float missing: {ir}"); assert!(ir.contains("@llvm.fptosi.sat.i64.f64"), "float_to_int_truncate intrinsic missing: {ir}"); assert!(ir.contains("fcmp uno double"), "is_nan missing (must be fcmp uno x, x): {ir}"); } ``` - [ ] **Step 2: Verify RED** Run: `cargo test -p ailang-codegen --lib lowers_float_fn_builtins` Expected: FAIL — first failing assertion will be `neg Int missing` or similar (none of the 4 builtins have lowering arms yet). - [ ] **Step 3: Declare the `@llvm.fptosi.sat.i64.f64` intrinsic** In `crates/ailang-codegen/src/lib.rs` around line 461 where `@strcmp` and `@printf` are declared, ADD: ```rust // Floats iter 4.4: saturating fp-to-int intrinsic for // float_to_int_truncate. NaN → 0, +Inf → i64::MAX, -Inf → // i64::MIN, finite-out-of-range saturates, finite-in-range // truncates toward zero. LLVM 12+, always available with // clang 22. out.push_str("declare i64 @llvm.fptosi.sat.i64.f64(double)\n\n"); ``` - [ ] **Step 4: Add the 5 lowering arms in `lower_app`** In `crates/ailang-codegen/src/lib.rs::lower_app`, add (location: after the existing comparison/arithmetic dispatch from Tasks 2-3, before the cross-module call dispatch at line 1769): ```rust // Floats iter 4.4: polymorphic neg + 3 monomorphic fn builtins. if name == "neg" { if args.len() != 1 { return Err(CodegenError::Internal("neg arity".into())); } let arg_ty = self.synth_arg_type(&args[0])?; let (a, _) = self.lower_term(&args[0])?; let dst = self.fresh_ssa(); match &arg_ty { Type::Con { name, .. } if name == "Int" => { self.body.push_str(&format!(" {dst} = sub i64 0, {a}\n")); return Ok((dst, "i64".into())); } Type::Con { name, .. } if name == "Float" => { // LLVM 8+ `fneg` correctly handles -0.0. self.body.push_str(&format!(" {dst} = fneg double {a}\n")); return Ok((dst, "double".into())); } other => return Err(CodegenError::Internal(format!( "`neg` not supported for type `{}`", ailang_core::pretty::type_to_string(other) ))), } } if name == "int_to_float" { if args.len() != 1 { return Err(CodegenError::Internal("int_to_float arity".into())); } let (a, _) = self.lower_term(&args[0])?; let dst = self.fresh_ssa(); self.body.push_str(&format!(" {dst} = sitofp i64 {a} to double\n")); return Ok((dst, "double".into())); } if name == "float_to_int_truncate" { if args.len() != 1 { return Err(CodegenError::Internal("float_to_int_truncate arity".into())); } let (a, _) = self.lower_term(&args[0])?; let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = call i64 @llvm.fptosi.sat.i64.f64(double {a})\n" )); return Ok((dst, "i64".into())); } if name == "is_nan" { if args.len() != 1 { return Err(CodegenError::Internal("is_nan arity".into())); } let (a, _) = self.lower_term(&args[0])?; let dst = self.fresh_ssa(); // `fcmp uno x, x` returns `i1 1` iff `x` is NaN — only // NaN compares unordered against itself. self.body.push_str(&format!(" {dst} = fcmp uno double {a}, {a}\n")); return Ok((dst, "i1".into())); } if name == "float_to_str" { // Deferred to iter 5+: needs runtime-allocated Str // (currently the codegen Str path uses only static // `@.str_*` globals; no malloc-backed dynamic-Str // infrastructure). The typecheck path in iter 3 // installs `float_to_str : (Float) -> Str`; until the // runtime gets a Str allocator, calling it crashes // codegen. This is intentional: don't ship an // unimplemented call to the LLM-author surface. unimplemented!("Floats milestone iter 5+: float_to_str needs dynamic Str allocation"); } ``` - [ ] **Step 5: Verify GREEN** Run: `cargo test -p ailang-codegen --lib lowers_float_fn_builtins` Expected: PASS. Run: `cargo test --workspace` Expected: 399+ passed, 0 failed. - [ ] **Step 6: Commit** ```bash git add crates/ailang-codegen/src/lib.rs git commit -m "floats iter 4.4: codegen neg/int_to_float/float_to_int_truncate/is_nan + float_to_str-deferred" ``` --- ## Task 5: Codegen Float constants (`nan`, `inf`, `neg_inf`) **Files:** - Modify: `crates/ailang-codegen/src/lib.rs::lower_term` Term::Var arm — intercept `nan`/`inf`/`neg_inf` before global-lookup fallback. - [ ] **Step 1: Find the Term::Var lowering site** Run: `grep -n "Term::Var.*=>" crates/ailang-codegen/src/lib.rs | head -5` Locate the `Term::Var { name } => { ... }` arm in `lower_term`. There may be several `Term::Var` matches in the file; the load-bearing one is in `lower_term` (around line 1228). - [ ] **Step 2: Write the RED test** In `crates/ailang-codegen/src/lib.rs::tests`, append: ```rust /// Floats iter 4.5 RED: `nan`/`inf`/`neg_inf` resolve as bare /// `Term::Var` references and lower to direct hex-float `double` /// SSA values at the use site (no global definition emitted — /// they are values, not unreachable-style terminators). #[test] fn lowers_float_constants() { use ailang_core::ast::*; fn fn_def(name: &str, body: Term) -> Def { Def::Fn(FnDef { name: name.into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::float()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, }) } let main_def = Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::unit()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, }); let m = Module { schema: ailang_core::SCHEMA.to_string(), name: "t".into(), imports: vec![], defs: vec![ fn_def("k_nan", Term::Var { name: "nan".into() }), fn_def("k_inf", Term::Var { name: "inf".into() }), fn_def("k_neg_inf", Term::Var { name: "neg_inf".into() }), main_def, ], }; let ir = emit_ir(&m).unwrap(); assert!(ir.contains("0x7FF8000000000000"), "nan bit pattern missing: {ir}"); assert!(ir.contains("0x7FF0000000000000"), "+inf bit pattern missing: {ir}"); assert!(ir.contains("0xFFF0000000000000"), "-inf bit pattern missing: {ir}"); } ``` - [ ] **Step 3: Verify RED** Run: `cargo test -p ailang-codegen --lib lowers_float_constants` Expected: FAIL — `nan` etc. fall through `Term::Var` to the global-lookup path, which doesn't have them. - [ ] **Step 4: Intercept the three constants in `lower_term`'s Term::Var arm** In `crates/ailang-codegen/src/lib.rs::lower_term`, find the `Term::Var { name }` arm (around line 1228). At the TOP of that arm, BEFORE any other resolution attempt, add: ```rust // Floats iter 4.5: bare-value Float constants resolve // directly to LLVM hex-float `double` SSA values at // the use site — no global declaration, no // intern-global path. Parallel to how `__unreachable__` // is intercepted, but as a value rather than a // terminator (constants are SSA values; the // unreachable-instruction path doesn't apply). match name.as_str() { "nan" => return Ok(("0x7FF8000000000000".into(), "double".into())), "inf" => return Ok(("0x7FF0000000000000".into(), "double".into())), "neg_inf" => return Ok(("0xFFF0000000000000".into(), "double".into())), _ => {} } ``` - [ ] **Step 5: Verify GREEN** Run: `cargo test -p ailang-codegen --lib lowers_float_constants` Expected: PASS. Run: `cargo test --workspace` Expected: 400+ passed, 0 failed. - [ ] **Step 6: Commit** ```bash git add crates/ailang-codegen/src/lib.rs git commit -m "floats iter 4.5: codegen Float constants nan/inf/neg_inf as hex-double SSA values" ``` --- ## Task 6: Codegen `io/print_float` + E2E fixture `examples/floats.ail.json` **Files:** - Modify: `crates/ailang-codegen/src/lib.rs::lower_do` (line ~2152 region) — add `"io/print_float"` arm. - Create: `examples/floats.ail.json` — small Float E2E fixture. - Create: `crates/ail/tests/floats_e2e.rs` — `ail build` + run + stdout-check. - [ ] **Step 1: Write the RED test for io/print_float lowering** In `crates/ailang-codegen/src/lib.rs::tests`, append: ```rust /// Floats iter 4.6 RED: `(do io/print_float 1.5)` lowers via /// `printf("%g\n", v)`, parallel to `io/print_int` at line 2152. #[test] fn lowers_io_print_float() { use ailang_core::ast::*; let body = Term::Do { op: "io/print_float".into(), args: vec![Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }], tail: false, }; let m = Module { schema: ailang_core::SCHEMA.to_string(), name: "t".into(), imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::unit()), effects: vec!["IO".into()], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, })], }; let ir = emit_ir(&m).unwrap(); assert!( ir.contains("call i32 (ptr, ...) @printf"), "io/print_float not emitting printf: {ir}" ); assert!( ir.contains("double 0x3FF8000000000000"), "Float arg not threaded through io/print_float: {ir}" ); // Verify the format string `%g\n` is interned. assert!( ir.contains("%g") || ir.contains("\\67"), // `g` ASCII = 67 = 0x47 "format string `%g\\n` not interned: {ir}" ); } ``` - [ ] **Step 2: Verify RED** Run: `cargo test -p ailang-codegen --lib lowers_io_print_float` Expected: FAIL — `io/print_float` op is unhandled in `lower_do`. - [ ] **Step 3: Add the `io/print_float` arm** In `crates/ailang-codegen/src/lib.rs::lower_do` (around line 2152), add immediately AFTER the `io/print_int` arm: ```rust "io/print_float" => { if args.len() != 1 { return Err(CodegenError::Internal( "io/print_float arity".into(), )); } let (v, vty) = self.lower_term(&args[0])?; if vty != "double" { return Err(CodegenError::Internal( "io/print_float needs double".into(), )); } let fmt = self.intern_string("fmt_float", "%g\n"); self.body.push_str(&format!( " {call_kw} i32 (ptr, ...) @printf(ptr @{fmt}, double {v})\n" )); if tail { self.body.push_str(" ret i8 0\n"); self.block_terminated = true; } Ok(("0".into(), "i8".into())) } ``` - [ ] **Step 4: Verify the unit test now GREEN** Run: `cargo test -p ailang-codegen --lib lowers_io_print_float` Expected: PASS. - [ ] **Step 5: Author the E2E fixture `examples/floats.ail.json`** Create `examples/floats.ail.json` with the following content: ```json { "schema": "ailang/v0", "name": "floats", "imports": [], "defs": [ { "kind": "fn", "name": "main", "type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": ["IO"] }, "params": [], "body": { "t": "seq", "lhs": { "t": "do", "op": "io/print_float", "args": [ { "t": "app", "fn": { "t": "var", "name": "+" }, "args": [ { "t": "lit", "lit": { "kind": "float", "bits": "3ff8000000000000" } }, { "t": "lit", "lit": { "kind": "float", "bits": "4004000000000000" } } ] } ] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_float", "args": [ { "t": "app", "fn": { "t": "var", "name": "int_to_float" }, "args": [ { "t": "lit", "lit": { "kind": "int", "value": 42 } } ] } ] }, "rhs": { "t": "do", "op": "io/print_float", "args": [ { "t": "app", "fn": { "t": "var", "name": "neg" }, "args": [ { "t": "lit", "lit": { "kind": "float", "bits": "3ff8000000000000" } } ] } ] } } } } ] } ``` This program prints three lines to stdout: 1. `1.5 + 2.5 = 4` (printf `%g` for 4.0 produces `4`) 2. `int_to_float(42) = 42` (printf `%g` for 42.0 produces `42`) 3. `neg(1.5) = -1.5` (printf `%g` for -1.5 produces `-1.5`) Expected stdout: ``` 4 42 -1.5 ``` - [ ] **Step 6: Author the E2E test file** Create `crates/ail/tests/floats_e2e.rs`: ```rust //! Floats milestone iter 4.6 — end-to-end fixture. //! //! `examples/floats.ail.json` is built via the public `ail build` //! CLI and run; stdout is matched against the expected three //! lines (`4`, `42`, `-1.5`). This test exercises the full //! pipeline — Float literal lowering, polymorphic `+` Float arm, //! `int_to_float` (sitofp), `neg` Float (fneg), `io/print_float` //! (printf `%g\n`). use std::path::PathBuf; use std::process::Command; fn workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent().unwrap() .parent().unwrap() .to_path_buf() } #[test] fn floats_example_prints_expected_stdout() { let root = workspace_root(); let example = root.join("examples").join("floats.ail.json"); assert!(example.exists(), "expected fixture at {:?}", example); // Build via `cargo run -p ail -- build` to use the in-tree CLI. let target_bin = std::env::temp_dir().join(format!( "ailang-floats-e2e-{}", std::process::id() )); let build = Command::new("cargo") .current_dir(&root) .args([ "run", "--quiet", "-p", "ail", "--", "build", example.to_str().unwrap(), "-o", target_bin.to_str().unwrap(), ]) .output() .expect("cargo run -p ail -- build"); assert!( build.status.success(), "ail build failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&build.stdout), String::from_utf8_lossy(&build.stderr), ); let run = Command::new(&target_bin) .output() .expect("run floats binary"); assert!( run.status.success(), "floats binary exited non-zero: status {:?}\nstdout: {}\nstderr: {}", run.status, String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr), ); let stdout = String::from_utf8_lossy(&run.stdout); let expected = "4\n42\n-1.5\n"; assert_eq!( stdout, expected, "stdout mismatch.\nGot:\n{}\nExpected:\n{}", stdout, expected, ); let _ = std::fs::remove_file(&target_bin); } ``` Note: this test depends on the `ail` CLI's `build` subcommand accepting the standard ` -o ` arg shape. Verify the actual CLI shape via `cargo run -p ail -- build --help` if the test fails to invoke the build cleanly. The test is gated on `ail build` succeeding from a clean checkout — if the test runner environment lacks `clang` or `libgc`/`libc`, the test will fail on the link step. Mark with `#[ignore]` ONLY if local dev wants to skip; CI must run it. - [ ] **Step 7: Run the E2E test** Run: `cargo test -p ail --test floats_e2e floats_example_prints_expected_stdout` Expected: PASS — three lines of expected output match exactly. If the test fails on the build step, inspect the stderr in the panic and address the actual issue. Do NOT mark `#[ignore]` to push past the failure. - [ ] **Step 8: Verify no workspace regression** Run: `cargo test --workspace` Expected: 401+ passed (= prior + 1 new unit + 1 new E2E), 0 failed. - [ ] **Step 9: Commit** ```bash git add crates/ailang-codegen/src/lib.rs \ examples/floats.ail.json \ crates/ail/tests/floats_e2e.rs git commit -m "floats iter 4.6: codegen io/print_float + examples/floats.ail.json E2E fixture" ``` --- ## Task 7: Iteration close — JOURNAL entry **Files:** - Modify: `docs/JOURNAL.md` — append entry at file end with the per-task SHAs from Tasks 1-6. - [ ] **Step 1: Append the iteration-close entry** The orchestrator will write this section directly per the established pattern (single-file edit; `Edit` tool). The text mirrors the per-task commit subjects, lists the deferred known debt (`float_to_str` runtime-Str allocation), and notes the end-to-end fixture as the milestone-acceptance gate. - [ ] **Step 2: Commit** ```bash git add docs/JOURNAL.md git commit -m "floats iter 4: JOURNAL — iteration close (codegen + E2E)" ``` --- ## Iteration acceptance - [ ] `cargo build --workspace` is clean. - [ ] `cargo test --workspace` is GREEN — pre-existing tests stay GREEN (the iter-3 close had 395 tests; iter 4 adds 5 codegen unit tests + 1 E2E test ≈ 401 expected). - [ ] `examples/floats.ail.json` builds via `ail build` and produces the expected stdout (`4\n42\n-1.5\n`). - [ ] No `unimplemented!` arms with the `"Floats milestone iter 4: codegen"` string remain in any crate (the iter-1 markers are all replaced). - [ ] `float_to_str` codegen arm carries an honest deferred `unimplemented!("Floats milestone iter 5+: ...")` marker — no silent fallback. - [ ] `lib.rs::tests::lowers_t_add_int` (the existing `add i64` regression) stays GREEN. - [ ] No file touched outside the five listed in the file map. - [ ] JOURNAL entry committed. When all seven tasks are committed and the acceptance checklist is green, hand back to the orchestrator. Iteration 5 (Prose round-trip + DESIGN.md §"Float semantics" + line-2033 update + drift-test extension; OPTIONALLY `float_to_str` runtime-Str allocation if the orchestrator chooses to scope it in) is the next dispatch.