Files
AILang/docs/journals/2026-05-13-iter-str-concat.md
T
Brummel e7e67e1a40 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.
2026-05-13 12:43:10 +02:00

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-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.rsenv.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_concatformat_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_concatformat_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_concatformat_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