From f9bf97be8075c947201d6aa32700a71901013f93 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 12 May 2026 17:51:27 +0200 Subject: [PATCH] =?UTF-8?q?iter=20hs.2:=20static-Str=20layout=20retrofit?= =?UTF-8?q?=20=E2=80=94=20drop=20sentinel=20rc-header=20slot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the amended heap-Str ABI spec (2a72a4a), static-Str globals lose the leading UINT64_MAX sentinel rc-header field that hs.1 introduced. Layout flips from <{ i64, i64, [N x i8] }> <{ i64 -1, i64 N, ... }> to <{ i64, [N x i8] }> <{ i64 N, ... }>; the IR-Str pointer now lands on the len-field at struct-index 0 (was 1) via constexpr-GEP. The four +8 consumer-side GEPs (@puts / @ail_str_eq / @ail_str_compare / @strcmp) do not change — the bytes still sit 8 bytes past the len-field. Static- Str pointers are kept out of ailang_rc_dec at the codegen level via move-tracking (iter 18d.3) and non-escape lowering (iter 18b), so no header slot is needed at the global; this is a codegen-level invariant, not a runtime guard. Change surface: three format strings + two doc-comments in lib.rs, rename + assertion update of two IR-shape tests, snapshot regen of hello.ll. No runtime changes, no checker changes, no new fixtures. Static-Str globals are now 8 bytes smaller per literal. cargo test --workspace green; cross_lang.py / compile_check.py / check.py all green. 3 tasks, 0 re-loops. --- .../2026-05-12-iter-hs.2.json | 12 +++ crates/ail/tests/snapshots/hello.ll | 4 +- crates/ailang-codegen/src/lib.rs | 76 +++++++++-------- docs/journals/2026-05-12-iter-hs.2.md | 81 +++++++++++++++++++ docs/journals/INDEX.md | 2 + 5 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-12-iter-hs.2.json create mode 100644 docs/journals/2026-05-12-iter-hs.2.md diff --git a/bench/orchestrator-stats/2026-05-12-iter-hs.2.json b/bench/orchestrator-stats/2026-05-12-iter-hs.2.json new file mode 100644 index 0000000..fb4ec69 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-12-iter-hs.2.json @@ -0,0 +1,12 @@ +{ + "iter_id": "hs.2", + "date": "2026-05-12", + "mode": "standard", + "outcome": "DONE", + "tasks_total": 3, + "tasks_completed": 3, + "reloops_per_task": { "1": 0, "2": 0, "3": 0 }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": null +} diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index db9f84e..9bc7a25 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -2,7 +2,7 @@ source_filename = "hello.ail" target triple = "" -@.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 +@.str_hello_str_0 = private unnamed_addr constant <{ i64, [15 x i8] }> <{ i64 14, [15 x i8] c"Hello, AILang.\00" }>, align 8 declare i32 @printf(ptr, ...) declare i32 @puts(ptr) @@ -15,7 +15,7 @@ declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_hello_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_hello_main_adapter, ptr null } define i8 @ail_hello_main() { entry: - %v1 = getelementptr inbounds i8, ptr getelementptr inbounds (<{ i64, i64, [15 x i8] }>, ptr @.str_hello_str_0, i32 0, i32 1), i64 8 + %v1 = getelementptr inbounds i8, ptr getelementptr inbounds (<{ i64, [15 x i8] }>, ptr @.str_hello_str_0, i32 0, i32 0), i64 8 call i32 @puts(ptr %v1) ret i8 0 } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 95d19e5..26688cf 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -459,17 +459,22 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result emitted_global = true; } } - // Iter hs.1: packed-struct globals for language `Str` literals. - // The `i64 -1` slot is the `UINT64_MAX` sentinel rc-header; the - // next `i64` is the byte length (excluding the trailing NUL); the + // 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, i64, [{total} x i8] }}> <{{ i64 -1, i64 {bytes_len}, [{total} x i8] c\"{escaped}\" }}>, align 8\n", + "@{name} = private unnamed_addr constant <{{ i64, [{total} x i8] }}> <{{ i64 {bytes_len}, [{total} x i8] c\"{escaped}\" }}>, align 8\n", )); emitted_global = true; } @@ -558,9 +563,9 @@ struct Emitter<'a> { body: String, /// String constants: content -> (global name (without `@`), llvm type length incl. \0) strings: BTreeMap, - /// Iter hs.1: language `Str` literals interned as packed-struct - /// globals (`<{ i64, i64, [N+1 x i8] }>`) carrying a `UINT64_MAX` - /// sentinel rc-header and an explicit `len` field. Parallel to + /// 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 /// like `%lld\n` / `true\n` in the raw `[N x i8]` shape). Same /// key shape; the two tables coexist. @@ -922,16 +927,18 @@ impl<'a> Emitter<'a> { ), Literal::Unit => ("i8".to_string(), "0".to_string()), Literal::Str { value } => { - // Iter hs.1: emit a packed-struct global and return a - // constexpr-GEP pointer landing on the `len`-field, so - // every IR-Str pointer in this codegen pipeline has - // the same shape (header at -8, len at 0, bytes at +8). + // 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, i64, [{total} x i8] }}>, ptr @{g}, i32 0, i32 1)", + "getelementptr inbounds (<{{ i64, [{total} x i8] }}>, ptr @{g}, i32 0, i32 0)", ), ) } @@ -1244,15 +1251,17 @@ impl<'a> Emitter<'a> { "i1".into(), ), Literal::Str { value } => { - // Iter hs.1: language `Str` literals materialise as + // Iter hs.2: language `Str` literals materialise as // a constexpr-GEP into the packed-struct global, - // landing on the `len`-field so the IR-Str - // pointer carries header at -8 and bytes at +8. + // 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, i64, [{total} x i8] }}>, ptr @{g}, i32 0, i32 1)", + "getelementptr inbounds (<{{ i64, [{total} x i8] }}>, ptr @{g}, i32 0, i32 0)", ), "ptr".into(), ) @@ -2593,10 +2602,10 @@ impl<'a> Emitter<'a> { name } - /// Iter hs.1: parallel to `intern_string`, but for language `Str` - /// literals emitted as packed-struct globals (sentinel + len + - /// bytes + NUL). Shares the same monotonic `str_counter` so the - /// produced global names remain alphabetically orderable + /// 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. fn intern_str_literal(&mut self, hint: &str, content: &str) -> String { if let Some((name, _)) = self.str_literals.get(content) { @@ -3807,13 +3816,15 @@ mod tests { } } - /// Iter hs.1: language `Str` literals emit as packed-struct - /// globals carrying a `UINT64_MAX` sentinel rc-header, an - /// explicit `len` field, and the bytes + trailing NUL. This - /// test pins the layout shape against a tiny single-Literal::Str - /// fixture. + /// 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_sentinel_and_len() { + fn static_str_global_uses_packed_struct_with_len() { let m = Module { schema: SCHEMA.into(), name: "t".into(), @@ -3843,14 +3854,15 @@ mod tests { }; let ir = emit_ir(&m).unwrap(); assert!( - ir.contains(r#"<{ i64, i64, [6 x i8] }> <{ i64 -1, i64 5, [6 x i8] c"hello\00" }>"#), - "expected packed-struct global with sentinel + len + bytes + NUL; ir was:\n{ir}" + 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}" ); } - /// Iter hs.1: a `Literal::Str` at a callsite materialises a - /// constexpr `getelementptr` landing on the `len`-field of the - /// packed-struct global, not the bare global name. This pins the + /// 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() { @@ -3879,7 +3891,7 @@ mod tests { }; let ir = emit_ir(&m).unwrap(); assert!( - ir.contains(r#"getelementptr inbounds (<{ i64, i64, [3 x i8] }>, ptr @.str_t_str_0, i32 0, i32 1)"#), + 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}" ); } diff --git a/docs/journals/2026-05-12-iter-hs.2.md b/docs/journals/2026-05-12-iter-hs.2.md new file mode 100644 index 0000000..6685961 --- /dev/null +++ b/docs/journals/2026-05-12-iter-hs.2.md @@ -0,0 +1,81 @@ +# iter hs.2 — Static-Str sentinel-slot retrofit + +**Date:** 2026-05-12 +**Started from:** 51a096cf8354362783d8caf2ec4b35179d935a7a +**Status:** DONE +**Tasks completed:** 3 of 3 + +## Summary + +Second iter of the heap-Str ABI milestone. Surgical codegen-side +retrofit of hs.1's static-`Str` global layout: per the amended parent +spec (`docs/specs/2026-05-12-heap-str-abi.md`) the sentinel rc-header +slot drops. Language `Str` literals now emit as +`<{ i64, [N+1 x i8] }> <{ i64 N, [N+1 x i8] c"…\00" }>` instead of +the hs.1 shape `<{ i64, i64, [N+1 x i8] }> <{ i64 -1, i64 N, …}>`. The +explicit `len` field becomes the first (and only non-byte) field on +which the IR-`Str` pointer lands via constexpr-GEP `i32 0, i32 0`. The +`+8 GEP` consumer-side calls (`@puts`, `@ail_str_eq`, `@ail_str_compare`, +inline `@strcmp`) do **not** change — the bytes still sit 8 bytes past +the `len`-field. Static-Str pointers are kept out of `ailang_rc_dec` +at the codegen level via move-tracking (iter 18d.3) and non-escape +lowering (iter 18b), so no header slot is needed at the global. + +Change surface: three format-string sites and two doc-comments in +`crates/ailang-codegen/src/lib.rs`, rename + assertion update of two +IR-shape tests in the same crate's test module, plus snapshot +regeneration of `crates/ail/tests/snapshots/hello.ll`. No runtime +changes, no checker changes, no new fixtures. Static-Str globals are +now 8 bytes smaller per literal. + +Acceptance verified end-to-end: `cargo test --workspace` green (every +test result `ok`, 0 failures across the workspace), `bench/cross_lang.py` +exit 0 (byte-identical stdout across all `ail/c` benchmark pairs), +`bench/compile_check.py` exit 0, `bench/check.py` exit 0 within the +known `latency.explicit_at_rc.*` nondeterminism tolerance. + +## Per-task notes + +- iter hs.2.1: renamed `static_str_global_uses_packed_struct_with_sentinel_and_len` + → `static_str_global_uses_packed_struct_with_len`; updated its + doc-comment and assertion substring (struct loses leading `i64, `, + initialiser loses leading `i64 -1, `). Updated assertion in + `static_str_callsite_pointer_is_payload_via_constexpr_gep` (name + preserved — "payload" still describes the `len`-field-as-pointer-target; + struct loses leading `i64, `, GEP indices `i32 0, i32 1` → `i32 0, i32 0`). + RED confirmed: both tests fail against hs.1 emission with the expected + assertion-substring mismatch. The four `+8 GEP` tests stayed green. +- iter hs.2.2: flipped the global-emission loop format string at + `lib.rs:471` to the 2-field shape; flipped both `Literal::Str` arms + (`emit_const_def` ~ `lib.rs:933`, `lower_term` ~ `lib.rs:1255`) to + emit constexpr-GEP `i32 0, i32 0` onto the 2-field struct type; + updated the two doc-comments on the `str_literals` field + (`lib.rs:561`) and on `intern_str_literal` (`lib.rs:2596`) to the + amended-spec wording. All five edits exactly per plan text. Static_str_ + tests GREEN, `_calls_` tests GREEN, full `cargo test -p ailang-codegen` + GREEN (33 tests). +- iter hs.2.3: regenerated `crates/ail/tests/snapshots/hello.ll` via + `UPDATE_SNAPSHOTS=1 cargo test -p ail --test ir_snapshot hello`. The + two affected lines (the `@.str_hello_str_0` global and its constexpr- + GEP at `%v1`) flipped to the new shape; the outer `i64 8` byte offset + preserved. Workspace `cargo test --workspace` green; `cross_lang.py`, + `compile_check.py`, `check.py` all green. + +## Concerns + +(none) + +## Known debt + +(none) + +## Files touched + +- `crates/ailang-codegen/src/lib.rs` — three format strings, two + doc-comments, two test names + assertions + doc-comments +- `crates/ail/tests/snapshots/hello.ll` — regenerated snapshot + (2 lines content change, byte offsets preserved) + +## Stats + +bench/orchestrator-stats/2026-05-12-iter-hs.2.json diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 1248ccb..1d0c514 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -31,3 +31,5 @@ - 2026-05-12 — iter ext-rename: `.ailx` → `.ail` extension rename across the live toolchain (61 example renames + 35 content edits + experiment-crate cohort rename `ailx → ail`); historical docs (journals, archive, specs, plans, frozen experiment runs) deliberately untouched; cargo+bench all-green; opens follow-up `.ail`-CLI-acceptance iter → 2026-05-12-iter-ext-rename.md - 2026-05-12 — iter ext-cli.1: `ail check`/build/run/...all 12 path-taking subcommands now accept `.ail` (Form A) inputs via `ailang_surface::{load_module, load_workspace}` (extension-dispatching loaders + injection point `core::load_workspace_with`); workspace imports prefer `.ail` sibling over `.ail.json`; new `WorkspaceLoadError::SurfaceParse` variant routes surface parse errors as `surface-parse-error` structured diagnostics through `ail check --json`; DESIGN.md §Decision 6 CLI addendum; +7 new tests; closes the loop opened by iter ext-rename → 2026-05-12-iter-ext-cli.1.md - 2026-05-12 — iter hs.1: heap-Str ABI iter 1 — static-Str LLVM globals migrate from `[N x i8]` to packed-struct `<{ i64, i64, [N x i8] }>` carrying a `UINT64_MAX` sentinel rc-header + explicit `len`; IR-Str pointers now land on the `len`-field; `@puts` / `@ail_str_eq` / `@ail_str_compare` / `@strcmp` callsites GEP +8 to recover the bytes pointer (4 sites; plan listed 3, Task-5 e2e regression surfaced the 4th); 6 IR-shape pins; format strings stay raw `[N x i8]`; cross_lang.py + compile_check.py + full cargo test --workspace all green; byte-identical stdout across every example → 2026-05-12-iter-hs.1.md +- 2026-05-12 — spec amendment: heap-str-abi — sentinel-rc-header story dropped after hs.2 BLOCKED finding (codegen-level elision via move-tracking + non-escape lowering keeps static-Str pointers out of `ailang_rc_dec` along every shipping execution path; no runtime guard needed); re-dispatched grounding-check PASS → see commit `2a72a4a` (no per-iter journal — spec edit) +- 2026-05-12 — iter hs.2: static-Str layout retrofit — drop the `UINT64_MAX` sentinel slot from packed-struct globals (`<{ i64, i64, [N x i8] }>` → `<{ i64, [N x i8] }>`); IR-`Str` pointer now lands on the `len`-field at struct-index 0 (was 1) via constexpr-GEP `i32 0, i32 0`; `+8` consumer-side GEPs at the four C-API callsites invariant across the switch; renamed `..._sentinel_and_len` test to `..._with_len`; updated 5 sites in `crates/ailang-codegen/src/lib.rs` (3 format strings + 2 doc-comments); regenerated `hello.ll` snapshot; full `cargo test --workspace`, cross_lang.py, compile_check.py, check.py all green; static-Str globals are now 8 bytes smaller per literal → 2026-05-12-iter-hs.2.md