Files
AILang/docs/journals/2026-05-12-iter-24.1.md
T
Brummel f38bad8c2b iter 24.1: bool_to_str + str_clone runtime + codegen wiring
First iter of milestone 24 (Show + print rewire). Wires two new
heap-Str-producing primitives parallel to hs.4's int_to_str /
float_to_str:

- runtime/str.c gains ailang_bool_to_str(bool) → heap-Str "true" /
  "false" and ailang_str_clone(const char *) → memcpy'd heap-Str
  copy. Both use the existing str_alloc slab helper.
- builtins.rs + synth.rs install the two signatures lockstep with
  ret_mode: Own; str_clone carries param_modes: [Borrow].
- IR-header preamble gains two unconditional `declare ptr @...`
  lines; Emitter::lower_app gets two new arms; is_static_callee
  whitelist extends with the two names.
- Five IR snapshots regenerate for the two new declares.
- Pre-existing-drift fix: int_to_str row added to builtins.rs::list()
  (hs.4 installed env.globals entry but missed the list() row).

Substantive deviation flagged by orchestrator (DONE_WITH_CONCERNS):
builtin signatures registered in uniqueness.rs::infer_module and
linearity.rs::check_module_with_visible (8 LOC × 2 files), symmetric
to iter 23.4-prep's class-method registration in the same globals
maps. Without this fix str_clone's param_modes: [Borrow] is invisible
to the App-arg walker, src_heap walks as Position::Consume, the
scope-close ailang_rc_dec is gated off, and the
str_clone_cross_realisation_uniform_abi test's plan-literal
`frees == 3` assertion does not hold. The fix is the substantively
correct repair, not a design departure.

9 new tests: 2 builtins-install unit, 2 IR-shape unit pins, 5 E2E
(2 RC-stats, 2 stdout-smoke for both Bool branches, 1 cross-
realisation). 4 new .ail.json fixtures.

Full cargo test --workspace: 513 passed, 0 failed.
bench/compile_check.py: 24/24 stable. bench/cross_lang.py: 25/25
stable.
2026-05-12 23:47:13 +02:00

11 KiB
Raw Blame History

iter 24.1 — bool_to_str + str_clone runtime + codegen wiring

Date: 2026-05-12 Started from: 8bfa09adc7 Status: DONE_WITH_CONCERNS Tasks completed: 6 of 6

Summary

First iter of milestone 24 ("Show + print rewire"). Wires two new heap-Str-producing primitives — bool_to_str : (Bool) -> Str and str_clone : (Str borrow) -> Str — through the runtime C code, type checker, codegen IR lowering, and IR-header preamble, mechanically mirror-imaging the hs.4 wiring for int_to_str / float_to_str. The two new symbols install lockstep in builtins.rs::install and synth.rs::builtin_value_type with ret_mode: Own; codegen gains two new IR-header declare lines, two Emitter::lower_app arms, and two is_static_callee whitelist entries. Plan's pre-existing-drift fix landed alongside: int_to_str row added to builtins.rs::list() (hs.4 added the env.globals entry but missed the list() row).

Nine new tests landed per plan: 2 builtins-install unit tests (install_bool_to_str_signature, install_str_clone_signature), 2 IR-shape unit pins (bool_to_str_emits_call_to_ailang_bool_to_str, str_clone_emits_call_to_ailang_str_clone), 5 E2E 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). Four .ail.json fixtures shipped. Five IR snapshots regenerated for the two new declare lines.

One substantive deviation from the plan's "purely-additive wiring" framing surfaced during Task 6 fixture validation: the str_clone_cross_realisation_uniform_abi test's literal assertion allocs == 3, frees == 3, live == 0 could not hold under the pre-24.1 uniqueness analyser because the analyser's globals map does not contain builtin signatures — str_clone's param_modes: [Borrow] is invisible to callee_arg_modes, so a src_heap let- binder consumed by str_clone(src_heap) walks as Position::Consume, incrementing consume_count and gating off the scope-close ailang_rc_dec. Fixed inline by registering builtin signatures in both uniqueness.rs::infer_module and linearity.rs::check_module_with_visible (8 lines × 2 files, symmetric to the existing class-method registration the linearity pass added in iter 23.4-prep). The fix is substantively correct (it applies a pre-existing pattern to a new class of callees) but is NOT in the plan's literal Edit text — see Concerns for the discussion.

