First iter of the heap-Str ABI milestone. Pure codegen refactor; every existing program produces byte-identical stdout.
Static-Str LLVM globals migrate from `[N x i8] c"…\00"` to `<{ i64, i64, [N x i8] }> <{ i64 -1, i64 N-1, [N x i8] c"…\00" }>`, carrying the UINT64_MAX sentinel rc-header (slot exploited in hs.2 via runtime short-circuit) and an explicit byte length. Two parallel intern paths (`intern_string` for raw format scaffolding, new `intern_str_literal` for language Str values) keep format strings untouched in `[N x i8]` shape.
IR-Str pointers now uniformly land on the len-field (offset +8 from rc-header, -8 from bytes). Four codegen sites that hand a Str pointer to a NUL-terminated-bytes C-API consumer now emit a +8 GEP first: `@puts` (io/print_str), `@ail_str_eq` (eq__Str intercept), `@ail_str_compare` (compare__Str intercept), and `@strcmp` (lower_eq Str arm — the 4th site was not in the plan; surfaced by Task-5 e2e regression and fixed inline).
6 new IR-shape pinning tests; `hello.ll` snapshot regenerated; cross_lang.py + compile_check.py + full `cargo test --workspace` green.
5.7 KiB
iter hs.1 — Heap-Str ABI: Static-Str layout migration
Date: 2026-05-12
Started from: 69bb5f9952
Status: DONE
Tasks completed: 5 of 5
Summary
First iter of the heap-Str ABI milestone. Pure codegen-side refactor:
language Str literals now emit as packed-struct LLVM globals
<{ i64, i64, [N+1 x i8] }> <{ i64 -1, i64 N, [N+1 x i8] c"...\00" }>
carrying a UINT64_MAX sentinel rc-header in the first slot, an
explicit byte length in the second, and the bytes plus trailing NUL
in the array tail. The IR-Str-pointer convention is now uniform: a
language Str value flowing through the IR is a ptr landing on the
len-field (offset +8 from the header, -8 from the bytes). Every
codegen site that hands a Str pointer to a NUL-terminated-bytes
C-API consumer emits a getelementptr inbounds i8, ptr <v>, i64 8
immediately before the call. The sentinel slot is hardcoded in the
global initialiser and not yet observed by any caller — hs.2 will
add the UINT64_MAX short-circuit to ailang_rc_inc /
ailang_rc_dec in runtime/rc.c to exploit it. Format strings
(%lld\n, %g\n, true\n, false\n) are unchanged — they are
runtime-internal scaffolding, never flow as language Str values,
and remain in the raw [N x i8] shape via the original
intern_string / all_strings path. Acceptance verified
end-to-end: full cargo test --workspace green (no regressions
across 74 e2e fixtures), bench/compile_check.py exit 0,
bench/cross_lang.py exit 0 (byte-identical stdout across all
ail/c benchmark pairs).
Per-task notes
- iter hs.1.1: introduced parallel
str_literalsinterning table onEmitter,intern_str_literalfn, workspace-levelall_str_literalsaggregation, and a parallel global-emission loop emitting the packed-struct shape. Switched bothLiteral::Strarms (emit_const_def,lower_term) to callintern_str_literaland materialise a constexprgetelementptr inbounds (<{ i64, i64, [N x i8] }>, ptr @g, i32 0, i32 1)landing on thelen-field. Two pinning tests added. - iter hs.1.2:
io/print_strarm now emits a+8 GEPto land on the bytes pointer before calling@puts. One pinning test added. - iter hs.1.3:
eq__Strarm oftry_emit_primitive_instance_bodynow emits+8 GEPs on both operands before calling@ail_str_eq. One pinning test added. - iter hs.1.4:
compare__Strarm oftry_emit_primitive_instance_bodynow emits+8 GEPs on both operands before calling@ail_str_compare. One pinning test added. - iter hs.1.5: regenerated
crates/ail/tests/snapshots/hello.llviaUPDATE_SNAPSHOTS=1. Fullcargo test --workspacerevealed a regression ineq_demotraced to a fourth codegen site not enumerated in the plan:lower_eq's"Str"arm calls@strcmpinline (separate from theeq__Strinstance-method intercept) to lower==onStrliterals and Str-pattern desugaring inclassify_str. Fixed inline with the same+8 GEPshape; one additional pinning test added.
Concerns
- The plan claimed three codegen sites pass a
Strpointer to aNUL-terminated-bytes C-API consumer (@puts,@ail_str_eq,@ail_str_compare); in fact a fourth site exists inlower_eq's"Str"arm, calling@strcmpdirectly. This is the dispatch path for the surface-level==operator onStrand for(pat-lit "...")desugar against aStrscrutinee. The fourth site is required to meet the iter's stated acceptance criterion (byte-identical stdout); fixing it was discovered via the e2e regression ineq_demorather than via the per-task IR-shape checks. The fix has the same shape as Tasks 2-4 and the corresponding pinning test (lower_eq_str_calls_strcmp_with_bytes_pointer) is added. The plan's "Files this plan creates or modifies" enumeration should listcrates/ailang-codegen/src/lib.rs:2520-2529as a fifth modify site for any future re-execution. - The plan's two test fixtures in Task 1 contained schema-syntax
errors that prevented compilation:
ConstDef.body(actual:value),Term::App { f, args }(actual:callee, args, tail), andTerm::App { Term::Var "io/print_str", ... }for what is actually aTerm::Doeffect op. Tests were adapted to compile while preserving the substantive assertions verbatim. Same applies to the test in Task 2 (Term::App→Term::Dofor theio/print_strinvocation and addedeffects: ["IO"]on the main fn so the typecheck-side dispatch finds the effect). These were small per-test fixture adaptations, not changes to the asserted IR shape.
Known debt
- The sentinel value
-1(UINT64_MAX) is hardcoded in the global initialiser.hs.2adds the matching short-circuit check inailang_rc_inc/ailang_rc_decso static-Str pointers can flow through generic RC paths without ref-count traffic. Untilhs.2lands, no caller actually observes the sentinel — staticStrslabs are still invisible to the RC runtime by construction (the call sites forinc/decagainstStrvalues are still gated by the same uniqueness-inference pass that elided them pre-iter).
Files touched
crates/ailang-codegen/src/lib.rs— Emitter field, intern fn, workspace aggregation, global emission, twoLiteral::Strarm switches, four C-API-callsite+8 GEPadjustments (io/print_str,eq__Str,compare__Str,lower_eqStr arm), six new IR-shape pinning tests in the test module.crates/ail/tests/snapshots/hello.ll— regenerated snapshot showing the new packed-struct global and the+8 GEPbefore@puts. No other snapshot undercrates/ail/tests/snapshots/changed (other snapshots contain only format-string globals which kept the raw[N x i8]shape).
Stats
bench/orchestrator-stats/2026-05-12-iter-hs.1.json