Files
AILang/docs/plans/24.1.md
T
Brummel 8bfa09adc7 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).
2026-05-12 23:33:46 +02:00

891 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).