Acceptance verified: full cargo test --workspace green (513 tests passing, 0 failures); the five new E2E tests pass with the plan's literal numeric assertions; the two new IR-shape pins pass; the five IR snapshot tests pass after UPDATE_SNAPSHOTS=1 regen (diff is exactly the two new declare ptr @ailang_bool_to_str(i1) and declare ptr @ailang_str_clone(ptr) lines). bench/compile_check.py 24/24 stable, 0 regressions. bench/cross_lang.py 25/25 stable, 0 regressions. Grep verification: 44 hits across runtime/str.c + builtins.rs + synth.rs + lib.rs (well above the plan's ≥ 10 threshold), 10 hits in snapshots (2 declares × 5 files), 0 build warnings.

Per-task notes

  • iter 24.1.1: Appended ailang_bool_to_str(bool b) -> char * and ailang_str_clone(const char *src) -> char * to runtime/str.c after ailang_float_to_str (str.c:128-143). Both use the existing static str_alloc(uint64_t) helper for heap-Str slab allocation; bool_to_str writes either "true" (4 bytes) or "false" (5 bytes); str_clone reads len from offset 0 of source and memcpy's bytes
    • trailing NUL to a fresh slab. Standalone clang -c compile green.
  • iter 24.1.2: Installed bool_to_str : (Bool) -> Str and str_clone : (Str borrow) -> Str in builtins.rs::install (builtins.rs:213-232, between the existing int_to_str block and the is_nan block), both with ret_mode: ParamMode::Own. Mirror- installed the same two signatures in synth.rs::builtin_value_type (synth.rs:181-194, lockstep). Added three rows to builtins.rs::list() (int_to_str drift fix from hs.4 + bool_to_str + str_clone). Two new unit tests (install_bool_to_str_signature, install_str_clone_signature) appended to builtins.rs::tests using the fallback explicit Term::Lit shape (plan-noted: lit_bool / lit_str helpers do not exist; only lit_int / lit_float are defined). Both new unit tests PASS.
  • iter 24.1.3: Codegen IR wiring. (a) Extended the IR-header preamble at lib.rs:539-548 with two unconditional declares declare ptr @ailang_bool_to_str(i1) and declare ptr @ailang_str_clone(ptr). (b) Inserted two new arms in Emitter::lower_app at lib.rs:1934-1969 (after the existing float_to_str arm), each emitting a direct call to the runtime C glue with arity check + lower_term+fresh_ssa+call triple matching int_to_str's structural shape. (c) Extended the is_static_callee builtin-callee whitelist at lib.rs:2133-2135 with bool_to_str and str_clone (strictly required so Term::Var { name: "bool_to_str" } reaches lower_app's chain instead of the UnknownVar fallback). Full workspace build clean.
  • iter 24.1.4: Two IR-shape unit pin tests (bool_to_str_emits_call_to_ailang_bool_to_str, str_clone_emits_call_to_ailang_str_clone) appended at the bottom of lib.rs::mod tests, mirroring the int_to_str_lowers_to_ailang_int_to_str_call template. Both PASS.
  • iter 24.1.5: Five IR snapshots (hello.ll, list.ll, max3.ll, sum.ll, ws_main.ll) regenerated via UPDATE_SNAPSHOTS=1. RED observed first as expected (5 ir_snapshot_* tests failed with "run UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_ to refresh"); GREEN after regen. Diff is exactly the two new declare lines. Each .ll file shows 2 matches for the new symbols.
  • iter 24.1.6: Four .ail.json fixtures created (bool_to_str_drop_rc.ail.json, bool_to_str_smoke_false.ail.json, str_clone_drop_rc.ail.json, str_clone_cross_realisation.ail.json) per the plan's verbatim JSON. Five new E2E tests appended to crates/ail/tests/e2e.rs after heap_str_repeated_print_balances_rc_stats. The str_clone_cross_realisation_uniform_abi test initially FAILED with frees=2, expected=3; root-caused to the uniqueness-analyser-globals scope gap (see Concerns); fixed by registering builtin signatures in uniqueness.rs::infer_module and linearity.rs::check_module_with_visible. After the fix all five new E2E tests PASS with the plan's literal numeric assertions. Full cargo test --workspace: 513 passed, 0 failed. bench/compile_check.py 24/24 stable. bench/cross_lang.py 25/25 stable. Grep verification per plan Step 9 met: 44 code-side hits, 10 snapshot hits, 0 build warnings.

