832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
333 lines
12 KiB
Markdown
333 lines
12 KiB
Markdown
# hs.3 — Heap-Str runtime additions — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0019-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 `<stdint.h>` (for `int64_t` / `uint64_t`),
|
|
`<stdio.h>` (for `snprintf`), and `<stdlib.h>` (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 <stdbool.h>` and `#include <string.h>` lines,
|
|
append three more:
|
|
|
|
```c
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
```
|
|
|
|
`<stdint.h>` provides `int64_t`/`uint64_t`. `<stdio.h>` provides
|
|
`snprintf`. `<stdlib.h>` 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
|
|
`<stdint.h>` / `<stdio.h>` / `<stdlib.h>` 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.
|