iter str-concat: heap-Str concatenation primitive in four-site lockstep
Closes fieldtest-form-a friction finding #4. `str_concat : (borrow Str, borrow Str) -> Str` ships in the four-site-lockstep pattern established by `str_clone` / `int_to_str` / `bool_to_str` (iter 24.1). The LLM-natural Show-MyType body `(app str_concat "label=" (app int_to_str x))` now parses, checks, builds, and runs end-to-end. Sites touched (lockstep): - runtime/str.c — `ailang_str_concat(a, b)` slab-allocates and memcpys both source payloads into a new heap-Str. - ailang-check/src/builtins.rs — `env.globals.insert("str_concat", Fn { 2x Str borrow, ret Str own, effects [] })` + `list()` row + `install_str_concat_signature` unit test. - ailang-codegen/src/lib.rs — `declare ptr @ailang_str_concat(ptr, ptr)` extern + `lower_app` arm after str_clone + `is_builtin_callable` extension + IR-pin unit test `str_concat_emits_call_to_ailang_str_concat`. - examples/show_user_adt_with_label.ail (new) + crates/ail/tests/ str_concat_e2e.rs (new) — corpus fixture exercising the LLM-natural Show body shape + E2E pin asserting check + build + run produce `Item 42\n`. Lockstep collision repaired: examples/bug_unbound_in_instance_method.ail used `str_concat` as its UNBOUND name (because that was the literal fieldtester repro). Renamed to `format_label` (LLM-author-realistic helper name that will never become a builtin) and updated the pin test `crates/ail/tests/unbound_in_instance_method_pin.rs` accordingly, preserving the regression guard's intent (instance-method-body walked through unbound-var check). DESIGN.md amended: new §"Heap-Str primitives" subsection between the milestone-24 Show-backer enumeration and the existing `Primitive output goes through ...` paragraph, cataloguing all five heap-Str primitives (`int_to_str`, `bool_to_str`, `float_to_str`, `str_clone`, `str_concat`) with signatures, iter origins, and the user-visible-vs-prelude-internal distinction. Show-backer block unchanged. IR snapshots regenerated (hello, list, max3, sum, ws_main) to absorb the new `declare ptr @ailang_str_concat(ptr, ptr)` line in the unconditional extern header — same upkeep pattern as hs.4 which regenerated the same 5 snapshots for the same reason (unconditional declares dead-stripped by clang -O2 when unused). Tests: 559 + 3 = 562 green (E2E pin + builtin-signature test + IR-pin test). Zero re-loops across all 7 tasks.
This commit is contained in:
@@ -1991,6 +1991,36 @@ Routing through `print` replaces the ad-hoc `io/print_int|bool|float`
|
||||
idiom for new code; retiring the per-type effect-ops is queued as a
|
||||
P2 follow-up.
|
||||
|
||||
### Heap-Str primitives
|
||||
|
||||
The runtime ships a small family of operations that produce or
|
||||
transform heap-allocated `Str` values uniformly across static-Str
|
||||
and heap-Str inputs (the consumer ABI is identical between
|
||||
realisations — see §"Str ABI"). All take their input(s) by `borrow`
|
||||
and return an owned `Str`. Each is registered as a builtin in
|
||||
`crates/ailang-check/src/builtins.rs`, lowered inline in
|
||||
`crates/ailang-codegen/src/lib.rs::lower_app` to a `call ptr @ailang_<name>`,
|
||||
and backed by a `runtime/str.c` C helper.
|
||||
|
||||
- `int_to_str : (borrow Int) -> Str` (iter 24.1) — decimal rendering
|
||||
of an `Int`. Backs `Show Int` in the prelude.
|
||||
- `bool_to_str : (borrow Bool) -> Str` (iter 24.1) — `"true"`/`"false"`.
|
||||
Backs `Show Bool` in the prelude.
|
||||
- `float_to_str : (borrow Float) -> Str` (iter 24.1, type-installed;
|
||||
codegen ships in a future iter per roadmap). Will back `Show Float`.
|
||||
- `str_clone : (borrow Str) -> Str` (iter 24.1) — allocates a fresh
|
||||
heap-Str copy of the input's bytes. Backs `Show Str` in the prelude.
|
||||
- `str_concat : (borrow Str, borrow Str) -> Str` (iter str-concat,
|
||||
2026-05-13) — combines two `Str` values into a single owned `Str`.
|
||||
General-purpose; commonly used in Show bodies for labelled output
|
||||
(`(app str_concat "label=" (app int_to_str x))`).
|
||||
|
||||
The four Show-backers above are not directly observable to the
|
||||
LLM-author writing a `Show <T>` instance — the prelude's instance
|
||||
bodies dispatch into them. `str_concat` IS directly observable
|
||||
because the LLM-author calls it explicitly when authoring an
|
||||
instance body that wants to combine fragments.
|
||||
|
||||
Primitive output goes through `io/print_int` / `io/print_bool` /
|
||||
`io/print_str` directly.
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# iter str-concat — heap-Str concatenation primitive in four-site lockstep
|
||||
|
||||
**Date:** 2026-05-13
|
||||
**Started from:** 679572a92d594d94b915d09fd60e7ef7f7bb83bd
|
||||
**Status:** DONE
|
||||
**Tasks completed:** 7 of 7
|
||||
|
||||
## Summary
|
||||
|
||||
`str_concat : (borrow Str, borrow Str) -> Str` shipped as a heap-Str
|
||||
primitive in the four-site-lockstep pattern established by `str_clone`
|
||||
(iter 24.1). Closes fieldtest-form-a friction finding #4. The
|
||||
LLM-natural Show-body shape
|
||||
`(app str_concat "label=" (app int_to_str x))` now parses, checks,
|
||||
builds, and runs end-to-end via the new
|
||||
`examples/show_user_adt_with_label.ail` fixture (`Item 42\n`
|
||||
through `print . MkItem 42`).
|
||||
|
||||
Net delta: +3 tests (559 baseline → 562 green): the E2E pin
|
||||
(`str_concat_e2e_show_user_adt_with_label`), the builtin-signature
|
||||
unit test (`install_str_concat_signature` asserting the arity-2
|
||||
Borrow-Borrow / Own-return shape directly on `env.globals`), and the
|
||||
codegen IR-pin (`str_concat_emits_call_to_ailang_str_concat`
|
||||
asserting both the extern declaration AND the call instruction in
|
||||
emitted IR). Five `crates/ail/tests/snapshots/*.ll` files were
|
||||
regenerated to absorb the new unconditional
|
||||
`declare ptr @ailang_str_concat(ptr, ptr)` line in the IR header
|
||||
preamble — same upkeep pattern as hs.4 (which regenerated the same
|
||||
5 snapshots for the unconditional `@ailang_int_to_str` /
|
||||
`@ailang_float_to_str` declares).
|
||||
|
||||
Task 5 repaired the bug-fixture / pin-test collision: the
|
||||
`bugfix-instance-body-unbound-var` iter (commit 77f584a) used
|
||||
`str_concat` as the literal unbound name because that was the
|
||||
LLM-natural shape the fieldtester reached for; renamed to
|
||||
`format_label` (LLM-author-realistic helper name) to preserve the
|
||||
pin's intent (instance-method-body walked through unbound-var check)
|
||||
while substituting a name that will never become a builtin.
|
||||
|
||||
## Per-task notes
|
||||
|
||||
- iter str-concat.1: RED fixture `examples/show_user_adt_with_label.ail`
|
||||
(single-ctor `Item Int` ADT + `Show Item` body producing labelled
|
||||
output) + E2E pin `crates/ail/tests/str_concat_e2e.rs` (check +
|
||||
build + run asserting `Item 42\n` stdout). Confirmed RED with
|
||||
`[unbound-var] prelude.Show: unknown identifier: 'str_concat'`.
|
||||
- iter str-concat.2: `runtime/str.c` C helper `ailang_str_concat`
|
||||
appended after `ailang_str_clone`; reads `len` from both source
|
||||
payloads, `str_alloc(la+lb)`, memcpys both bytes, NUL-terminates.
|
||||
Clean `clang -O2 -c` compile.
|
||||
- iter str-concat.3: `crates/ailang-check/src/builtins.rs` —
|
||||
`env.globals.insert("str_concat", ...)` install entry after
|
||||
str_clone block (arity-2, both Borrow, Own return); list() row
|
||||
`("str_concat", "(Str, Str) -> Str")` after str_clone row;
|
||||
`install_str_concat_signature` test reaches into `env.globals`
|
||||
directly to match the arity-2 / param_modes / ret_mode discipline
|
||||
(deeper than the sibling `synth_in_builtins_env`-based tests
|
||||
because str_concat is the first builtin with arity-2 Borrow params).
|
||||
- iter str-concat.4: `crates/ailang-codegen/src/lib.rs` four-site lockstep:
|
||||
extern declaration after `@ailang_str_clone`; lower_app arm after
|
||||
str_clone arm (arity-2 check + two `lower_term` + `fresh_ssa` +
|
||||
body push); `is_builtin_callable` list extended with `"str_concat"`;
|
||||
IR-pin test mirrors the sibling `str_clone_emits_call_to_ailang_str_clone`
|
||||
pattern (Module/FnDef/Term::Do scaffolding via `super::*` +
|
||||
`SCHEMA`, not the plan's `ailang_surface::parse` structural sketch —
|
||||
plan explicitly authorised this in Step 4 note). IR-pin test
|
||||
asserts both the extern declare AND the call site. RED→GREEN flip
|
||||
on the E2E pin happens at this task. Five IR snapshots regenerated
|
||||
via `UPDATE_SNAPSHOTS=1` to absorb the new declare line (sole
|
||||
diff verified at line 17 across all snapshots).
|
||||
- iter str-concat.5: bug-fixture / pin-test collision repaired —
|
||||
`examples/bug_unbound_in_instance_method.ail` line 14 rename
|
||||
`str_concat` → `format_label`; pin test
|
||||
`crates/ail/tests/unbound_in_instance_method_pin.rs` `replace_all`
|
||||
rename (one test name changed to
|
||||
`check_fires_unbound_var_for_format_label_in_instance_method_body`).
|
||||
Both pin tests pass; new fixture `ail check` reports
|
||||
`ok (23 symbols across 2 modules)` — matches the plan's empirical
|
||||
guess exactly.
|
||||
- iter str-concat.6: `docs/DESIGN.md` new §"Heap-Str primitives"
|
||||
subsection between the milestone-24 paragraph and the
|
||||
`Primitive output goes through ...` paragraph; lists all five
|
||||
shipping primitives with their type, iter origin, and prelude
|
||||
role, plus the user-visible vs prelude-internal distinction.
|
||||
- iter str-concat.7: `docs/roadmap.md` struck `[x] [feature] str_concat`
|
||||
entry at end of P2 cluster (right before `## P3 — Ideas`). All
|
||||
four cited fixtures (`show_user_adt_with_label`, `show_user_adt`,
|
||||
`test_22c_user_class_e2e`, `cmp_max_smoke`) `ail check` clean.
|
||||
`ail builtins | grep str_concat` returns `str_concat (Str, Str) -> Str`.
|
||||
|
||||
## Concerns
|
||||
|
||||
- Task 3 (Step 5) test-count prediction was off: plan expected
|
||||
`passed: 560 failed: 1` after registering the builtin but actual
|
||||
was `passed: 558 failed: 3`. Reason: the two
|
||||
`unbound_in_instance_method_pin` tests turn RED as soon as
|
||||
`str_concat` is registered in the checker, NOT only at codegen.
|
||||
The pins assert on `ail check`'s `[unbound-var]` diagnostic, which
|
||||
depends only on the checker registry. Implementation is correct;
|
||||
Task 5 fully repairs. No follow-up needed beyond noting the
|
||||
prediction-vs-actual gap.
|
||||
- Task 4 (Step 7) test-count prediction was off: plan expected
|
||||
`passed: 562 failed: 1` after codegen lands; actual after
|
||||
IR-snapshot regen was `passed: 560 failed: 2`. The difference is
|
||||
fully accounted for by the 2 still-RED pin tests (repaired in
|
||||
T5) and by the IR-snapshot regen being out-of-band of the
|
||||
per-task counters in the plan. Final state at T5 Step 5 is
|
||||
exactly `passed: 562 failed: 0`, matching the plan's iter target.
|
||||
- IR snapshot regeneration was not explicitly scripted in the plan
|
||||
for Task 4, but it follows the hs.4 precedent (which regenerated
|
||||
the same 5 snapshots for the same reason: a new unconditional
|
||||
declare line in the IR header). The sole diff in each snapshot
|
||||
is the new `declare ptr @ailang_str_concat(ptr, ptr)` line on
|
||||
IR line 17, immediately before the existing
|
||||
`declare i64 @llvm.fptosi.sat.i64.f64(double)` declaration.
|
||||
|
||||
## Known debt
|
||||
|
||||
- (none)
|
||||
|
||||
## Files touched
|
||||
|
||||
Code:
|
||||
- `runtime/str.c` (M) — append `ailang_str_concat` helper
|
||||
- `crates/ailang-check/src/builtins.rs` (M) — install entry, list()
|
||||
row, signature unit test
|
||||
- `crates/ailang-codegen/src/lib.rs` (M) — extern declare,
|
||||
lower_app arm, is_builtin_callable list, IR-pin unit test
|
||||
- `crates/ail/tests/str_concat_e2e.rs` (new) — E2E pin
|
||||
- `crates/ail/tests/unbound_in_instance_method_pin.rs` (M) — rename
|
||||
`str_concat` → `format_label` (4 sites + 1 test name)
|
||||
- `examples/show_user_adt_with_label.ail` (new) — Show body fixture
|
||||
- `examples/bug_unbound_in_instance_method.ail` (M) — rename
|
||||
`str_concat` → `format_label` (1 site)
|
||||
|
||||
IR snapshots regenerated (5; sole diff = new declare line):
|
||||
- `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`
|
||||
|
||||
Docs:
|
||||
- `docs/DESIGN.md` (M) — new §"Heap-Str primitives" subsection
|
||||
- `docs/roadmap.md` (M) — struck `[x] [feature] str_concat` line
|
||||
|
||||
## Stats
|
||||
|
||||
bench/orchestrator-stats/2026-05-13-iter-str-concat.json
|
||||
@@ -328,6 +328,15 @@ context. Pick the next milestone from P1.)_
|
||||
other consumer survives.
|
||||
- context: JOURNAL 2026-05-11 ("Iteration ct.4") — milestone close
|
||||
follow-up; closed by iter ctt.1 — recon confirmed the rebuild is the load-bearing consumer of in-band DuplicateCtor (pinned by `crates/ailang-check/tests/duplicate_ctor_pin.rs`); the asymmetry with the mono side is intentional.
|
||||
- [x] **\[feature\]** `str_concat : (borrow Str, borrow Str) -> Str` —
|
||||
heap-Str concatenation primitive. Shipped 2026-05-13 as iter
|
||||
str-concat (closes fieldtest-form-a friction #4). Symmetric to the
|
||||
iter 24.1 `str_clone` / `int_to_str` / `bool_to_str` plumbing:
|
||||
runtime C helper (`ailang_str_concat`), `ailang-check` builtin
|
||||
registration, `ailang-codegen` extern + `lower_app` arm, plus a
|
||||
fresh `examples/show_user_adt_with_label.ail` corpus fixture
|
||||
exercising the LLM-natural Show-body shape.
|
||||
- context: per-iter journal 2026-05-13-iter-str-concat.md.
|
||||
|
||||
## P3 — Ideas
|
||||
|
||||
|
||||
Reference in New Issue
Block a user