# hs.2 — Static-Str sentinel-slot retrofit — Implementation Plan > **Parent spec:** `docs/specs/0019-heap-str-abi.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **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:** 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:** `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 - 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 — Update IR-shape tests to the amended-spec layout (RED) **Files:** - Modify: `crates/ailang-codegen/src/lib.rs:3808-3885` - [ ] **Step 1: Rename and rewrite assert in `static_str_global_uses_packed_struct_with_sentinel_and_len`** 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: 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 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}" ); } ``` 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. - [ ] **Step 2: Update assert in `static_str_callsite_pointer_is_payload_via_constexpr_gep`** 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: ```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}" ); } ``` 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`. - [ ] **Step 3: Run both updated tests and confirm they fail RED** Run: ``` cargo test -p ailang-codegen --lib static_str_ ``` 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. - [ ] **Step 4: Confirm the four `+8 GEP` tests still pass under hs.1 emission** Run: ``` cargo test -p ailang-codegen --lib _calls_ ``` 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 2 — Switch emission and callsite GEPs to the amended-spec layout (GREEN) **Files:** - 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: Switch the global-emission loop (single source of the `i64 -1` sentinel literal)** Locate the loop at `crates/ailang-codegen/src/lib.rs:466-475`. Replace the comment (lines 462-465) and the format string (line 472): ```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; } } ``` 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 2: Switch the first `Literal::Str` callsite GEP (in `emit_const_def`)** 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`). --- ## Task 3 — Regenerate hello.ll snapshot + workspace regression sweep **Files:** - Modify: `crates/ail/tests/snapshots/hello.ll` - [ ] **Step 1: Inspect the current snapshot** Run: ``` grep -n -E "i64 -1|i32 0, i32 1|<\{ i64, i64," crates/ail/tests/snapshots/hello.ll ``` 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.