From 187d04814d4485bee1bb85f6f7433c2f3d146fb7 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 12 May 2026 17:56:03 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20hs.3=20=E2=80=94=20heap-Str=20runtime?= =?UTF-8?q?=20additions,=202=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure runtime/str.c addition: str_alloc helper + ailang_int_to_str + ailang_float_to_str. No codegen, checker, or build-pipeline changes in this iter — the IR-side wiring (declare lines, lowering, checker install, rc.c always-linked) all batch into hs.4. Acceptance gate: runtime continues to build and link with the new symbols present but unreferenced (clang -O2 dead-strips them until hs.4 wires a caller). Two tasks: (1) append three functions + includes + extern decl to str.c, verify via clang -c + nm; (2) full workspace + cross_lang + compile_check + check sweep stays green. --- docs/plans/hs.3.md | 332 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 docs/plans/hs.3.md diff --git a/docs/plans/hs.3.md b/docs/plans/hs.3.md new file mode 100644 index 0000000..382e282 --- /dev/null +++ b/docs/plans/hs.3.md @@ -0,0 +1,332 @@ +# hs.3 — Heap-Str runtime additions — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-12-heap-str-abi.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Add three new symbols to `runtime/str.c`: a private +`str_alloc(uint64_t len)` helper that allocates a heap-Str slab via +the existing `ailang_rc_alloc` and writes the length prefix; plus +`ailang_int_to_str(int64_t)` and `ailang_float_to_str(double)`, the +two extern formatters that compose `str_alloc` with `snprintf`. No +IR-side caller is wired in this iter — that lands in hs.4 together +with the IR-header `declare` lines, the `Literal::Call` lowering, the +checker install, and the build-pipeline change to link `rc.c` +unconditionally. Hs.3 is the runtime-infrastructure foundation; the +acceptance gate is that the workspace continues to build and that all +existing regression sweeps stay green with the new symbols present but +unreferenced. + +**Architecture:** Single-file modification of `runtime/str.c`. New +`#include` lines pick up `` (for `int64_t` / `uint64_t`), +`` (for `snprintf`), and `` (for `abort` and the +`size_t` shape of `ailang_rc_alloc`). A four-line `extern` block +declares `ailang_rc_alloc` from `runtime/rc.c` (which already exposes +it with default external linkage). Two new public extern functions +and one new private static helper are appended after +`ail_str_compare`. The Linker drops the new symbols at the unused-fn +elision pass under `clang -O2` for every program that does not yet +call them — which is every program until hs.4 wires the codegen +lowering. The `runtime/str.c` translation unit is unconditionally +compiled and linked across all three `--alloc` strategies (see the +build-pipeline notes from hs.3 recon), so the new symbols are present +in every binary regardless of allocator choice. + +**Tech Stack:** C99 (`runtime/str.c`). No Rust changes. No build- +pipeline changes. No new tests; the existing workspace + bench +sweeps verify that the runtime continues to compile and link. + +--- + +## Files this plan creates or modifies + +- Modify: `runtime/str.c` — append three new functions + (one private static helper + two extern formatters) and the + supporting `#include` / `extern` lines. Existing functions + `ail_str_eq` (lines 24-26) and `ail_str_compare` (lines 35-40) + stay unchanged. +- Untouched: `runtime/rc.c` — `ailang_rc_alloc` is already exposed at + `runtime/rc.c:113` with default external linkage. +- Untouched: `runtime/bump.c` — orthogonal allocator stub. +- Untouched: `crates/ail/src/main.rs:2271-2293` — the existing + unconditional `runtime/str.c` clang-link step already covers the + new symbols. +- Untouched: `crates/ailang-codegen/src/lib.rs` — IR-side + declarations and lowering land in hs.4. +- Untouched: `crates/ailang-check/src/builtins.rs` — checker install + lands in hs.4. + +--- + +## Task 1 — Append the three new symbols to runtime/str.c + +**Files:** +- Modify: `runtime/str.c` + +- [ ] **Step 1: Extend the include block** + +Locate the existing include block at `runtime/str.c:16-17`. After +the existing `#include ` and `#include ` lines, +append three more: + +```c +#include +#include +#include +``` + +`` provides `int64_t`/`uint64_t`. `` provides +`snprintf`. `` provides `abort` and the `size_t` shape used +by the `ailang_rc_alloc` extern declaration that follows in Step 2. + +- [ ] **Step 2: Add the `ailang_rc_alloc` extern declaration** + +Immediately below the include block (still before the existing +`ail_str_eq` definition), add: + +```c +/* 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). + */ +extern void *ailang_rc_alloc(size_t size); +``` + +The body of the file remains unchanged from line 19 (the +`ail_str_eq` doc-comment) through line 40 (the closing brace of +`ail_str_compare`). + +- [ ] **Step 3: Append the `str_alloc` private helper after + `ail_str_compare`** + +After the closing brace of `ail_str_compare` (currently line 40), +append: + +```c +/* 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; +} +``` + +The `(uint64_t *)payload` cast aliases the first eight bytes of the +payload as a single `uint64_t` length slot, matching the IR-side +convention (`<{ i64, [N+1 x i8] }>` for static-Str; here heap-Str's +analog of the same layout, with the rc-header sitting *above* the +payload at `payload - 8` instead of being baked into the global as +the static-Str path does). + +- [ ] **Step 4: Append `ailang_int_to_str`** + +Append immediately after `str_alloc`: + +```c +/* 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; +} +``` + +The `(long long)n` cast keeps the format specifier portable across +platforms where `int64_t` may not be exactly `long long`. The `+ 8` +offset on the `memcpy` lands at the bytes region (past the +len-field). The terminating NUL at `payload[8 + len]` is what keeps +`@strcmp` and `@puts` working through the shared consumer ABI. + +- [ ] **Step 5: Append `ailang_float_to_str`** + +Append immediately after `ailang_int_to_str`: + +```c +/* 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; +} +``` + +The body is structurally identical to `ailang_int_to_str`; only the +format specifier and the input type differ. Keeping the two +implementations parallel rather than collapsing into a generic +template trades a small redundancy for very explicit per-builtin +behaviour the future LLM-author can read top-to-bottom without +indirection. + +- [ ] **Step 6: Compile-check str.c standalone** + +Run: +``` +clang -c -O2 -Wall -Werror -o /tmp/str_hs3.o runtime/str.c +``` + +Expected: exit code 0, no warnings. This verifies the new symbols +parse and pass `-Wall -Werror` cleanly (signed/unsigned mismatches, +undeclared functions, missing includes, format-string mismatches all +caught here). + +- [ ] **Step 7: Inspect the resulting symbol table** + +Run: +``` +nm /tmp/str_hs3.o | grep -E "ail_str|str_alloc|ailang_int_to_str|ailang_float_to_str" +``` + +Expected output (order may vary): +``` +0000000000000000 T ail_str_compare +0000000000000000 T ail_str_eq +0000000000000000 T ailang_float_to_str +0000000000000000 T ailang_int_to_str + U ailang_rc_alloc +0000000000000000 t str_alloc +``` + +Confirms: two existing public symbols (`ail_str_eq`, +`ail_str_compare`) plus two new public symbols (`ailang_int_to_str`, +`ailang_float_to_str`, both uppercase `T`), one new private symbol +(`str_alloc`, lowercase `t` for static linkage), and one undefined +reference (`ailang_rc_alloc`, `U`) that the link step will resolve +against `runtime/rc.c`. + +--- + +## Task 2 — Regression sweep with the new symbols present but unreferenced + +**Files:** +- (none) + +- [ ] **Step 1: Full workspace test sweep** + +Run: +``` +cargo test --workspace +``` + +Expected: every test **PASSes**. The workspace tests link `str.c` +unconditionally; `clang -O2` will dead-strip the three new +unreferenced symbols at link time, so no observable behaviour +changes. Failure here means either: (a) str.c failed to compile for +a reason Task 1 Step 6 missed (likely an interaction with the +`-Werror` flag the ail build pipeline uses), or (b) the new symbols +forced an unexpected link-time error (likely missing +`` / `` / `` propagation, or unresolved +`ailang_rc_alloc` reference if a test happens to link str.c without +rc.c — examine the failing test's `AllocStrategy` setting). + +- [ ] **Step 2: 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 +that no observable program behaviour changed. + +- [ ] **Step 3: Workspace-compile regression sweep** + +Run: +``` +bench/compile_check.py +``` + +Expected: every entry **green** (every program in `examples/` still +compiles end-to-end through `ail check` + codegen + clang link). + +- [ ] **Step 4: Latency baselines check** + +Run: +``` +bench/check.py +``` + +Expected: green within the known `latency.explicit_at_rc.*` +nondeterminism tolerance documented in recent audits. The new +symbols are dead-stripped from every binary; no path-length or +allocation change is possible from hs.3 alone. + +--- + +## Acceptance for this iteration + +- `runtime/str.c` exports `ailang_int_to_str(int64_t) -> char *` and + `ailang_float_to_str(double) -> char *` (verified via `nm`). +- `str_alloc(uint64_t)` is static (not in the export symbol set; + lowercase-`t` `nm` line). +- The new functions call `ailang_rc_alloc` correctly (the `U` + reference in `nm`). +- `cargo test --workspace` green. +- `bench/cross_lang.py`, `bench/compile_check.py`, `bench/check.py` + all green. +- No Rust, codegen, checker, or build-pipeline changes. +- The runtime functions are present but unreferenced from any IR + caller; hs.4 wires the codegen + checker + linker work that turns + them into live builtins.