diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 03eb52d..38a15c3 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2335,11 +2335,49 @@ matches via IEEE-`==`, and bit-exact equality is rarely what an LLM-author wants. Use ordering operators (`<`, `>`, ...) and `is_nan` to discriminate Floats. -`float_to_str` (Float → Str) is **type-installed but codegen- -deferred** to a follow-up milestone: it requires runtime-allocated -`Str` (the current Str path uses only static `@.str_*` globals; -no malloc-backed dynamic-Str infrastructure). Calling it -typechecks but produces a structured `CodegenError::Internal`. +`float_to_str` (Float → Str) and `int_to_str` (Int → Str) are +fully wired through checker, codegen, and runtime. Both allocate +a fresh heap-Str slab at the call site (see "Str ABI" below for +the dual realisation) and carry `ret_mode: Own` so the let-binder +for the call result is RC-tracked and the slab is freed at scope +close. + +**Str ABI.** A `Str` is a pointer to a structure with `i64 len` at +offset 0 followed by `len` bytes plus a trailing `NUL` at offset 8. +Two realisations share this consumer ABI: + +| Realisation | Origin | rc_header | Memory | +|-------------|-------------------------------------------------|-----------|-----------------------------------------| +| static-Str | string literals (`@.str_*` LLVM globals) | none | `.rodata`, packed-struct `<{ i64, [N+1 x i8] }>` | +| heap-Str | runtime allocations (`int_to_str`, `float_to_str`, ...) | yes, at `payload - 8` | `malloc`'d via `ailang_rc_alloc(8 + len + 1)` | + +Every consumer (`@puts`, `@strcmp`, `@ail_str_eq`, +`@ail_str_compare`) GEPs `+8` from the Str pointer to reach the +bytes, regardless of realisation. The byte-comparison semantics +are inherited from libc `strcmp` — locale-independent, NUL- +terminated. + +The heap-Str realisation participates in standard RC: the +`rc_header` slot eight bytes before the `len` field is managed +by `ailang_rc_alloc` / `ailang_rc_inc` / `ailang_rc_dec` exactly +like any other RC-allocated cell. The static-Str realisation has +no `rc_header` slot at all; the bytes at `payload - 8` belong to +the previous global in `.rodata` and reading them is undefined. + +**The static-Str non-RC invariant is enforced at codegen.** Two +mechanisms keep static-Str pointers out of `ailang_rc_dec` along +every shipping execution path: (1) the non-escape lowering pass +(iter 18b) and the move-tracking partial-drop logic (iter 18d.3) +prevent let-binders or pattern-binders for static-Str literals +from reaching scope-close drop emission; (2) the `Type::Con { name: "Str" }` +carve-outs in `field_drop_call` and in the `Term::App` arm of +`drop_symbol_for_binder` (both in `crates/ailang-codegen/src/drop.rs`) +route the rare case that *does* reach drop emission through +`ailang_rc_dec`, which itself only fires for heap-Str at runtime +(static-Str pointers never carry a live rc_header; if codegen ever +let one through, the runtime would corrupt `.rodata`-adjacent +memory). No runtime guard backs the invariant up; the codegen +proof is the protection. ## What is not (yet) supported @@ -2384,9 +2422,11 @@ What **is** supported (and used as the smoke test for the pipeline): `not : (Bool) -> Bool`; conversions `int_to_float : (Int) -> Float`, `float_to_int_truncate : (Float) -> Int` (saturating, NaN → 0), - `float_to_str : (Float) -> Str` (codegen lowering deferred — - symbol installed but errors at codegen pending runtime Str - allocation); inspection `is_nan : (Float) -> Bool` (LLVM + `float_to_str : (Float) -> Str`, + `int_to_str : (Int) -> Str` (both allocate a heap-Str slab at + call time and return it with `ret_mode: Own`; see "Str ABI" for + the dual heap-/static-Str realisation); inspection + `is_nan : (Float) -> Bool` (LLVM `fcmp uno`); Float bit-pattern constants `nan : Float`, `inf : Float`, `neg_inf : Float` (resolved as bare values, lower to direct hex-float `double` SSA constants at use site); the IO diff --git a/docs/journals/2026-05-12-audit-eob.md b/docs/journals/2026-05-12-audit-eob.md new file mode 100644 index 0000000..e8a6b53 --- /dev/null +++ b/docs/journals/2026-05-12-audit-eob.md @@ -0,0 +1,143 @@ +# audit-eob — Milestone close: heap-str-abi (closing iter eob.1) + +**Date:** 2026-05-12 +**Milestone:** heap-str-abi (hs.1 + hs.2 + hs.3 + hs.4 + eob.1) +**Status:** Closed, three DESIGN.md tidies applied inline (`eob.tidy`) + +## Architect drift review + +`ailang-architect` reported `drift_found` across the commit range +`750f97e..78e8338`. Lockstep invariants and codegen ABI all +intact; three high-severity items in DESIGN.md plus one medium +record-keeping note. + +**[high — spec acceptance]** `docs/DESIGN.md:2338-2342` — +`float_to_str` described as "type-installed but codegen-deferred" +with `CodegenError::Internal`. Stale since hs.4. + +**[high — spec acceptance]** `docs/DESIGN.md:2387` — same caveat +inside the "What is supported" inventory; `int_to_str` was absent +from the inventory entirely. + +**[high — spec acceptance]** `docs/DESIGN.md` (missing) — no +"Str ABI" anchor documenting the heap-Str / static-Str dual +realisation, shared consumer ABI, or codegen-elision invariant +for static-Str-in-RC-paths. + +**[medium — record-keeping]** `docs/journals/2026-05-12-iter-hs.4.md` +closed `DONE_WITH_CONCERNS` with weakened RC assertions; eob.1 +retroactively restored the strict invariants. No forward-pointer +appended; readers cross-walk via INDEX.md. + +### Tidy fixes applied inline (`eob.tidy`) + +All three high-severity DESIGN.md drift items addressed in this +audit commit (Boss-mechanical edits — no separate plan / implement +dispatch warranted; the spec amend was small, the Str-ABI-anchor +content was already fully known from the just-closed milestone): + +1. `docs/DESIGN.md:2338-2343` (float_to_str caveat block): replaced + the codegen-deferred prose with a one-paragraph statement that + `float_to_str` and `int_to_str` are fully wired, allocate a + heap-Str slab, and carry `ret_mode: Own`. Forward-references the + new Str-ABI anchor. + +2. `docs/DESIGN.md:2387-2392` (inventory bullet): dropped the + codegen-deferred parenthetical from `float_to_str`; added + `int_to_str : (Int) -> Str` to the inventory; pointer to the + Str-ABI section for the dual realisation. + +3. `docs/DESIGN.md` (new "Str ABI" subsection placed after the + Float-semantics block, before "What is not (yet) supported"): + documents the two realisations (static-Str packed-struct + `<{ i64, [N+1 x i8] }>` globals in `.rodata`; heap-Str slabs + allocated via `ailang_rc_alloc(8 + len + 1)`); the shared + consumer ABI (Str pointer at len-field offset 0, bytes at + `payload + 8`, inherited `strcmp` semantics); and the + codegen-level non-RC invariant for static-Str (move-tracking + partial-drop + non-escape lowering + `Type::Con { name: "Str" }` + carve-outs at `field_drop_call` and `drop_symbol_for_binder`'s + App arm). + +Medium-severity hs.4-journal forward-pointer: skipped. INDEX.md +already links hs.4 → eob.1 chronologically; the eob.1 journal +explicitly cross-references hs.4's deferred fix as the iter's +motivation. A forward-pointer in hs.4's own file would push the +append-only convention; the cross-walk from INDEX is sufficient. + +## Bench-regression check + +Sequence run as Boss (the `&&` chain in audit-SKILL.md tripped at +`check.py`'s exit 1; `compile_check.py` and `cross_lang.py` then +run separately to read their exit codes cleanly): + +- `check.py`: **exit 1** — 63 metrics; 2 regressed, 5 improved + beyond tolerance, 56 stable. + - Regressions: `bench_list_sum.bump_s` +12.60% (tol 10%); + `bench_hof_pipeline.bump_s` +11.65% (tol 10%). Both only + marginally over tolerance; the other four `bump_s` metrics + (`bench_tree_walk`, `bench_closure_chain`, + `bench_compute_collatz`, `bench_list_sum_explicit`) are stable + within ±7%. + - Improvements: three on the `latency.explicit_at_rc` cluster + (`p99_us` -37.37%, `p99_9_us` -40.14%, `p99_over_median` + -37.35%); two `gc_over_bump` mirrors on `bench_list_sum` and + `bench_hof_pipeline` (ratio falls because the denominator — + `bump_s` — rose; same physical signal as the bump regressions). +- `compile_check.py`: exit 0; 24 metrics, all stable within ±25%. +- `cross_lang.py`: exit 0; 25 metrics, all stable within ±15%. + +### Classification + +**Latency cluster** (`latency.explicit_at_rc.p99_us` / +`p99_9_us` / `p99_over_median`): third consecutive sighting +across `audit-cma` → `audit-ms` → `audit-eob`. `audit-ms` flagged +this as "one more audit before considering ratification"; this is +that one more audit. + +eob.1's only codegen-side touchpoints are the `drop.rs` Str +carve-out (8 LOC) and the `ret_mode: Own` flip at four signature +sites; none of those should plausibly shift latency p99 by 37%. +The signal predates this milestone (audit-cma was three milestones +ago). Conservative call: still **do not** baseline-update on this +audit. The cluster is real and persistent, but the underlying +cause is unknown, and a baseline update without an identified +attribution would obscure the next signal that *does* have an +attributable cause. + +Recommendation forward: if the cluster persists into the next +two audits unchanged, ratify with `--update-baseline` and a +journal entry naming the persistence as the ratify rationale +(absence of identified cause being the standing fact). If the +cluster shifts or shrinks meaningfully, that is itself the +attribution signal. + +**bump_s cluster** (`bench_list_sum.bump_s`, +`bench_hof_pipeline.bump_s`): **first sighting** in any recent +audit. Both metrics only marginally over the 10% tolerance. +The other four `bump_s` measurements are stable, which makes +"shared underlying bump-allocator change" unlikely; per-fixture +system noise (12.6% on a ~50ms benchmark is ~6ms of jitter) is +the most parsimonious explanation. + +eob.1 made zero changes to bump-allocator codepaths. The +implementer's end-report flagged this same cluster as appearing +during the workspace bench sweep before commit. Conservative call: +first-sighting rule — **do not** baseline-update; observe in the +next audit; if it persists, investigate. + +**Baseline left pristine on every script** for the third +consecutive audit. No `--update-baseline` invocation. + +## Status + +Closed. Heap-str-abi milestone fully landed: +- Static-Str layout migrated (hs.1, hs.2). +- Heap-Str runtime additions wired (hs.3, hs.4). +- Effect-op-borrow rule + RC-discipline closes (eob.1). +- DESIGN.md anchors both arg-position rules + Str-ABI dual + realisation (eob.1 + eob.tidy in this audit). +- WhatsNew entry + roadmap P1 close (eob.1). + +Working tree exits audit clean. main HEAD advances by exactly +this audit commit. diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 890875c..3d351a6 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -36,3 +36,4 @@ - 2026-05-12 — iter hs.3: heap-Str runtime additions — three new symbols appended to `runtime/str.c` (`static str_alloc(uint64_t)` slab helper, `ailang_int_to_str(int64_t)`, `ailang_float_to_str(double)`); supporting ``/``/`` includes + `extern void *ailang_rc_alloc(size_t)` declaration; the extern was promoted from plain-external to `__attribute__((weak))` after the first regression sweep surfaced 11 e2e link failures under `--alloc=gc`/`bump` (public `T` symbols pull in their transitive callees regardless of upstream usage; rc.c is not linked under gc/bump in hs.3 — repair makes str.c link-safe in isolation, no-op once hs.4 lands the unconditional rc.c link); cargo test --workspace + cross_lang.py + compile_check.py + check.py all green on re-sweep; no IR-side caller wired yet (hs.4) → 2026-05-12-iter-hs.3.md - 2026-05-12 — iter hs.4: IR + checker + linker wiring — `int_to_str : (Int) -> Str` installed in checker + synth.rs lockstep partner; IR-header preamble unconditionally declares both `@ailang_int_to_str` / `@ailang_float_to_str`; `Emitter::lower_app` gets new `int_to_str` arm and replaces `float_to_str`'s `CodegenError::Internal` with the actual call emission; `is_static_callee` whitelist extends to `int_to_str`; `runtime/rc.c` hoisted out of `AllocStrategy::Rc` arm to the unconditional link path (the `__attribute__((weak))` on str.c's `ailang_rc_alloc` extern becomes the documented no-op); 2 new IR-shape pins + 4 new E2E (2 stdout-smoke + 2 RC-stats) + 4 `.ail.json` fixtures; `drop.rs` Str-arm comment refreshed to dual-realisation framing; 5 IR snapshots regen for the 2 new declare lines. Status: DONE_WITH_CONCERNS — heap-Str RC-discipline incomplete (slabs leak at program end) because plan's literal `ret_mode: Implicit` interacts with uniqueness analyser walking `Term::Do` args as `Position::Consume`. Test asserts weakened from `allocs == frees && live == 0` to `allocs >= 1`. Substantive fix requires spec-level effect-op arg-mode decision (deferred, bounce-back to user) → 2026-05-12-iter-hs.4.md - 2026-05-12 — iter eob.1: Effect-op args walked as Borrow (uniqueness.rs + linearity.rs + linearity.rs doc-comment); heap-Str RC discipline closes (int_to_str / float_to_str `ret_mode: Implicit` → `Own` at 4 lockstep sites across builtins.rs + synth.rs; `drop_symbol_for_binder` App-arm gets `Str` carve-out symmetric to `field_drop_call`'s existing one); 2 pre-existing RED tests at e2e.rs (commit 592d87b) flipped to GREEN unchanged with predicted concrete numbers (`allocs==1,frees==1,live==0` and `allocs==2,frees==2,live==0`); 2 new positive tests + fixtures (primitive-Int through `io/print_int`: zero RC traffic; heap-Str repeated borrow through 2× `io/print_str`: `allocs==1,frees==1,live==0`); DESIGN.md §Decision 10 anchors both arg-position rules together (Ctor=Consume, Do=Borrow, plus a paragraph linking Do=Borrow to ret_mode==Own letbinder-trackability); WhatsNew entry shipped, roadmap P1 `[milestone] Heap-Str ABI` checked off, Post-22-Prelude `depends on:` line removed; 7/7 tasks first-shot, zero review re-loops; lint side-effect surface (over-strict-mode on (own T) params used solely via effect-ops) was empty for the current corpus → 2026-05-12-iter-eob.1.md +- 2026-05-12 — audit-eob: milestone close (heap-str-abi) — architect drift fixed inline as `eob.tidy` (3 DESIGN.md edits: float_to_str / int_to_str caveats dropped, "Str ABI" anchor added documenting both heap-Str and static-Str realisations with their shared consumer ABI and codegen-level non-RC invariant for static-Str); bench mixed (latency.explicit_at_rc improvement cluster reappears for the 3rd consecutive audit + new marginal bump_s regressions on list_sum/hof_pipeline 12% over 10% tol), baseline pristine for 3rd consecutive audit (conservative call: latency cluster has no identified cause across 3 audits — ratify-without-attribution would obscure future signal; bump_s cluster is first-sighting, observe next audit) → 2026-05-12-audit-eob.md