From 1cf281b2178570dfed5472e90d69eb80466fc642 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 12 May 2026 18:03:46 +0200 Subject: [PATCH] =?UTF-8?q?iter=20hs.3:=20heap-Str=20runtime=20additions?= =?UTF-8?q?=20=E2=80=94=20str=5Falloc=20+=20int=5Fto=5Fstr=20+=20float=5Ft?= =?UTF-8?q?o=5Fstr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append three new symbols to runtime/str.c: a private str_alloc(uint64_t) slab helper that allocates [rc_header | len | bytes... | NUL] via ailang_rc_alloc and writes the len prefix, plus two extern formatters ailang_int_to_str(int64_t) and ailang_float_to_str(double) that compose str_alloc with snprintf("%lld") / snprintf("%g"). Defensive abort() on truncation; 64-byte stack buffer comfortably oversized for either formatter. The extern declaration of ailang_rc_alloc carries __attribute__((weak)). First regression sweep without it surfaced 11 e2e link failures under --alloc=gc and --alloc=bump: public T-visible symbols (int_to_str / float_to_str) pull their transitive callees (rc_alloc) into every binary's symbol set, but rc.c is not linked under gc/bump in hs.3. The weak attribute makes the cross-allocator link safe in isolation; under rc the strong definition wins as usual. Retained after hs.4 (when rc.c becomes unconditionally linked) as a no-op that keeps str.c link-safe in isolation. No IR-side caller wired yet — hs.4 lands the IR-header declare lines, the int_to_str/float_to_str codegen lowering, the checker install, and the unconditional rc.c link. cargo test --workspace + cross_lang.py + compile_check.py + check.py all green on re-sweep. --- .../2026-05-12-iter-hs.3.json | 12 ++ docs/journals/2026-05-12-iter-hs.3.md | 111 ++++++++++++++++++ docs/journals/INDEX.md | 1 + runtime/str.c | 103 ++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 bench/orchestrator-stats/2026-05-12-iter-hs.3.json create mode 100644 docs/journals/2026-05-12-iter-hs.3.md diff --git a/bench/orchestrator-stats/2026-05-12-iter-hs.3.json b/bench/orchestrator-stats/2026-05-12-iter-hs.3.json new file mode 100644 index 0000000..32f909d --- /dev/null +++ b/bench/orchestrator-stats/2026-05-12-iter-hs.3.json @@ -0,0 +1,12 @@ +{ + "iter_id": "hs.3", + "date": "2026-05-12", + "mode": "standard", + "outcome": "DONE", + "tasks_total": 2, + "tasks_completed": 2, + "reloops_per_task": { "1": 0, "2": 1 }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": null +} diff --git a/docs/journals/2026-05-12-iter-hs.3.md b/docs/journals/2026-05-12-iter-hs.3.md new file mode 100644 index 0000000..26904d4 --- /dev/null +++ b/docs/journals/2026-05-12-iter-hs.3.md @@ -0,0 +1,111 @@ +# iter hs.3 — Heap-Str runtime additions + +**Date:** 2026-05-12 +**Started from:** 187d04814d4485bee1bb85f6f7433c2f3d146fb7 +**Status:** DONE +**Tasks completed:** 2 of 2 + +## Summary + +Third iter of the heap-Str ABI milestone. Single-file additive change +to `runtime/str.c`: three new symbols (private `static char +*str_alloc(uint64_t)` slab helper plus two extern formatters +`ailang_int_to_str(int64_t)` and `ailang_float_to_str(double)`), the +supporting `` / `` / `` includes, and an +`extern void *ailang_rc_alloc(size_t)` declaration. No IR-side caller +is wired yet — hs.4 will land codegen + checker + linker work that +makes the formatters live builtins. + +One deviation from the plan: the `extern` declaration of +`ailang_rc_alloc` was promoted from plain-external to +`__attribute__((weak))`. The plan's assertion that the new symbols +sit "present-but-unreferenced" across all `--alloc` strategies turned +out to be wrong at the link layer — `ailang_int_to_str` / +`ailang_float_to_str` are publicly-visible `T` symbols, so the linker +preserves them and their transitive callees (including +`ailang_rc_alloc`) in every binary regardless of program-level usage. +Under `--alloc=gc` and `--alloc=bump` the build pipeline does not +link `rc.c`, so the unresolved-symbol error fires at link time and +took out 11 e2e tests on the first regression sweep. The weak +attribute makes the reference link-safe in isolation: under gc/bump +it resolves to NULL (never dereferenced — no IR caller exists yet); +under rc the strong definition wins as usual. The attribute is +retained after hs.4 unconditionally links rc.c (where it becomes a +no-op) to keep `runtime/str.c` link-safe in isolation, matching the +spec's "single-file abstraction" framing of the runtime split. + +Acceptance verified: `cargo test --workspace` (zero failures across +the full test surface), `bench/cross_lang.py` (25/25 metrics stable, +exit 0), `bench/compile_check.py` (24/24 metrics stable, exit 0), +`bench/check.py` (zero regressed, exit 0; same five +`latency.explicit_at_rc.*` improvements outside tolerance that +audit-cma and audit-ms have already documented as +not-coupled-to-milestone — baseline left untouched per established +practice). `clang -c -O2 -Wall -Werror` on `runtime/str.c` standalone +also exits 0 with no warnings. + +## Per-task notes + +- iter hs.3.1: appended three `#include` lines (`stdint.h`, + `stdio.h`, `stdlib.h`), one weak `extern void *ailang_rc_alloc` + declaration, and three new function bodies (`str_alloc`, + `ailang_int_to_str`, `ailang_float_to_str`) to `runtime/str.c`. + Compile-check via `clang -c -O2 -Wall -Werror` green; nm + symbol-table inspection: 4 public `T` symbols (`ail_str_eq`, + `ail_str_compare`, `ailang_int_to_str`, `ailang_float_to_str`), + `str_alloc` absent at `-O2` (inlined into both callers — only + visible as `t str_alloc` at `-O0`; static-linkage invariant + preserved), `ailang_rc_alloc` shown as `w` (weak undefined). +- iter hs.3.2: ran the four regression gates. First-pass sweep + surfaced the 11-test linker failure under `--alloc=gc`/`bump` + caused by the plan's "rc_alloc reference is harmless if + unreferenced" assumption; weak-attribute repair re-ran clean. All + four gates green on the re-sweep. + +## Concerns + +- The plan's Task 1 Step 7 nm-output expected-line block ("`U + ailang_rc_alloc`" and "`0000000000000000 t str_alloc`") is + inaccurate at the optimisation level the plan itself specifies + (`-O2`): the grep regex does not match `ailang_rc_alloc`, and + `clang -O2` inlines the 2-line `str_alloc` helper into both + callers so no `t str_alloc` symbol survives the compilation unit. + Both deviations are `-O2` consequences, not bugs — the + semantically meaningful invariants (`str_alloc` is static and + absent from `T`; `ailang_rc_alloc` is a non-`T` undefined + reference) both hold. A future plan author writing nm-output + checks should inspect at `-O0` for symbol presence and at `-O2` + for what actually ships, or relax the expected text from + literal-match to invariant-check (e.g. "no `T str_alloc`", + "`ailang_rc_alloc` is `U` or `w`"). +- The plan's core "no build-pipeline change in hs.3" boundary held, + but the supporting claim "symbols are present in every binary + regardless of allocator choice" was false at the link layer. The + weak-attribute repair makes it true at the cost of one extra + attribute and a long-form doc-comment explaining the cross- + allocator linking story. A future plan that defers a build- + pipeline change should explicitly call out cross-translation-unit + symbol dependencies as a gating concern. + +## Known debt + +- hs.4 will wire `ailang_int_to_str` / `ailang_float_to_str` from + the IR side and unconditionally link `rc.c`; at that point the + `__attribute__((weak))` on the `ailang_rc_alloc` declaration + becomes a no-op (strong definitions always satisfy weak + references). The attribute is intentionally retained — see the + doc-comment in `runtime/str.c` — to keep the translation unit + link-safe in isolation, matching the parent spec's framing of + `runtime/str.c` as the heap-Str ABI's single point of contact. + +## Files touched + +- `runtime/str.c` — three new functions appended after + `ail_str_compare`, plus three new `#include` lines and one weak + `extern` declaration after the existing include block. Existing + `ail_str_eq` / `ail_str_compare` bodies and the file's top + comment block untouched. + +## Stats + +bench/orchestrator-stats/2026-05-12-iter-hs.3.json diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 1d0c514..ec1c086 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -33,3 +33,4 @@ - 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 +- 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 diff --git a/runtime/str.c b/runtime/str.c index 113899b..6687db1 100644 --- a/runtime/str.c +++ b/runtime/str.c @@ -15,6 +15,30 @@ #include #include +#include +#include +#include + +/* Defined in runtime/rc.c — exposed with default external linkage. + * Returns a zero-initialised payload whose `rc_header` sits at + * `payload - 8` and is initialised to refcount 1. str.c's heap-Str + * helpers call this regardless of the program's --alloc strategy, + * because heap-Str values are always refcounted (the global + * `--alloc` flag governs ADT allocation, not Str allocation). + * + * Declared `__attribute__((weak))` in hs.3 so the publicly-visible + * `ailang_int_to_str` / `ailang_float_to_str` symbols can link into + * binaries that do not pull in `runtime/rc.c` (i.e. `--alloc=gc` and + * `--alloc=bump` targets in hs.3, where `rc.c` is not linked). Under + * such targets the reference resolves to NULL; the heap-Str helpers + * are not yet called from any IR site (hs.4 wires the lowering), so + * the NULL is never dereferenced at runtime. Under `--alloc=rc` the + * strong definition in `rc.c` wins as usual. Once hs.4 lands the + * unconditional `rc.c` link the `weak` attribute becomes a no-op + * (strong definitions always satisfy weak references); the attribute + * is intentionally retained so str.c remains link-safe in isolation. + */ +extern void *ailang_rc_alloc(size_t size) __attribute__((weak)); /* Byte-equality on NUL-terminated strings. Mirrors strcmp(...) == 0 * but exposed under a stable AILang-namespaced ABI so the codegen @@ -38,3 +62,82 @@ int ail_str_compare(const char *a, const char *b) { if (r > 0) return 1; return 0; } + +/* Heap-Str slab allocator. Lays out: + * + * [ rc_header (8B) | len (8B) | bytes... | NUL ] + * ^ ^ + * payload-8 payload (returned) + * + * Calls `ailang_rc_alloc(8 + len + 1)`. The +8 is the len field; + * the +1 is the trailing NUL byte that keeps `@strcmp` and `@puts` + * happy under the shared consumer ABI. Writes `len` into the first + * 8 bytes of the payload and returns the payload pointer; the + * caller is responsible for filling bytes [8 .. 8 + len) and the + * `NUL` byte at offset `8 + len`. + * + * Private (static linkage) — callers within this TU only. + */ +static char *str_alloc(uint64_t len) { + char *payload = (char *)ailang_rc_alloc(8 + len + 1); + *(uint64_t *)payload = len; + return payload; +} + +/* Convert an i64 into a heap-allocated Str. Format `"%lld"`. The + * worst-case width is 20 chars (i64::MIN = "-9223372036854775808"); + * a 64-byte stack buffer is comfortably oversized. `snprintf`'s + * return tells us how many bytes *would* have been written; if that + * exceeds the buffer we abort() defensively — this should be + * unreachable for any valid i64 input but survives format-string + * widening if a future change ever swaps `%lld` for something more + * verbose without re-sizing the buffer. + * + * Returns the payload pointer of a fresh heap-Str slab. Caller owns + * the refcount (init to 1 by `ailang_rc_alloc`); standard RC + * discipline (`ailang_rc_inc` / `ailang_rc_dec`) applies. + */ +char *ailang_int_to_str(int64_t n) { + char buf[64]; + int written = snprintf(buf, sizeof(buf), "%lld", (long long)n); + if (written < 0 || (size_t)written >= sizeof(buf)) { + fprintf(stderr, + "ailang_int_to_str: snprintf truncation/error " + "(written=%d, buffer=%zu) — should be unreachable for any valid i64; aborting\n", + written, sizeof(buf)); + abort(); + } + uint64_t len = (uint64_t)written; + char *payload = str_alloc(len); + memcpy(payload + 8, buf, len); + payload[8 + len] = '\0'; + return payload; +} + +/* Convert a double into a heap-allocated Str. Format `"%g"` — + * matches `io/print_float`'s libc rendering for IEEE consistency. + * NaN renders as libc-default (`nan`/`-nan`/etc.); ±Inf as `inf` + * /`-inf`. Worst-case width for `%g` on a `double` is bounded by + * the libc default precision (`%.6g` ⇒ at most ~13 chars including + * sign and exponent); the 64-byte stack buffer is comfortably + * oversized. Defensive `abort()` on truncation, same shape as + * `ailang_int_to_str`. + * + * Returns the payload pointer of a fresh heap-Str slab. + */ +char *ailang_float_to_str(double x) { + char buf[64]; + int written = snprintf(buf, sizeof(buf), "%g", x); + if (written < 0 || (size_t)written >= sizeof(buf)) { + fprintf(stderr, + "ailang_float_to_str: snprintf truncation/error " + "(written=%d, buffer=%zu) — should be unreachable for any finite double; aborting\n", + written, sizeof(buf)); + abort(); + } + uint64_t len = (uint64_t)written; + char *payload = str_alloc(len); + memcpy(payload + 8, buf, len); + payload[8 + len] = '\0'; + return payload; +}