f38bad8c2b
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.
217 lines
11 KiB
Markdown
217 lines
11 KiB
Markdown
# iter 24.1 — `bool_to_str` + `str_clone` runtime + codegen wiring
|
||
|
||
**Date:** 2026-05-12
|
||
**Started from:** 8bfa09adc7774a1c94af609fc4c78a70ba77be48
|
||
**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
|