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.
7.3 KiB
iter str-concat — heap-Str concatenation primitive in four-site lockstep
Date: 2026-05-13
Started from: 679572a92d
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-ctorItem IntADT +Show Itembody producing labelled output) + E2E pincrates/ail/tests/str_concat_e2e.rs(check + build + run assertingItem 42\nstdout). Confirmed RED with[unbound-var] prelude.Show: unknown identifier: 'str_concat'. - iter str-concat.2:
runtime/str.cC helperailang_str_concatappended afterailang_str_clone; readslenfrom both source payloads,str_alloc(la+lb), memcpys both bytes, NUL-terminates. Cleanclang -O2 -ccompile. - 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_signaturetest reaches intoenv.globalsdirectly to match the arity-2 / param_modes / ret_mode discipline (deeper than the siblingsynth_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.rsfour-site lockstep: extern declaration after@ailang_str_clone; lower_app arm after str_clone arm (arity-2 check + twolower_term+fresh_ssa+ body push);is_builtin_callablelist extended with"str_concat"; IR-pin test mirrors the siblingstr_clone_emits_call_to_ailang_str_clonepattern (Module/FnDef/Term::Do scaffolding viasuper::*+SCHEMA, not the plan'sailang_surface::parsestructural 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 viaUPDATE_SNAPSHOTS=1to 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.ailline 14 renamestr_concat→format_label; pin testcrates/ail/tests/unbound_in_instance_method_pin.rsreplace_allrename (one test name changed tocheck_fires_unbound_var_for_format_label_in_instance_method_body). Both pin tests pass; new fixtureail checkreportsok (23 symbols across 2 modules)— matches the plan's empirical guess exactly. - iter str-concat.6:
docs/DESIGN.mdnew §"Heap-Str primitives" subsection between the milestone-24 paragraph and thePrimitive 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.mdstruck[x] [feature] str_concatentry 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 checkclean.ail builtins | grep str_concatreturnsstr_concat (Str, Str) -> Str.
Concerns
- Task 3 (Step 5) test-count prediction was off: plan expected
passed: 560 failed: 1after registering the builtin but actual waspassed: 558 failed: 3. Reason: the twounbound_in_instance_method_pintests turn RED as soon asstr_concatis registered in the checker, NOT only at codegen. The pins assert onail 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: 1after codegen lands; actual after IR-snapshot regen waspassed: 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 exactlypassed: 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 existingdeclare i64 @llvm.fptosi.sat.i64.f64(double)declaration.
Known debt
- (none)
Files touched
Code:
runtime/str.c(M) — appendailang_str_concathelpercrates/ailang-check/src/builtins.rs(M) — install entry, list() row, signature unit testcrates/ailang-codegen/src/lib.rs(M) — extern declare, lower_app arm, is_builtin_callable list, IR-pin unit testcrates/ail/tests/str_concat_e2e.rs(new) — E2E pincrates/ail/tests/unbound_in_instance_method_pin.rs(M) — renamestr_concat→format_label(4 sites + 1 test name)examples/show_user_adt_with_label.ail(new) — Show body fixtureexamples/bug_unbound_in_instance_method.ail(M) — renamestr_concat→format_label(1 site)
IR snapshots regenerated (5; sole diff = new declare line):
crates/ail/tests/snapshots/hello.llcrates/ail/tests/snapshots/list.llcrates/ail/tests/snapshots/max3.llcrates/ail/tests/snapshots/sum.llcrates/ail/tests/snapshots/ws_main.ll
Docs:
docs/DESIGN.md(M) — new §"Heap-Str primitives" subsectiondocs/roadmap.md(M) — struck[x] [feature] str_concatline
Stats
bench/orchestrator-stats/2026-05-13-iter-str-concat.json