plan: 24.1 — bool_to_str + str_clone runtime + codegen wiring, 6 tasks
Six tasks: (1) runtime C functions in runtime/str.c, (2) builtin install lockstep across builtins.rs + synth.rs + list() row + 2 unit tests, (3) codegen IR wiring (2 declares + 2 lower_app arms + is_static_callee whitelist), (4) 2 IR-shape unit pins, (5) regen 5 IR snapshots, (6) 4 fixtures + 5 E2E tests + acceptance gate. Mechanical mirror of hs.4's int_to_str/float_to_str pattern. Plan captures every Edit verbatim (old_string + new_string) per planner Iron Law. Pre-existing drift fix included inline: int_to_str row missing from builtins.rs::list() since hs.4 — adding it adjacent to the two new rows. Three open questions from plan-recon resolved by Boss judgement (C uses bool from <stdbool.h>; tests live in crates/ail/tests/e2e.rs as the existing real path; int_to_str list() row drift fixed inline).
This commit is contained in:
@@ -0,0 +1,890 @@
|
||||
# 24.1 — `bool_to_str` + `str_clone` Runtime + Codegen Wiring — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/2026-05-12-24-show-print.md` (commit 953f2e1)
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
||||
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Wire two new heap-Str-producing primitives — `bool_to_str`
|
||||
and `str_clone` — through the runtime C code, type checker, codegen
|
||||
IR lowering, and IR-header preamble, mechanically mirror-imaging
|
||||
the existing hs.4 wiring for `int_to_str` / `float_to_str`.
|
||||
|
||||
**Architecture:** Pure-additive wiring across four crates / files.
|
||||
The two new C functions live in `runtime/str.c` next to
|
||||
`ailang_int_to_str` / `ailang_float_to_str`. Type signatures land
|
||||
lockstep in `crates/ailang-check/src/builtins.rs::install` and
|
||||
`crates/ailang-codegen/src/synth.rs::builtin_value_type` (both
|
||||
sides MUST agree on `ParamMode::Own` for `ret_mode`). Codegen
|
||||
gains two IR-header declares, two `Emitter::lower_app` arms, and
|
||||
two `is_static_callee` whitelist entries — all in
|
||||
`crates/ailang-codegen/src/lib.rs`. Five IR snapshots regenerate
|
||||
because the header preamble grows by two lines. Nine new tests
|
||||
land: 2 builtins-install unit tests (parallel to
|
||||
`install_int_to_str_signature`), 2 IR-shape unit pins (parallel to
|
||||
`int_to_str_lowers_to_ailang_int_to_str_call`), 2 RC-stats E2E,
|
||||
2 stdout-smoke E2E (one per Bool branch), plus a cross-realisation
|
||||
E2E for `str_clone`. No checker-level architectural decisions —
|
||||
all design is committed in the parent spec.
|
||||
|
||||
**Tech Stack:** `ailang-check::builtins`,
|
||||
`ailang-codegen::lib::Emitter::{lower_app, is_static_callee}`,
|
||||
`ailang-codegen::synth::builtin_value_type`, `runtime/str.c` (C
|
||||
with `<stdbool.h>`), `crates/ail/tests/e2e.rs` (E2E harness with
|
||||
`build_and_run_with_rc_stats` from eob.1).
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `runtime/str.c:143` — append two new C functions
|
||||
`ailang_bool_to_str(bool)` and `ailang_str_clone(const char *)`.
|
||||
- Modify: `crates/ailang-check/src/builtins.rs:212` — insert two new
|
||||
`env.globals.insert` blocks for `bool_to_str` and `str_clone`
|
||||
(parallel to the existing `int_to_str` / `float_to_str` blocks at
|
||||
`:193-212`).
|
||||
- Modify: `crates/ailang-check/src/builtins.rs:307` — add row
|
||||
`("int_to_str", "(Int) -> Str")` (pre-existing drift: hs.4 installed
|
||||
it in `env.globals` but missed the `list()` row) plus two new rows
|
||||
for `bool_to_str` and `str_clone`.
|
||||
- Modify: `crates/ailang-check/src/builtins.rs:467` — append two new
|
||||
unit tests `install_bool_to_str_signature` and
|
||||
`install_str_clone_signature` (parallel to the existing
|
||||
`install_int_to_str_signature` at `:461-467`).
|
||||
- Modify: `crates/ailang-codegen/src/synth.rs:180` — insert two new
|
||||
arms in `builtin_value_type` for `bool_to_str` and `str_clone`
|
||||
(parallel to the existing `int_to_str` arm at `:174-180`).
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:538` — insert two new
|
||||
IR-header declares `declare ptr @ailang_bool_to_str(i1)` and
|
||||
`declare ptr @ailang_str_clone(ptr)` after the existing
|
||||
`@ailang_int_to_str` / `@ailang_float_to_str` declares at
|
||||
`:537-538`.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:1933` — insert two new
|
||||
`lower_app` arms for `bool_to_str` and `str_clone` after the
|
||||
existing `float_to_str` arm at `:1915-1933`.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:2133` — extend
|
||||
`is_static_callee` `matches!` whitelist (at `:2126-2134`) with
|
||||
`"bool_to_str"` and `"str_clone"`.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:4204` — append two new
|
||||
IR-shape pin tests `bool_to_str_emits_call_to_ailang_bool_to_str`
|
||||
and `str_clone_emits_call_to_ailang_str_clone` (parallel to the
|
||||
existing `int_to_str_lowers_to_ailang_int_to_str_call` at
|
||||
`:4131-4165`).
|
||||
- Modify: `crates/ail/tests/snapshots/hello.ll`, `list.ll`, `max3.ll`,
|
||||
`sum.ll`, `ws_main.ll` — regenerate 5 IR snapshots via
|
||||
`UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_` after the IR header
|
||||
grows by two `declare` lines.
|
||||
- Create: `examples/bool_to_str_drop_rc.ail.json` — `let s = bool_to_str true in do io/print_str s` for the RC-stats test.
|
||||
- Create: `examples/bool_to_str_smoke_false.ail.json` — `let s = bool_to_str false in do io/print_str s` for the false-branch stdout-smoke test.
|
||||
- Create: `examples/str_clone_drop_rc.ail.json` — `let s = str_clone "hello" in do io/print_str s` for the RC-stats test (also doubles as the stdout-smoke test for `str_clone`).
|
||||
- Create: `examples/str_clone_cross_realisation.ail.json` — clones both `int_to_str 42` (heap-Str input) and the static-Str literal `"abc"` and prints both, pinning the uniform consumer ABI claim.
|
||||
- Test: `crates/ail/tests/e2e.rs:2657` — append five new tests
|
||||
(`bool_to_str_drop_balances_rc_stats`,
|
||||
`bool_to_str_emits_true_branch`, `bool_to_str_emits_false_branch`,
|
||||
`str_clone_drop_balances_rc_stats`,
|
||||
`str_clone_cross_realisation_uniform_abi`).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Runtime C functions in `runtime/str.c`
|
||||
|
||||
Add two new C functions paralleling `ailang_int_to_str` /
|
||||
`ailang_float_to_str`. Both allocate fresh heap-Str slabs via the
|
||||
existing `str_alloc` helper (which calls `ailang_rc_alloc`) and
|
||||
return the payload pointer. `<stdbool.h>` is already included at
|
||||
`:16` so `bool` is in scope.
|
||||
|
||||
**Files:**
|
||||
- Modify: `runtime/str.c:143`
|
||||
|
||||
- [ ] **Step 1: Append `ailang_bool_to_str` after `ailang_float_to_str`**
|
||||
|
||||
Edit `runtime/str.c` — insert after the closing brace of
|
||||
`ailang_float_to_str` (which ends at line 143):
|
||||
|
||||
```c
|
||||
|
||||
/* Convert a bool into a heap-allocated Str — "true" (4 bytes) or
|
||||
* "false" (5 bytes). Heap-allocates every call; the static-Str
|
||||
* literal alternative (used by a hypothetical `\x -> if x then "true"
|
||||
* else "false"` schema body) would require phi-of-static-Str
|
||||
* through the drop-elision pipeline which is not load-bearing-
|
||||
* ratified today (see parent spec §"Out of scope" / `bool_to_str` as
|
||||
* schema-if).
|
||||
*
|
||||
* Returns the payload pointer of a fresh heap-Str slab.
|
||||
*/
|
||||
char *ailang_bool_to_str(bool b) {
|
||||
const char *lit = b ? "true" : "false";
|
||||
uint64_t len = b ? 4 : 5;
|
||||
char *payload = str_alloc(len);
|
||||
memcpy(payload + 8, lit, len);
|
||||
payload[8 + len] = '\0';
|
||||
return payload;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Append `ailang_str_clone` after `ailang_bool_to_str`**
|
||||
|
||||
Edit `runtime/str.c` — insert after the closing brace of the just-
|
||||
added `ailang_bool_to_str`:
|
||||
|
||||
```c
|
||||
|
||||
/* Clone a Str into a fresh heap-Str slab. Reads `len` from the
|
||||
* first 8 bytes of the source payload, allocates a new slab of the
|
||||
* same length via `str_alloc`, and memcpy's the payload bytes plus
|
||||
* the trailing NUL. Works uniformly on static-Str and heap-Str
|
||||
* inputs because the consumer ABI (i64 len at offset 0, bytes
|
||||
* starting at offset 8, NUL at offset 8 + len) is identical
|
||||
* between realisations (see DESIGN.md §"Str ABI"); the rc_header at
|
||||
* offset -8 — present only on heap-Str — is never consulted.
|
||||
*
|
||||
* Used by `show__Str` (parent spec milestone 24): converts a
|
||||
* borrowed Str input into an Owned heap-Str output, preserving
|
||||
* the uniform `show : forall a. Show a => (a borrow) -> Str Own`
|
||||
* signature.
|
||||
*
|
||||
* Returns the payload pointer of a fresh heap-Str slab.
|
||||
*/
|
||||
char *ailang_str_clone(const char *src) {
|
||||
uint64_t len = *(const uint64_t *)src;
|
||||
char *payload = str_alloc(len);
|
||||
memcpy(payload + 8, src + 8, len);
|
||||
payload[8 + len] = '\0';
|
||||
return payload;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify str.c compiles standalone via `cargo build --workspace`**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -20`
|
||||
Expected: clean build, no warnings, no errors. (The new C functions
|
||||
are unreachable from any IR call site until Task 3 lands the
|
||||
codegen wiring, but the .o builds fine in isolation.)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Builtin install — checker + synth.rs lockstep + `list()` rows + unit tests
|
||||
|
||||
Install both signatures in `crates/ailang-check/src/builtins.rs::install`
|
||||
with `ret_mode: ParamMode::Own`, mirror in
|
||||
`crates/ailang-codegen/src/synth.rs::builtin_value_type`. Add the
|
||||
two new rows plus the pre-existing-missing `int_to_str` row in
|
||||
`list()`. Two new unit tests in builtins.rs's tests module.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-check/src/builtins.rs:212`
|
||||
- Modify: `crates/ailang-check/src/builtins.rs:307`
|
||||
- Modify: `crates/ailang-check/src/builtins.rs:467`
|
||||
- Modify: `crates/ailang-codegen/src/synth.rs:180`
|
||||
|
||||
- [ ] **Step 1: Insert `bool_to_str` and `str_clone` env.globals.insert blocks**
|
||||
|
||||
Edit `crates/ailang-check/src/builtins.rs:212` — insert after the
|
||||
closing of the `int_to_str` block (which ends at line 212, the line
|
||||
with `);`) and before the `env.globals.insert("is_nan".into(), ...)`
|
||||
block at line 213:
|
||||
|
||||
```rust
|
||||
env.globals.insert(
|
||||
"bool_to_str".into(),
|
||||
Type::Fn {
|
||||
params: vec![Type::bool_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
env.globals.insert(
|
||||
"str_clone".into(),
|
||||
Type::Fn {
|
||||
params: vec![Type::str_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![ailang_core::ast::ParamMode::Borrow],
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Note: `str_clone`'s `param_modes` carries an explicit `Borrow` for
|
||||
the single Str input, matching the parent spec's
|
||||
`(Str borrow) -> Str` signature. `bool_to_str`'s `param_modes` stays
|
||||
empty (Bool is bit-copied; no borrow/own distinction).
|
||||
|
||||
- [ ] **Step 2: Add three rows to `list()` vec — `int_to_str` (drift fix), `bool_to_str`, `str_clone`**
|
||||
|
||||
Edit `crates/ailang-check/src/builtins.rs:307` — replace the line:
|
||||
|
||||
```rust
|
||||
("float_to_str", "(Float) -> Str"),
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
("float_to_str", "(Float) -> Str"),
|
||||
("int_to_str", "(Int) -> Str"),
|
||||
("bool_to_str", "(Bool) -> Str"),
|
||||
("str_clone", "(Str) -> Str"),
|
||||
```
|
||||
|
||||
(The `int_to_str` row is the pre-existing-drift fix flagged by
|
||||
plan-recon; hs.4 added the `env.globals` entry at `:204` but missed
|
||||
the corresponding `list()` row.)
|
||||
|
||||
- [ ] **Step 3: Append `install_bool_to_str_signature` and `install_str_clone_signature` unit tests**
|
||||
|
||||
Edit `crates/ailang-check/src/builtins.rs:467` — insert after the
|
||||
closing brace of `install_int_to_str_signature` (which ends at line
|
||||
467) and before the next test (`install_is_nan_signature` at line
|
||||
473):
|
||||
|
||||
```rust
|
||||
|
||||
/// Iter 24.1: `bool_to_str : (Bool) -> Str`. Codegen lowers via the
|
||||
/// runtime C glue `ailang_bool_to_str` from `runtime/str.c`.
|
||||
#[test]
|
||||
fn install_bool_to_str_signature() {
|
||||
let ty = synth_in_builtins_env(&app("bool_to_str", vec![lit_bool(true)]));
|
||||
assert_eq!(ty, Type::str_(), "(bool_to_str true) must type as Str");
|
||||
}
|
||||
|
||||
/// Iter 24.1: `str_clone : (Str borrow) -> Str`. Codegen lowers via the
|
||||
/// runtime C glue `ailang_str_clone` from `runtime/str.c`.
|
||||
#[test]
|
||||
fn install_str_clone_signature() {
|
||||
let ty = synth_in_builtins_env(&app("str_clone", vec![lit_str("hi")]));
|
||||
assert_eq!(ty, Type::str_(), "(str_clone \"hi\") must type as Str");
|
||||
}
|
||||
```
|
||||
|
||||
Note: if `lit_bool` / `lit_str` test helpers do not exist in the
|
||||
local test module, fall back to the explicit shape:
|
||||
|
||||
```rust
|
||||
let ty = synth_in_builtins_env(&app(
|
||||
"bool_to_str",
|
||||
vec![Term::Lit { lit: Literal::Bool { value: true } }],
|
||||
));
|
||||
```
|
||||
|
||||
(check the existing `install_is_nan_signature` test at `:474-479`
|
||||
for the in-use helper shape; if it uses `lit_float`, then
|
||||
`lit_bool` / `lit_str` likely exist as siblings.)
|
||||
|
||||
- [ ] **Step 4: Run builtins.rs tests to verify all three new tests pass**
|
||||
|
||||
Run: `cargo test --workspace -p ailang-check builtins::tests 2>&1 | tail -20`
|
||||
Expected: all builtins::tests green, including the two new
|
||||
`install_bool_to_str_signature` and `install_str_clone_signature`.
|
||||
|
||||
- [ ] **Step 5: Mirror lockstep arms in `synth.rs::builtin_value_type`**
|
||||
|
||||
Edit `crates/ailang-codegen/src/synth.rs:180` — insert after the
|
||||
closing brace of the `int_to_str` arm (which ends at line 180,
|
||||
`},`) and before the `is_nan` arm at line 181:
|
||||
|
||||
```rust
|
||||
"bool_to_str" => Type::Fn {
|
||||
params: vec![Type::bool_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"str_clone" => Type::Fn {
|
||||
params: vec![Type::str_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![ParamMode::Borrow],
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Verify lockstep — full workspace builds clean**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -20`
|
||||
Expected: clean build, no warnings. Both `builtins.rs::install` and
|
||||
`synth.rs::builtin_value_type` now agree on the two new symbols.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Codegen IR wiring — header preamble declares + `lower_app` arms + `is_static_callee` whitelist
|
||||
|
||||
Three sub-edits all in `crates/ailang-codegen/src/lib.rs`. Adds two
|
||||
IR-header `declare` lines, two `lower_app` call-emission arms, and
|
||||
two whitelist entries in `is_static_callee`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:538`
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:1933`
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:2133`
|
||||
|
||||
- [ ] **Step 1: Add two IR-header declares**
|
||||
|
||||
Edit `crates/ailang-codegen/src/lib.rs:538` — replace the line:
|
||||
|
||||
```rust
|
||||
out.push_str("declare ptr @ailang_float_to_str(double)\n");
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
out.push_str("declare ptr @ailang_float_to_str(double)\n");
|
||||
// Iter 24.1: heap-Str primitive externs for the `show` instances
|
||||
// — `ailang_bool_to_str` (Show Bool) and `ailang_str_clone`
|
||||
// (Show Str). Same dual-realisation ABI as int_to_str /
|
||||
// float_to_str: rc_header at offset -8 on the heap-Str output;
|
||||
// consumer ABI shared with static-Str (len at offset 0, bytes
|
||||
// at offset 8). Declared unconditionally on the same rationale
|
||||
// — runtime/str.c is unconditionally linked and clang -O2 dead-
|
||||
// strips when no caller exists.
|
||||
out.push_str("declare ptr @ailang_bool_to_str(i1)\n");
|
||||
out.push_str("declare ptr @ailang_str_clone(ptr)\n");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add two `lower_app` arms**
|
||||
|
||||
Edit `crates/ailang-codegen/src/lib.rs:1933` — insert after the
|
||||
closing brace of the `float_to_str` arm (which ends at line 1933,
|
||||
the line with `}`) and before line 1934 (the blank line preceding
|
||||
the cross-module-call comment block):
|
||||
|
||||
```rust
|
||||
if name == "bool_to_str" {
|
||||
// Iter 24.1: lowers to the runtime C glue
|
||||
// `ailang_bool_to_str(i1) -> ptr` defined in
|
||||
// `runtime/str.c`. Returned pointer is a heap-Str
|
||||
// (rc_header at offset -8; consumer ABI shared with
|
||||
// static-Str). Used by `show__Bool` in milestone 24.
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal("bool_to_str arity".into()));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call ptr @ailang_bool_to_str(i1 {a})\n"
|
||||
));
|
||||
return Ok((dst, "ptr".to_string()));
|
||||
}
|
||||
if name == "str_clone" {
|
||||
// Iter 24.1: lowers to the runtime C glue
|
||||
// `ailang_str_clone(ptr) -> ptr` defined in
|
||||
// `runtime/str.c`. Reads `len` from offset 0 of the
|
||||
// source Str payload and allocates a fresh heap-Str
|
||||
// slab; works uniformly on static-Str and heap-Str
|
||||
// inputs because the consumer ABI is identical. Used by
|
||||
// `show__Str` in milestone 24.
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal("str_clone arity".into()));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call ptr @ailang_str_clone(ptr {a})\n"
|
||||
));
|
||||
return Ok((dst, "ptr".to_string()));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Extend `is_static_callee` whitelist**
|
||||
|
||||
Edit `crates/ailang-codegen/src/lib.rs:2133` — replace the line:
|
||||
|
||||
```rust
|
||||
| "int_to_str"
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
| "int_to_str"
|
||||
| "bool_to_str"
|
||||
| "str_clone"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build workspace to verify codegen compiles**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -20`
|
||||
Expected: clean build, no warnings, no errors. The new arms are
|
||||
unreachable from any test yet, but the file compiles.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: IR-shape unit pin tests in `lib.rs::tests`
|
||||
|
||||
Two new tests in `crates/ailang-codegen/src/lib.rs`'s `mod tests`
|
||||
block, mechanical copies of the existing
|
||||
`int_to_str_lowers_to_ailang_int_to_str_call` template.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:4204`
|
||||
|
||||
- [ ] **Step 1: Append `bool_to_str_emits_call_to_ailang_bool_to_str`**
|
||||
|
||||
Edit `crates/ailang-codegen/src/lib.rs:4204` — insert after the
|
||||
closing brace of `float_to_str_no_longer_errors_internal` (which
|
||||
ends at line 4204, `}`) and before the `}` of `mod tests` at line
|
||||
4205:
|
||||
|
||||
```rust
|
||||
|
||||
/// Iter 24.1: a `Term::App` calling `bool_to_str` lowers to
|
||||
/// `call ptr @ailang_bool_to_str(i1 %a)`. Pins the new builtin's
|
||||
/// lowering shape against the runtime C glue introduced in this
|
||||
/// iter (mirror of hs.4's `int_to_str_lowers_to_ailang_int_to_str_call`).
|
||||
#[test]
|
||||
fn bool_to_str_emits_call_to_ailang_bool_to_str() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
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: Term::Do {
|
||||
op: "io/print_str".into(),
|
||||
args: vec![Term::App {
|
||||
callee: Box::new(Term::Var { name: "bool_to_str".into() }),
|
||||
args: vec![Term::Lit { lit: Literal::Bool { value: true } }],
|
||||
tail: false,
|
||||
}],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("call ptr @ailang_bool_to_str(i1 "),
|
||||
"expected lowering of bool_to_str to call @ailang_bool_to_str; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Append `str_clone_emits_call_to_ailang_str_clone`**
|
||||
|
||||
Edit `crates/ailang-codegen/src/lib.rs` — insert immediately after
|
||||
the closing brace of `bool_to_str_emits_call_to_ailang_bool_to_str`
|
||||
just added in Step 1:
|
||||
|
||||
```rust
|
||||
|
||||
/// Iter 24.1: a `Term::App` calling `str_clone` lowers to
|
||||
/// `call ptr @ailang_str_clone(ptr %a)`. Pins the new builtin's
|
||||
/// lowering shape against the runtime C glue introduced in this
|
||||
/// iter (mirror of hs.4's `int_to_str_lowers_to_ailang_int_to_str_call`).
|
||||
#[test]
|
||||
fn str_clone_emits_call_to_ailang_str_clone() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
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: Term::Do {
|
||||
op: "io/print_str".into(),
|
||||
args: vec![Term::App {
|
||||
callee: Box::new(Term::Var { name: "str_clone".into() }),
|
||||
args: vec![Term::Lit { lit: Literal::Str { value: "hi".into() } }],
|
||||
tail: false,
|
||||
}],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("call ptr @ailang_str_clone(ptr "),
|
||||
"expected lowering of str_clone to call @ailang_str_clone; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the two new tests**
|
||||
|
||||
Run: `cargo test --workspace -p ailang-codegen bool_to_str_emits_call_to_ailang_bool_to_str str_clone_emits_call_to_ailang_str_clone 2>&1 | tail -20`
|
||||
Expected: both PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Regenerate the five IR snapshots
|
||||
|
||||
The IR header preamble grew by two `declare` lines (Task 3 Step 1),
|
||||
which shifts the byte content of every IR snapshot. Five existing
|
||||
snapshots in `crates/ail/tests/snapshots/` need regeneration. The
|
||||
snapshot harness lives at `crates/ail/tests/ir_snapshot.rs:76` and
|
||||
honours `UPDATE_SNAPSHOTS=1` to overwrite the goldens.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ail/tests/snapshots/hello.ll`
|
||||
- Modify: `crates/ail/tests/snapshots/list.ll`
|
||||
- Modify: `crates/ail/tests/snapshots/max3.ll`
|
||||
- Modify: `crates/ail/tests/snapshots/sum.ll`
|
||||
- Modify: `crates/ail/tests/snapshots/ws_main.ll`
|
||||
|
||||
- [ ] **Step 1: Verify the IR-snapshot tests FAIL pre-regen**
|
||||
|
||||
Run: `cargo test --workspace -p ail ir_snapshot_ 2>&1 | tail -30`
|
||||
Expected: all five `ir_snapshot_*` tests FAIL with diff output
|
||||
showing the new `declare ptr @ailang_bool_to_str(i1)` and
|
||||
`declare ptr @ailang_str_clone(ptr)` lines appearing in the
|
||||
generated IR but not in the golden.
|
||||
|
||||
- [ ] **Step 2: Regenerate snapshots**
|
||||
|
||||
Run: `UPDATE_SNAPSHOTS=1 cargo test --workspace -p ail ir_snapshot_ 2>&1 | tail -10`
|
||||
Expected: all five `ir_snapshot_*` tests PASS (because
|
||||
`UPDATE_SNAPSHOTS=1` overwrites the goldens rather than asserting
|
||||
against them).
|
||||
|
||||
- [ ] **Step 3: Verify the regenerated snapshots have exactly the two new declares**
|
||||
|
||||
Run: `grep -c "declare ptr @ailang_bool_to_str\|declare ptr @ailang_str_clone" crates/ail/tests/snapshots/*.ll`
|
||||
Expected: each of the 5 files reports 2 (one match for each new
|
||||
declare line).
|
||||
|
||||
- [ ] **Step 4: Verify the IR-snapshot tests now PASS without UPDATE_SNAPSHOTS**
|
||||
|
||||
Run: `cargo test --workspace -p ail ir_snapshot_ 2>&1 | tail -10`
|
||||
Expected: all five `ir_snapshot_*` tests PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: E2E fixtures + tests + acceptance gate
|
||||
|
||||
Four new `.ail.json` fixtures in `examples/`, five new E2E tests in
|
||||
`crates/ail/tests/e2e.rs`, then the full acceptance run.
|
||||
|
||||
**Files:**
|
||||
- Create: `examples/bool_to_str_drop_rc.ail.json`
|
||||
- Create: `examples/bool_to_str_smoke_false.ail.json`
|
||||
- Create: `examples/str_clone_drop_rc.ail.json`
|
||||
- Create: `examples/str_clone_cross_realisation.ail.json`
|
||||
- Modify: `crates/ail/tests/e2e.rs:2657`
|
||||
|
||||
- [ ] **Step 1: Create `examples/bool_to_str_drop_rc.ail.json`**
|
||||
|
||||
Write a new file `examples/bool_to_str_drop_rc.ail.json` with this
|
||||
exact content (template: `examples/int_to_str_drop_rc.ail.json`
|
||||
adapted for `bool_to_str` with literal `true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "bool_to_str_drop_rc",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: pin that `bool_to_str` participates in RC discipline. Bind the heap-Str result to `s`, consume `s` once in `io/print_str`, let the binder drop at scope close. Under --alloc=rc with AILANG_RC_STATS=1 the atexit summary must report allocs == 1 && frees == 1 && live == 0 — the heap-Str slab allocated by str_alloc inside ailang_bool_to_str rides the same rc_header + ailang_rc_dec path as int_to_str.",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "bool_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "bool", "value": true } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create `examples/bool_to_str_smoke_false.ail.json`**
|
||||
|
||||
Write a new file `examples/bool_to_str_smoke_false.ail.json` with this
|
||||
exact content (identical to Step 1's fixture but with literal `false`):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "bool_to_str_smoke_false",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: stdout-smoke for the false branch of bool_to_str. Stdout must be `false\\n` (puts adds the newline). Companion of bool_to_str_drop_rc.ail.json (which covers the true branch and the RC-stats invariant).",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "bool_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "bool", "value": false } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create `examples/str_clone_drop_rc.ail.json`**
|
||||
|
||||
Write a new file `examples/str_clone_drop_rc.ail.json` with this
|
||||
exact content (clones the static-Str literal `"hello"`):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "str_clone_drop_rc",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: pin that `str_clone` participates in RC discipline AND emits correct bytes. Input is a static-Str literal; the clone produces a fresh heap-Str slab whose payload is byte-equal to the input. RC stats under --alloc=rc with AILANG_RC_STATS=1: allocs == 1 (the clone), frees == 1 (let-binder drop at scope close), live == 0.",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "str", "value": "hello" } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create `examples/str_clone_cross_realisation.ail.json`**
|
||||
|
||||
Write a new file `examples/str_clone_cross_realisation.ail.json`
|
||||
that clones one heap-Str (output of `int_to_str 42`) and one
|
||||
static-Str (literal `"abc"`) in sequence, printing both:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "str_clone_cross_realisation",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: cross-realisation invariant for str_clone. Clones a heap-Str (produced by int_to_str 42) AND a static-Str (literal `abc`); both clones produce fresh heap-Str slabs that print their input bytes. Pins the uniform-consumer-ABI claim — str_clone only reads the len-field at offset 0 plus the bytes plus the NUL; it never consults the (absent for static-Str) rc_header. RC stats under --alloc=rc: allocs == 3 (int_to_str output + two str_clone outputs), frees == 3, live == 0.",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "src_heap",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "int_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "c1",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "var", "name": "src_heap" }]
|
||||
},
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "c2",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "str", "value": "abc" } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "_a",
|
||||
"value": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "c1" }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "c2" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Append five new tests to `crates/ail/tests/e2e.rs`**
|
||||
|
||||
Edit `crates/ail/tests/e2e.rs:2657` — insert immediately after the
|
||||
closing brace of `heap_str_repeated_print_balances_rc_stats` (which
|
||||
ends at line 2657, the last `}` before the EOF):
|
||||
|
||||
```rust
|
||||
|
||||
/// Iter 24.1: `bool_to_str(true)` bound to a let, consumed by
|
||||
/// `io/print_str`. The heap-Str slab from `ailang_bool_to_str`
|
||||
/// rides the same rc_header + ailang_rc_dec path as int_to_str's
|
||||
/// output — `allocs == 1, frees == 1, live == 0` and stdout is
|
||||
/// `true\n`. Companion of `bool_to_str_emits_false_branch` which
|
||||
/// covers the false-branch stdout.
|
||||
#[test]
|
||||
fn bool_to_str_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("bool_to_str_drop_rc.ail.json");
|
||||
assert_eq!(stdout, "true\n");
|
||||
assert_eq!(allocs, 1, "expected exactly one heap-Str slab; got allocs={allocs}");
|
||||
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter 24.1: stdout-smoke for the true branch — same fixture as
|
||||
/// `bool_to_str_drop_balances_rc_stats` but checked here without
|
||||
/// the RC-stats overhead. Pins the byte content of `ailang_bool_to_str`'s
|
||||
/// "true" slab.
|
||||
#[test]
|
||||
fn bool_to_str_emits_true_branch() {
|
||||
let out = build_and_run("bool_to_str_drop_rc.ail.json");
|
||||
assert_eq!(out, "true\n");
|
||||
}
|
||||
|
||||
/// Iter 24.1: stdout-smoke for the false branch. Pins the byte
|
||||
/// content of `ailang_bool_to_str`'s "false" slab.
|
||||
#[test]
|
||||
fn bool_to_str_emits_false_branch() {
|
||||
let out = build_and_run("bool_to_str_smoke_false.ail.json");
|
||||
assert_eq!(out, "false\n");
|
||||
}
|
||||
|
||||
/// Iter 24.1: `str_clone("hello")` bound to a let, consumed by
|
||||
/// `io/print_str`. The heap-Str clone allocates a fresh slab (via
|
||||
/// str_alloc) and the let-binder drops it at scope close.
|
||||
/// `allocs == 1, frees == 1, live == 0`; stdout is `hello\n` (puts
|
||||
/// adds the newline) — confirming the memcpy preserved every byte.
|
||||
#[test]
|
||||
fn str_clone_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("str_clone_drop_rc.ail.json");
|
||||
assert_eq!(stdout, "hello\n");
|
||||
assert_eq!(allocs, 1, "expected exactly one heap-Str clone slab; got allocs={allocs}");
|
||||
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter 24.1: cross-realisation invariant — `str_clone` works
|
||||
/// uniformly on heap-Str input (`int_to_str 42`'s output) AND on
|
||||
/// static-Str input (literal `"abc"`). Both clones print their
|
||||
/// input bytes; RC stats account for three heap-Str slabs (one
|
||||
/// from int_to_str, two from str_clone) and three matching frees.
|
||||
/// Pins the "uniform consumer ABI" claim from DESIGN.md §"Str
|
||||
/// ABI" — str_clone only reads len + bytes + NUL, never the
|
||||
/// rc_header.
|
||||
#[test]
|
||||
fn str_clone_cross_realisation_uniform_abi() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("str_clone_cross_realisation.ail.json");
|
||||
assert_eq!(stdout, "42\nabc\n");
|
||||
assert_eq!(allocs, 3, "expected one int_to_str slab + two str_clone slabs; got allocs={allocs}");
|
||||
assert_eq!(frees, 3, "expected three matching frees; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the five new e2e tests**
|
||||
|
||||
Run: `cargo test --workspace -p ail bool_to_str_drop_balances_rc_stats bool_to_str_emits_true_branch bool_to_str_emits_false_branch str_clone_drop_balances_rc_stats str_clone_cross_realisation_uniform_abi 2>&1 | tail -30`
|
||||
Expected: all five PASS.
|
||||
|
||||
- [ ] **Step 7: Full workspace test run — acceptance gate**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | tail -30`
|
||||
Expected: all tests green, including the six new ones (two unit
|
||||
IR-pins from Task 4, four E2E tests from Task 6 Step 5 — well, five
|
||||
counting the smoke-false). No previously-passing tests regress.
|
||||
|
||||
- [ ] **Step 8: Bench scripts — acceptance gate**
|
||||
|
||||
Run: `bench/compile_check.py 2>&1 | tail -10`
|
||||
Expected: exit 0, all metrics within tolerance. (Codegen additions
|
||||
shouldn't move compile-time meaningfully; only two new declares
|
||||
plus two arms.)
|
||||
|
||||
Run: `bench/cross_lang.py 2>&1 | tail -10`
|
||||
Expected: exit 0, all metrics within tolerance. (Runtime additions
|
||||
are unreached by any benchmark — no callers yet.)
|
||||
|
||||
- [ ] **Step 9: Grep verification — every new symbol is wired through every layer**
|
||||
|
||||
Run: `grep -rn "bool_to_str\|str_clone" runtime/str.c crates/ailang-check/src/builtins.rs crates/ailang-codegen/src/synth.rs crates/ailang-codegen/src/lib.rs | grep -v test | wc -l`
|
||||
Expected: ≥ 10 hits (the eight code edits from Tasks 1-3 plus the
|
||||
two whitelist entries in is_static_callee).
|
||||
|
||||
Run: `grep -n "ailang_bool_to_str\|ailang_str_clone" crates/ail/tests/snapshots/*.ll | wc -l`
|
||||
Expected: 10 hits (2 declares × 5 snapshot files).
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | grep -E "warning|error" | head`
|
||||
Expected: empty (no warnings, no errors).
|
||||
Reference in New Issue
Block a user