832375f2ac
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.
1240 lines
44 KiB
Markdown
1240 lines
44 KiB
Markdown
# 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<TypeName>`); `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 `<input> -o <output>` 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.
|