832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
481 lines
18 KiB
Markdown
481 lines
18 KiB
Markdown
# 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
|
|
`@<name> = 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.
|