Concerns

  • Analyser-globals registration extends the plan's "purely-additive" scope. The plan framed 24.1 as "Pure-additive wiring across four crates / files." The actual landed change extends to two more files (uniqueness.rs, linearity.rs) with 8 lines each — a fix to the uniqueness / linearity analyser-globals scope so the str_clone builtin's param_modes: [Borrow] is visible to the callee_arg_modes walker. Without the fix, the str_clone_cross_realisation_uniform_abi test's plan-literal assertion frees == 3 does not hold (one heap-Str slab leaks because the App-arg walker can't see the Borrow mode → walks src_heap as Consume → consume_count == 1 → drop emission gated off).

    The fix is symmetric to the iter 23.4-prep extension that registered class-method signatures in the same globals maps for the same reason (a builtin/class-method callee carrying param_modes would not be visible to the analyser, false-firing consume-while-borrowed lints AND, in this iter, leaking heap-Str slabs). The plan's verbatim Edit text did not include the fix; the plan's verbatim test assertion required it. Per the orchestrator- agent's "make the reasonable call and continue" mandate I applied the fix in-place; it is the substantively-correct repair, not a design departure.

    The hs.4 journal's "Known debt" item on this is partially closed by this iter (the App-side of the Borrow-walker visibility is now fixed for builtin callees). The eob.1 fix (effect-op args walk as Borrow regardless of callee param_modes) covered the Term::Do side; the corresponding Term::App side is now covered for builtins. User-fn Term::App callees still benefit from their own def being registered in the analyser's globals from the module's own def list (lines 113-115 in uniqueness.rs); imported user-fn callees benefit from the iter 23.5 visible-extra registration in linearity.rs (lines 220-238). So the only remaining unknown-callee case is one this iter does not encounter.

Known debt

  • str_clone could rc_inc on heap-Str input instead of allocating a fresh copy, but this requires distinguishing heap-Str from static-Str at runtime — a tagging scheme the codebase deliberately dropped post-hs.2 (UINT64_MAX sentinel removal). Out of scope per the parent spec's "str_clone as rc_header bump" entry.

  • bool_to_str as schema-if with two static-Str literals would avoid the per-call heap allocation but requires phi-of-static-Str through the drop-elision pipeline that is not load-bearing-ratified today. Open commitment per the parent spec.

Files touched

  • Modified (12 files):

    • runtime/str.c — appended ailang_bool_to_str (16 LOC incl. comment) and ailang_str_clone (18 LOC incl. comment)
    • crates/ailang-check/src/builtins.rs — two new env.globals.insert blocks (bool_to_str + str_clone), three new rows in list() (drift fix + two new), two new unit tests
    • crates/ailang-codegen/src/synth.rs — two new arms in builtin_value_type (lockstep partner of the checker insert)
    • crates/ailang-codegen/src/lib.rs — IR-header preamble extends with 2 new declare lines; 2 new lower_app arms; 2-entry is_static_callee whitelist extension; 2 new IR-shape pin tests
    • crates/ailang-check/src/uniqueness.rs — register builtin signatures in infer_module's globals map so callee_arg_modes resolves builtin param_modes correctly
    • crates/ailang-check/src/linearity.rs — register builtin signatures in check_module_with_visible's globals map for the same reason
    • crates/ail/tests/e2e.rs — 5 new E2E tests appended
    • crates/ail/tests/snapshots/hello.ll, crates/ail/tests/snapshots/list.ll, crates/ail/tests/snapshots/max3.ll, crates/ail/tests/snapshots/sum.ll, crates/ail/tests/snapshots/ws_main.ll — golden-IR regen via UPDATE_SNAPSHOTS=1; diff is exactly the two new declare ptr @ailang_bool_to_str(i1) and declare ptr @ailang_str_clone(ptr) lines
  • Created (4 files):

    • examples/bool_to_str_drop_rc.ail.json — RC-stats fixture for bool_to_str true (4-byte slab, single drop)
    • examples/bool_to_str_smoke_false.ail.json — stdout-smoke fixture for bool_to_str false (5-byte slab)
    • examples/str_clone_drop_rc.ail.json — RC-stats fixture for str_clone "hello" (static-Str input → fresh heap-Str clone)
    • examples/str_clone_cross_realisation.ail.json — cross- realisation invariant: clones one heap-Str (output of int_to_str 42) AND one static-Str (literal "abc"), printing both; pins the "uniform consumer ABI" claim

Stats

bench/orchestrator-stats/2026-05-12-iter-24.1.json