From 51a096cf8354362783d8caf2ec4b35179d935a7a Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 12 May 2026 17:45:23 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20hs.2=20=E2=80=94=20rewrite=20for=20stat?= =?UTF-8?q?ic-Str=20sentinel-slot=20retrofit,=203=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the defunct hs.2 plan at 8ca602d (runtime-sentinel + Str-in-ADT drop safety). After the spec amendment at 2a72a4a dropped the sentinel-rc-header story, the iter scope is now a surgical retrofit of hs.1's emission: drop the leading i64 sentinel slot from static-Str globals, land the IR-Str pointer on the (now first) len-field via GEP (i32 0, i32 0). The four +8 consumer GEPs are invariant across the switch. Three tasks: (1) update the two IR-shape tests to the new layout (RED); (2) flip three format-string sites + two doc-comments in lib.rs (GREEN); (3) regenerate hello.ll snapshot + workspace + cross_lang + compile_check + check. --- docs/plans/hs.2.md | 782 ++++++++++++++++++++++++--------------------- 1 file changed, 422 insertions(+), 360 deletions(-) diff --git a/docs/plans/hs.2.md b/docs/plans/hs.2.md index a744efd..be2fc49 100644 --- a/docs/plans/hs.2.md +++ b/docs/plans/hs.2.md @@ -1,418 +1,480 @@ -# hs.2 — Heap-Str ABI: runtime sentinel + Str-in-ADT drop safety — Implementation Plan +# hs.2 — Static-Str sentinel-slot retrofit — Implementation Plan > **Parent spec:** `docs/specs/2026-05-12-heap-str-abi.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. -**Goal:** Make `ailang_rc_inc` and `ailang_rc_dec` short-circuit on -a `UINT64_MAX` sentinel header, so the static-`Str` slabs emitted -by hs.1's packed-struct layout (whose first slot is `i64 -1`) flow -safely through generic RC paths instead of attempting to write -back to `.rodata` memory. Add the load-bearing safety test -`str_field_in_adt_drops_static_str_noop`, which is the first test -in the project to exercise the path "ADT with a `Str` field, -instantiated with a static literal, dropped under `--alloc=rc`". +**Goal:** Retrofit hs.1's emitted static-`Str` LLVM globals from the +sentinel-rc-header layout `<{i64, i64, [N x i8]}> <{i64 -1, i64 N, c"…\0"}>` +to the amended-spec layout `<{i64, [N x i8]}> <{i64 N, c"…\0"}>`. The +sentinel rc-header slot is removed; the explicit `len` field becomes +the first (and only non-byte) field of the packed struct, on which the +IR-`Str` pointer lands. The `+8` consumer-side GEPs at the four C-API +callsites do **not** change. -**Architecture:** Pure runtime-side change plus one new fixture -and one new e2e test. The hs.1 codegen change emits static-Str -globals whose first `i64` slot is `-1` (= `UINT64_MAX` when read -as `uint64_t`). The hs.1 IR-Str-pointer convention places the -flowing pointer at the `len`-field of the struct, so -`header_of(payload) = payload - 8` lands on that sentinel slot. -Pre-hs.2, `ailang_rc_dec(static_str_field_ptr)` reads -`*hdr = UINT64_MAX` (passes the underflow check at `runtime/rc.c:166` -since `UINT64_MAX != 0`), then `*hdr -= 1` attempts to write -`UINT64_MAX - 1` to the read-only data section → SIGSEGV. hs.2 -inserts a sentinel check that short-circuits both functions -before either reads or writes the header value any further, making -the static-`Str` case a defined no-op. `ailang_rc_alloc`'s counters -are untouched on the short-circuit path (`g_rc_alloc_count` only -fires inside `ailang_rc_alloc` itself; `g_rc_free_count` only fires -on the to-zero branch in `ailang_rc_dec`, after the sentinel check) -— so `AILANG_RC_STATS`-based tests continue to report accurate -allocs/frees for the heap path while seeing zero traffic from -static `Str` literals. +**Architecture:** Three format-string sites in `crates/ailang-codegen/src/lib.rs` +(one global emission, two `Literal::Str` callsite GEPs) flip from the +3-field struct shape to the 2-field shape and from GEP-index `(0,1)` to +`(0,0)`. Two IR-shape tests in the same crate's test module update +their asserts (one also renames). The `crates/ail/tests/snapshots/hello.ll` +snapshot regenerates via `UPDATE_SNAPSHOTS=1`. No runtime changes, no +checker changes, no new fixtures. -**Tech Stack:** C runtime (`runtime/rc.c`), AILang fixture -(`examples/`), Rust e2e harness (`crates/ail/tests/e2e.rs`). +**Tech Stack:** `crates/ailang-codegen` (Rust, LLVM-IR string emission), +`crates/ail` (snapshot test harness). Pure Rust; no C runtime touched. + +**Recon note:** Recon agent bypassed per `CLAUDE.md` "When NOT to +delegate" — the change surface is five string-literal sites in one +file plus one snapshot file, and all five sites were located and +quoted verbatim from disk during plan-writing. --- ## Files this plan creates or modifies -- **Create:** `examples/rc_str_field_in_adt_static.ail.json` — new - fixture mirroring `examples/rc_box_drop.ail.json` shape with a - `Str` field in place of an `Int` field. Exercises the - static-`Str`-in-ADT drop path under `--alloc=rc`. -- **Modify:** `runtime/rc.c:132-145` — add sentinel-header - short-circuit to `ailang_rc_inc`. -- **Modify:** `runtime/rc.c:161-177` — add sentinel-header - short-circuit to `ailang_rc_dec`. -- **Test:** `crates/ail/tests/e2e.rs` (append at end of file, after - current line 2561) — new test `str_field_in_adt_drops_static_str_noop` - using the existing `build_and_run_with_rc_stats` helper at - `crates/ail/tests/e2e.rs:2093-2150`. +- Modify: `crates/ailang-codegen/src/lib.rs:462-475` — global-emission + loop (the `all_str_literals` aggregation that writes + `@ = private unnamed_addr constant <{ i64, i64, [...] }> ...`) +- Modify: `crates/ailang-codegen/src/lib.rs:924-937` — first + `Literal::Str` arm in `emit_const_def`, constexpr-GEP format string +- Modify: `crates/ailang-codegen/src/lib.rs:1246-1259` — second + `Literal::Str` arm in `lower_term`, constexpr-GEP format string +- Modify: `crates/ailang-codegen/src/lib.rs:561-565` — doc-comment on + the `str_literals` field describing the layout (mentions + `UINT64_MAX` sentinel rc-header; that detail must go) +- Modify: `crates/ailang-codegen/src/lib.rs:2596-2600` — doc-comment on + `intern_str_literal` describing the layout (same correction) +- Modify: `crates/ailang-codegen/src/lib.rs:3808-3849` — IR-shape test + `static_str_global_uses_packed_struct_with_sentinel_and_len`: rename + + doc-comment update + assertion-substring update +- Modify: `crates/ailang-codegen/src/lib.rs:3851-3885` — IR-shape test + `static_str_callsite_pointer_is_payload_via_constexpr_gep`: + doc-comment update + assertion-substring update +- Modify: `crates/ail/tests/snapshots/hello.ll` — regenerate via + `UPDATE_SNAPSHOTS=1`; expected bytes flip from sentinel-prefixed + to len-prefixed shape +- Untouched: the four `+8 GEP` pinning tests + (`print_str_calls_puts_with_bytes_pointer`, + `eq_str_calls_ail_str_eq_with_bytes_pointer`, + `compare_str_calls_ail_str_compare_with_bytes_pointer`, + `lower_eq_str_calls_strcmp_with_bytes_pointer`) — consumer ABI is + invariant across the layout switch +- Untouched: any `runtime/*.c` — no runtime change +- Untouched: any test fixture under `examples/` — observable program + output is invariant --- -## Task 1: Author the static-Str-in-ADT fixture and confirm it builds - -The new test in Task 2 requires a `.ail.json` fixture exercising -`type StrBox = | MkStrBox(Str)` with a static-`Str` payload under -`--alloc=rc`. This task creates that fixture and confirms it -typechecks and builds without errors (running the binary is -deferred to Task 2 — pre-hs.2 the binary would crash under -`--alloc=rc`). +## Task 1 — Update IR-shape tests to the amended-spec layout (RED) **Files:** -- Create: `examples/rc_str_field_in_adt_static.ail.json` +- Modify: `crates/ailang-codegen/src/lib.rs:3808-3885` -- [ ] **Step 1: Create the fixture.** +- [ ] **Step 1: Rename and rewrite assert in + `static_str_global_uses_packed_struct_with_sentinel_and_len`** -Write the file at `examples/rc_str_field_in_adt_static.ail.json` -with this exact content, modelled verbatim on -`examples/rc_box_drop.ail.json` with `Int` → `Str` and -`io/print_int` → `io/print_str`: - -```json -{ - "schema": "ailang/v0", - "name": "rc_str_field_in_adt_static", - "imports": [], - "defs": [ - { - "kind": "type", - "name": "StrBox", - "vars": [], - "doc": "Single-cell ADT around a Str. Iter hs.2 RC fixture: exercises the static-Str-in-ADT drop path under --alloc=rc. Pre-hs.2 the binary segfaults at drop time because `ailang_rc_dec(static_str_payload_ptr)` reads UINT64_MAX from the sentinel header slot and attempts to write UINT64_MAX-1 to .rodata. Post-hs.2 `ailang_rc_dec` short-circuits on the sentinel; the drop is a defined no-op and the outer StrBox cell frees normally.", - "ctors": [ - { - "name": "MkStrBox", - "fields": [{ "k": "con", "name": "Str" }] - } - ] - }, - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Unit" }, - "effects": ["IO"] - }, - "params": [], - "doc": "Bind a heap-allocated MkStrBox(\"hello\") to `b`, read its Str via match, print it. Under --alloc=rc the let-scope close calls drop_t_StrBox(b), which in turn calls ailang_rc_dec on the Str field — that pointer is the static-Str literal global from hs.1, so the sentinel-header short-circuit must make this safe.", - "body": { - "t": "let", - "name": "b", - "value": { - "t": "ctor", - "type": "StrBox", - "ctor": "MkStrBox", - "args": [{ "t": "lit", "lit": { "kind": "str", "value": "hello" } }] - }, - "body": { - "t": "match", - "scrutinee": { "t": "var", "name": "b" }, - "arms": [ - { - "pat": { - "p": "ctor", - "ctor": "MkStrBox", - "fields": [{ "p": "var", "name": "s" }] - }, - "body": { - "t": "do", - "op": "io/print_str", - "args": [{ "t": "var", "name": "s" }] - } - } - ] - } - } - } - ] -} -``` - -- [ ] **Step 2: Confirm the fixture typechecks.** - -Run: `cargo run --bin ail --quiet -- check examples/rc_str_field_in_adt_static.ail.json` -Expected: exit 0, no diagnostics on stderr. The fixture follows -the canonical-form schema verbatim; any diagnostic would indicate -a copy-paste error. - -- [ ] **Step 3: Confirm the fixture builds under `--alloc=rc`.** - -Run: `cargo run --bin ail --quiet -- build examples/rc_str_field_in_adt_static.ail.json --alloc=rc -o /tmp/hs2_fixture_build_check` -Expected: exit 0; a binary appears at `/tmp/hs2_fixture_build_check`. -**Do NOT execute the binary in this step** — pre-hs.2 it segfaults -on the static-Str field drop. The Task 2 RED-test step runs it. - ---- - -## Task 2: Sentinel-header short-circuit in `ailang_rc_inc` and `ailang_rc_dec` (TDD) - -This is the heart of the iter. The RED test is the -spec's load-bearing `str_field_in_adt_drops_static_str_noop` -safety gate — until hs.2's runtime change lands, no test in the -project exercises this path, and the path is latent UB. - -**Files:** -- Modify: `runtime/rc.c:132-145, 161-177` -- Test: `crates/ail/tests/e2e.rs` (append after current line 2561) - -- [ ] **Step 1: Write the failing test `str_field_in_adt_drops_static_str_noop`.** - -Append to `crates/ail/tests/e2e.rs` at the end of the file: +Locate the test at `crates/ailang-codegen/src/lib.rs:3816`. Replace the +test function name, its doc-comment immediately above (lines +3810-3814), and the inner `assert!`-substring. After the edit the test +reads: ```rust -/// Iter hs.2 load-bearing safety gate. The hs.1 layout migration -/// turned the path "ADT-with-Str-field instantiated with a static -/// literal, dropped under --alloc=rc" into latent UB: the -/// per-type drop fn calls `ailang_rc_dec(field_ptr)` on the -/// static-Str payload pointer, which under hs.1 lands on the -/// `len`-field of the packed-struct global; `header_of(ptr) = -/// ptr - 8` reads the sentinel slot (= UINT64_MAX); the -/// underflow check `*hdr == 0` passes (UINT64_MAX != 0); then -/// `*hdr -= 1` attempts to write to the read-only data section. -/// hs.2 short-circuits both `ailang_rc_inc` and `ailang_rc_dec` -/// on the sentinel before the header is mutated, making the -/// path a defined no-op. This test is the first in the project -/// to exercise it: pre-hs.2 the binary segfaults; post-hs.2 it -/// reports `live=0` and exits cleanly. +/// Iter hs.2: language `Str` literals emit as packed-struct globals +/// carrying an explicit `len` field followed by the bytes + trailing +/// NUL. (The hs.1-era sentinel rc-header slot was removed per the +/// amended spec; static-Str pointers are kept out of `ailang_rc_dec` +/// at the codegen level via move-tracking and non-escape lowering, +/// so no header slot is needed.) This test pins the layout shape +/// against a tiny single-`Literal::Str` fixture. #[test] -fn str_field_in_adt_drops_static_str_noop() { - let (stdout, allocs, frees, live) = - build_and_run_with_rc_stats("rc_str_field_in_adt_static.ail.json"); - assert_eq!( - stdout.trim(), - "hello", - "fixture must print the matched Str payload" - ); - assert_eq!( - live, 0, - "static-Str-in-ADT drop must not leak; live={live} \ - (allocs={allocs} frees={frees}). The sentinel short-circuit \ - in ailang_rc_dec for static-Str field pointers must not \ - increment g_rc_free_count, and the outer StrBox cell must \ - drop normally." +fn static_str_global_uses_packed_struct_with_len() { + let m = Module { + schema: SCHEMA.into(), + name: "t".into(), + imports: vec![], + defs: vec![ + Def::Const(ConstDef { + name: "greeting".into(), + ty: Type::str_(), + value: Term::Lit { lit: Literal::Str { value: "hello".into() } }, + doc: None, + }), + Def::Fn(FnDef { + name: "main".into(), + ty: Type::Fn { + params: vec![], + ret: Box::new(Type::unit()), + effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, + }, + params: vec![], + body: Term::Lit { lit: Literal::Unit }, + suppress: vec![], + doc: None, + }), + ], + }; + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains(r#"<{ i64, [6 x i8] }> <{ i64 5, [6 x i8] c"hello\00" }>"#), + "expected packed-struct global with len + bytes + NUL; ir was:\n{ir}" ); } ``` -- [ ] **Step 2: Run the test to verify it fails.** +The two changes versus hs.1: (a) function name drops the +`_with_sentinel_and_len` qualifier and gains `_with_len`; (b) the +assert-substring drops the leading `i64, ` from the struct type and +the leading `i64 -1, ` from the initialiser. -Run: `cargo test --workspace -p ail str_field_in_adt_drops_static_str_noop` -Expected: FAIL. The most likely failure mode is the assertion -`output.status.success()` inside `build_and_run_with_rc_stats` -at `crates/ail/tests/e2e.rs:2119-2123` panicking with -`binary (--alloc=rc, AILANG_RC_STATS=1) exited non-zero`, -because the binary either SIGSEGVs writing to `.rodata` or -gets caught by `ailang_rc_dec`'s `abort()` on a write attempt. -A secondary possible failure mode is the rc-stats line being -absent from stderr because the program crashed before the -`atexit` handler ran — in that case the panic is at -`crates/ail/tests/e2e.rs:2129-2131` -(`missing ailang_rc_stats line in stderr`). Either failure -shape is the expected RED state; both go GREEN once Step 3 -lands. +- [ ] **Step 2: Update assert in + `static_str_callsite_pointer_is_payload_via_constexpr_gep`** -- [ ] **Step 3: Add the sentinel-header short-circuit to `ailang_rc_inc`.** +Locate the test at `crates/ailang-codegen/src/lib.rs:3856`. The +function name stays — "payload" remains correct since the IR-Str +pointer still lands on the `len`-field slot, which is the payload +start. Replace its doc-comment (lines 3851-3854) and the inner +`assert!`-substring: -Modify `runtime/rc.c:132-145`. The current body is: - -```c -void ailang_rc_inc(void *payload) { - if (payload == NULL) { - return; - } - /* Static closure-pair env pointers (Iter 8b) live in the LLVM data - * segment, not in heap memory we allocated. Codegen elides inc/dec - * for known-static pointers (the `@`-prefix gate added in 18c.3), - * so this path is not reached for them in practice. The runtime - * itself has no header-bit flag distinguishing static from heap; - * if codegen ever loses the elision, inc on a static pointer is - * undefined behaviour. */ - ailang_rc_header_t *hdr = header_of(payload); - *hdr += 1; +```rust +/// Iter hs.2: a `Literal::Str` at a callsite materialises a constexpr +/// `getelementptr` landing on the `len`-field of the packed-struct +/// global (now the *first* field, since the hs.1-era sentinel +/// rc-header slot was removed per the amended spec). This pins the +/// IR-Str-pointer convention used uniformly by all consumers. +#[test] +fn static_str_callsite_pointer_is_payload_via_constexpr_gep() { + // ... body unchanged ... + assert!( + ir.contains(r#"getelementptr inbounds (<{ i64, [3 x i8] }>, ptr @.str_t_str_0, i32 0, i32 0)"#), + "expected constexpr-GEP-to-len-field at callsite; ir was:\n{ir}" + ); } ``` -Replace with: +The two changes versus hs.1: (a) struct type drops the leading `i64, `; +(b) GEP indices change from `i32 0, i32 1` to `i32 0, i32 0`. -```c -void ailang_rc_inc(void *payload) { - if (payload == NULL) { - return; - } - /* Static closure-pair env pointers (Iter 8b) live in the LLVM data - * segment, not in heap memory we allocated. Codegen elides inc/dec - * for known-static pointers (the `@`-prefix gate added in 18c.3), - * so this path is not reached for them in practice. The runtime - * itself has no header-bit flag distinguishing static from heap; - * if codegen ever loses the elision, inc on a static pointer is - * undefined behaviour. */ - ailang_rc_header_t *hdr = header_of(payload); - /* Iter hs.2: static-Str literal slabs (emitted by the codegen as - * packed-struct globals in iter hs.1) carry the sentinel header - * value UINT64_MAX in the slot at `payload - 8`. Such slabs live - * in the `.rodata` section; their refcount must never be mutated. - * Codegen does not elide inc/dec on static-Str field pointers - * (the `@`-prefix elision in 18c.3 covered only closure-pair - * env pointers, not Str payload pointers materialised via - * constexpr GEP). The sentinel-header short-circuit makes inc - * a defined no-op for such pointers. The same convention - * applies to `ailang_rc_dec` below. */ - if (*hdr == UINT64_MAX) { - return; - } - *hdr += 1; -} +- [ ] **Step 3: Run both updated tests and confirm they fail RED** + +Run: +``` +cargo test -p ailang-codegen --lib static_str_ ``` -- [ ] **Step 4: Add the sentinel-header short-circuit to `ailang_rc_dec`.** +Expected: both tests **FAIL** with assertion-mismatch messages — the +emitted IR still carries the hs.1 sentinel-prefixed shape; the new +asserts demand the hs.2 len-only shape. The failure messages will +print the actual IR so the substring mismatch is visible. -Modify `runtime/rc.c:161-177`. The current body is: +- [ ] **Step 4: Confirm the four `+8 GEP` tests still pass under hs.1 + emission** -```c -void ailang_rc_dec(void *payload) { - if (payload == NULL) { - return; - } - ailang_rc_header_t *hdr = header_of(payload); - if (*hdr == 0) { - fprintf(stderr, - "ailang_rc_dec: refcount underflow at %p (already zero)\n", - payload); - abort(); - } - *hdr -= 1; - if (*hdr == 0) { - free(hdr); - g_rc_free_count++; - } -} +Run: +``` +cargo test -p ailang-codegen --lib _calls_ ``` -Replace with: - -```c -void ailang_rc_dec(void *payload) { - if (payload == NULL) { - return; - } - ailang_rc_header_t *hdr = header_of(payload); - /* Iter hs.2: static-Str literal slabs carry the sentinel - * UINT64_MAX in the header slot and live in `.rodata`. - * Short-circuit before the underflow check (UINT64_MAX != 0 - * would otherwise pass the underflow guard and reach the - * read-modify-write at `*hdr -= 1`, segfaulting on `.rodata`). - * See the matching comment in `ailang_rc_inc` above. */ - if (*hdr == UINT64_MAX) { - return; - } - if (*hdr == 0) { - fprintf(stderr, - "ailang_rc_dec: refcount underflow at %p (already zero)\n", - payload); - abort(); - } - *hdr -= 1; - if (*hdr == 0) { - free(hdr); - g_rc_free_count++; - } -} -``` - -- [ ] **Step 5: Confirm `UINT64_MAX` is reachable in `runtime/rc.c`.** - -Run: `grep -n '#include ' runtime/rc.c` -Expected: a single line `48:#include ` (or equivalent). -`UINT64_MAX` is provided by ``, which is already -included by `runtime/rc.c:48`. No new include is needed. -If for some reason the include is missing, add `#include ` -to the include block at the top of the file. - -- [ ] **Step 6: Run the test to verify it passes.** - -Run: `cargo test --workspace -p ail str_field_in_adt_drops_static_str_noop` -Expected: PASS. The fixture's binary runs cleanly, prints -`hello`, the atexit handler reports `allocs=N frees=N live=0` -for the heap-side StrBox cell (one alloc for the StrBox outer, -one matching free at let-scope close), and the test's -`assert_eq!(live, 0, ...)` succeeds. +Expected: `print_str_calls_puts_with_bytes_pointer`, +`eq_str_calls_ail_str_eq_with_bytes_pointer`, +`compare_str_calls_ail_str_compare_with_bytes_pointer`, +`lower_eq_str_calls_strcmp_with_bytes_pointer` all **PASS**. These tests +assert only on the `i64 8` byte offset and the consumed-symbol name; +they do not depend on the struct shape and must remain green. --- -## Task 3: Full regression sweep - -Confirm the runtime change broke nothing else. The sentinel -short-circuit adds one early-return branch in two hot-path -functions; in principle the only behavioural effect is on -pointers whose header equals `UINT64_MAX`, which today is only -the static-Str case. Belt-and-braces verify against every other -RC-discipline test and against the cross-language reference -corpus. +## Task 2 — Switch emission and callsite GEPs to the amended-spec layout (GREEN) **Files:** -- (none — read-only verification) +- Modify: `crates/ailang-codegen/src/lib.rs:462-475` +- Modify: `crates/ailang-codegen/src/lib.rs:924-937` +- Modify: `crates/ailang-codegen/src/lib.rs:1246-1259` +- Modify: `crates/ailang-codegen/src/lib.rs:561-565` +- Modify: `crates/ailang-codegen/src/lib.rs:2596-2600` -- [ ] **Step 1: Run the full cargo test workspace.** +- [ ] **Step 1: Switch the global-emission loop (single source of the + `i64 -1` sentinel literal)** -Run: `cargo test --workspace` -Expected: PASS, all tests green. Specifically, the existing -`alloc_rc_*` tests (12 fixtures exercising heap allocations -of various ADTs under `--alloc=rc`) continue to report -matching allocs/frees because none of them carry a `Str` -field and therefore none of them ever calls -`ailang_rc_dec(static_str_ptr)`. +Locate the loop at `crates/ailang-codegen/src/lib.rs:466-475`. Replace +the comment (lines 462-465) and the format string (line 472): -- [ ] **Step 2: Run `bench/compile_check.py`.** +```rust + // Iter hs.2: packed-struct globals for language `Str` literals. + // First `i64` is the byte length (excluding the trailing NUL); the + // `[N+1 x i8]` carries the bytes followed by the terminating NUL. + // The IR-`Str` pointer that flows through the rest of codegen lands + // on the `len`-field via constexpr-GEP at the `Literal::Str` arms + // (see emit_const_def / lower_term). Static-Str pointers are kept + // out of `ailang_rc_dec` by codegen-level elision (move-tracking + // from iter 18d.3 + non-escape lowering from iter 18b), so no + // sentinel rc-header slot is needed at the global. + for entries in all_str_literals.values() { + for (name, content) in entries { + let escaped = ll_string_literal(content); + let total = c_byte_len(content); // bytes + NUL + let bytes_len = total - 1; // bytes only + out.push_str(&format!( + "@{name} = private unnamed_addr constant <{{ i64, [{total} x i8] }}> <{{ i64 {bytes_len}, [{total} x i8] c\"{escaped}\" }}>, align 8\n", + )); + emitted_global = true; + } + } +``` -Run: `python3 bench/compile_check.py` -Expected: every `examples/` program — including the new -`rc_str_field_in_adt_static.ail.json` — compiles without -errors or warnings; the script exits 0. +Diff versus hs.1: comment loses the `i64 -1`/sentinel paragraph and +gains the codegen-elision justification; format string loses the +leading `i64, ` from the struct type and the leading `i64 -1, ` from +the initialiser. -- [ ] **Step 3: Run `bench/cross_lang.py`.** +- [ ] **Step 2: Switch the first `Literal::Str` callsite GEP (in + `emit_const_def`)** -Run: `python3 bench/cross_lang.py` -Expected: every fixture in `bench/reference/` produces -byte-identical stdout from its AILang and C implementations; -the script exits 0. None of these fixtures carries an -ADT-with-`Str`-field today, so the sentinel short-circuit -is not exercised; the test confirms no orthogonal regression -crept in. +Locate the arm at `crates/ailang-codegen/src/lib.rs:924-937`. Replace +the comment and the format string: + +```rust + Literal::Str { value } => { + // Iter hs.2: emit a packed-struct global and return a + // constexpr-GEP pointer landing on the `len`-field (now + // the first field of the packed struct, since the + // hs.1-era sentinel rc-header slot was removed). Every + // IR-Str pointer in this codegen pipeline has the + // shape len at offset 0, bytes at offset 8. + let g = self.intern_str_literal("str", value); + let total = c_byte_len(value); // bytes + NUL + ( + "ptr".to_string(), + format!( + "getelementptr inbounds (<{{ i64, [{total} x i8] }}>, ptr @{g}, i32 0, i32 0)", + ), + ) + } +``` + +Diff versus hs.1: comment drops the "header at -8" phrase; struct type +in the GEP loses the leading `i64, `; GEP indices change from +`i32 0, i32 1` to `i32 0, i32 0`. + +- [ ] **Step 3: Switch the second `Literal::Str` callsite GEP (in + `lower_term`)** + +Locate the arm at `crates/ailang-codegen/src/lib.rs:1246-1259`. Replace +the comment and the format string (the two arms are intentionally +parallel; the edits are symmetric): + +```rust + Literal::Str { value } => { + // Iter hs.2: language `Str` literals materialise as + // a constexpr-GEP into the packed-struct global, + // landing on the `len`-field (now the first field, + // since the hs.1-era sentinel rc-header slot was + // removed). IR-Str pointer carries len at 0, bytes + // at +8. + let g = self.intern_str_literal("str", value); + let total = c_byte_len(value); // bytes + NUL + ( + format!( + "getelementptr inbounds (<{{ i64, [{total} x i8] }}>, ptr @{g}, i32 0, i32 0)", + ), + "ptr".into(), + ) + } +``` + +Same diff shape as Step 2. + +- [ ] **Step 4: Update the two doc-comments that describe the layout** + +Two doc-comments still describe the hs.1 layout. Update both: + +`crates/ailang-codegen/src/lib.rs:561-565` (on the `str_literals` +field): +```rust + /// Iter hs.1 (amended hs.2): language `Str` literals interned as + /// packed-struct globals (`<{ i64, [N+1 x i8] }>`) carrying an + /// explicit `len` field and the bytes + trailing NUL. Parallel to + /// `strings` (which still serves runtime-internal format strings +``` + +`crates/ailang-codegen/src/lib.rs:2596-2600` (on +`intern_str_literal`): +```rust + /// Iter hs.1 (amended hs.2): parallel to `intern_string`, but for + /// language `Str` literals emitted as packed-struct globals + /// (len + bytes + NUL). Shares the same monotonic `str_counter` + /// so the produced global names remain alphabetically orderable + /// alongside format-string globals. +``` + +Both edits remove only the "sentinel + " phrase and (in the second +case) the "UINT64_MAX sentinel rc-header" phrase; the structural +description is otherwise unchanged. + +- [ ] **Step 5: Run both updated tests and confirm they pass GREEN** + +Run: +``` +cargo test -p ailang-codegen --lib static_str_ +``` + +Expected: both `static_str_global_uses_packed_struct_with_len` and +`static_str_callsite_pointer_is_payload_via_constexpr_gep` **PASS**. + +- [ ] **Step 6: Re-run the four `+8 GEP` tests and confirm they still + pass** + +Run: +``` +cargo test -p ailang-codegen --lib _calls_ +``` + +Expected: all four `_calls_*_with_bytes_pointer` tests **PASS** — the ++8 offset is invariant across the layout switch (the bytes still sit +8 bytes past the len-field; the only thing the layout-switch moved is +where the IR-Str pointer was previously landing, and the IR-Str pointer +still lands on the `len`-field — just at struct-index 0 instead of 1). + +- [ ] **Step 7: Run the full ailang-codegen test suite for any + collateral** + +Run: +``` +cargo test -p ailang-codegen +``` + +Expected: all tests **PASS**. Tests not touched by Tasks 1-2 (drop +walks, ADT lowering, match, eq, compare) do not reference the static- +Str global shape and must remain green. If any test fails, it likely +contains a literal substring assert that quotes the hs.1 struct shape; +patch the assert in place using the same shape transformation +(`i64, i64, [...]` → `i64, [...]`; `i64 -1, i64 N` → `i64 N`; +`i32 0, i32 1` → `i32 0, i32 0`). --- -## Notes for the implementer +## Task 3 — Regenerate hello.ll snapshot + workspace regression sweep -- The hs.1 packed-struct globals emit `i64 -1` for the sentinel - slot. When read as `uint64_t` this is bit-for-bit equivalent to - `UINT64_MAX` (`0xFFFFFFFFFFFFFFFF` in both signed twos-complement - -1 and unsigned UINT64_MAX representations). The codegen-side - emits `-1`; the runtime-side checks against `UINT64_MAX`. Both - refer to the same 64-bit pattern. +**Files:** +- Modify: `crates/ail/tests/snapshots/hello.ll` -- Beyond the sentinel-Str case, the only way a real refcount could - reach `UINT64_MAX` is via `2^64` independent references — which - would consume more memory than exists on any machine. The - collision-with-real-refcount case is structurally impossible, - not just unlikely. +- [ ] **Step 1: Inspect the current snapshot** -- The block-comment at `runtime/rc.c:136-142` describes a different - static-pointer case (closure-pair env pointers in the `.data` - segment, which have no header at all and rely entirely on - codegen-side elision). It is NOT outdated by hs.2 — the - closure-pair case still relies on codegen-side elision, and the - sentinel guard is orthogonal additional protection for a - different kind of static pointer (Str literals). Do not edit - that comment. +Run: +``` +grep -n -E "i64 -1|i32 0, i32 1|<\{ i64, i64," crates/ail/tests/snapshots/hello.ll +``` -- No `git commit` step. The Boss commits the working-tree diff at - iter end after audit-level inspection. +Expected output (line numbers may vary slightly): +``` +5:@.str_hello_str_0 = private unnamed_addr constant <{ i64, i64, [15 x i8] }> <{ i64 -1, i64 14, [15 x i8] c"Hello, AILang.\00" }>, align 8 +18: %v1 = getelementptr inbounds i8, ptr getelementptr inbounds (<{ i64, i64, [15 x i8] }>, ptr @.str_hello_str_0, i32 0, i32 1), i64 8 +``` + +Confirms two lines need to flip. + +- [ ] **Step 2: Regenerate the snapshot under UPDATE_SNAPSHOTS=1** + +Run: +``` +UPDATE_SNAPSHOTS=1 cargo test -p ail --test e2e hello +``` + +(If the test harness name differs, locate it with: +`grep -rn "UPDATE_SNAPSHOTS" crates/ail/tests/` and run the matched +test.) + +Expected: the snapshot file is rewritten in place. The two flipped +lines now read: +``` +@.str_hello_str_0 = private unnamed_addr constant <{ i64, [15 x i8] }> <{ i64 14, [15 x i8] c"Hello, AILang.\00" }>, align 8 +... + %v1 = getelementptr inbounds i8, ptr getelementptr inbounds (<{ i64, [15 x i8] }>, ptr @.str_hello_str_0, i32 0, i32 0), i64 8 +``` + +(The `i64 -1` literal is gone; the struct type loses one `i64,`; the +GEP-index pair changes from `i32 0, i32 1` to `i32 0, i32 0`. The +`i64 8` byte-offset in the outer `getelementptr i8` is unchanged.) + +- [ ] **Step 3: Re-run the snapshot test without UPDATE_SNAPSHOTS=1 + and confirm it passes** + +Run: +``` +cargo test -p ail --test e2e hello +``` + +Expected: **PASS**. The regenerated snapshot now matches the live +emission. + +- [ ] **Step 4: Full workspace test sweep** + +Run: +``` +cargo test --workspace +``` + +Expected: every test **PASSes**. Special attention to any test that +greps for `i64 -1` or for the 3-field struct type in emitted IR +(unlikely outside the two tests Task 1 already covers; the +ailang-prose / ailang-check / ailang-surface crates do not look at +codegen output). If any test fails, decide whether to: (a) patch the +test in place using the same shape transformation if it pinned hs.1 +emission, or (b) treat it as a real regression and stop. + +- [ ] **Step 5: Cross-language stdout regression sweep** + +Run: +``` +bench/cross_lang.py +``` + +Expected: every entry **green** (byte-identical stdout vs. the C +reference corpus in `bench/reference/`). This is the strongest gate: +it verifies that the layout switch did not change observable program +output for any program in `examples/`. Any divergence is a real +regression — re-inspect the four `+8 GEP` callsites first, since +those are the consumer-side ABI and most likely to misread the new +layout. + +- [ ] **Step 6: Workspace-compile regression sweep** + +Run: +``` +bench/compile_check.py +``` + +Expected: every entry **green** (compile succeeds for the full +`examples/` corpus). This catches cases where a program compiled +under hs.1 but fails under hs.2 codegen for reasons orthogonal to +stdout output. + +- [ ] **Step 7: Latency baselines check** + +Run: +``` +bench/check.py +``` + +Expected: green within the known `latency.explicit_at_rc.*` +nondeterminism tolerance documented in recent audits. The hs.2 retrofit +is a struct-shape change, not a code-path-count change; no latency +delta is expected. + +--- + +## Acceptance for this iteration + +- Both updated IR-shape tests pass. +- All four +8 GEP tests pass unchanged. +- `crates/ail/tests/snapshots/hello.ll` reflects the new layout. +- `cargo test --workspace` green. +- `bench/cross_lang.py`, `bench/compile_check.py`, `bench/check.py` + all green. +- The hs.1 sentinel-rc-header story is gone from both the runtime + emission and the test asserts; static-Str globals are now 8 bytes + smaller per literal. +- No runtime-side change, no checker-side change, no new fixtures or + test files.