Bullet-form one-pager for converting raw C into ABC idioms. Each section states the goal it serves, then the concrete moves. References the canonical docs — read them for full coverage:
CLAUDE.md — coding guidelines (esp. §1, §5, §10, §16)abc/S.md — slice types and typed-function tableabc/B.md — buffer types and operationsabc/INDEX.md — boundary-movers and module indexdog/INDEX.md — tokenizer + dog/ shared infraGoal: express every read and write as a typed slice/buffer op so data consumption is tracked and no pointer ever escapes into arithmetic.
sFeed / sFeed1 (all-or-nothing, returns
SNOROOM if input doesn't fit). Examples: u64sFeed1(slice, val), u8sFeed(into, from).
sDrain / sDrain1 (write whatfits, advance both sides). Use when input may exceed remaining room and progress matters.
a_dup(T, name, src) or `*sMv(dst,
src) (e.g. u8csMv, u8sMv). Never slice0 = ptr; slice1 = ptr + len;`.
a_pad(T, name, len), not raw T name[len].a_cstr(name, "lit").$for / $rof / $eat, sUsed1 / sUsed,
sShed (S.md). No raw *p++, ptr + N, end - start.
u8csLen over $len,
u8bReset over Breset, u8sMv over $mv. Reach for a generic only when the typed form doesn't exist.
slices and buffers): abc/INDEX.md §"Slice / buffer boundary movers".
pointer arithmetics, pass them to slice based API, then drop or rip apart into pointers. Never ever do that! Slice API is nice as long as you dont switch back and forth between modes. Mode switching creates impedance mismatch and makes things worse.
call(u8sFeed...) etc to prevent garbage/incomplete data. This does not apply with explicit const lengths only, then just u8sFeed()
a_carve and friends
Goal: take per-call scratch from the BASS arena so it dies at the call()/try() boundary, never leaking via heap or a fixed stack buffer. Prefer BASS over heap u8bAllocate or fixed a_pad() for scratch.
a_carve(T, name, cap) — acquire a writable buffer of cap
elements (the workhorse, e.g. a_carve(u8, buf, 1UL<<20)). Use for scratch you feed into.
a_lign(T, gauge) … a_cquire(T, slice) — open a gauge onBASS, fill it, then close it into a slice. The bracketing pair for "build a slice of unknown length in place".
a_rent(T, news, src) / a_ren(news, src) — one-shot copy of
src into a fresh BASS slice news (a_ren = the u8 case).
a_pad(T, name, len) (fixed T name[len]
buffer), a_dup(T, name, src) (alias a slice), a_cstr(name, "lit") (literal slice), a_path(name, …) (path buffer).
Rules (PRO.h §"BASS-implicit arena macros"):
call() —
call() snapshots+restores BASS and would undo the acquisition. Acquire in the op's own sane() frame; a call(...) rewinds BASS to its entry, freeing whatever the callee carved (so a per-iteration call/try frees that iteration's carves).
__ + return __, exactly like call().a_carve / a_rent between an a_lign and its matching
a_cquire — those advance both DATA and IDLE and corrupt the in-flight gauge (a must() guard enforces DATA-empty at entry).
wh128 etc.) must zerob(name) after
acquire — BASS reuses memory that carries leftover bytes from previously-rewound carves (see graf/INDEX.md).
Goal: keep paths as owned, NUL-terminated buffers built with the PATH helpers, never a hand-assembled char[N].
path8b — owned path buffer, *NUL-terminated by
construction*. The right type whenever code currently materialises a char[N] to satisfy a NUL contract.
a_path(name, base?, seg…) — stack path buffer, optionallypre-fed.
PATHu8bFeed, PATHu8bAdd, PATHu8bPush.$path(buf) — u8cs view over a path buffer's NUL-terminated
DATA. Use this when handing a path8b to a function expecting u8cs.
PATHu8bAlloc / PATHu8bFree (4 KiB).Goal: carry SHAs in the typed records and accept any 6–40 hex hashlet prefix, never a raw byte array plus a manual length.
sha1 (20 raw bytes) and sha1hex (40 hex ASCII)
from dog/WHIFF.h. Conversions: sha1hexFromSha1, sha1FromSha1hex, sha1hexFromHex, sha1hexSlice.
in this range; len != 40 is too strict.
u8[20] / char[40] plus manual length.
Goal: zero structs with the typed helpers so intent is explicit and never a hand-rolled memset.
zero(x) / zerop(p) from abc/S.h. Don't hand-roll
memset(&x, 0, sizeof(x)) or memset(p, 0, sizeof(*p)). Also, typed fns are preferred, e.g. sha1Zero(&hash)
Goal: when a callee only reads bytes, change its signature to take a slice and pass it directly, killing the copy-into-scratch dance.
memcpy(buf, slice[0], len); buf[len]=0; foo(buf) is a
refactor candidate. Change foo's signature to take u8csc (or path8sc) and pass the slice directly. The truncate-cap + memcpy + manual NUL goes away — and so does the hidden info loss when paths exceed FILE_PATH_MAX_LEN.
const char *line,
switch to u8csc line and format with U8SFMT / u8sFmt.
Goal: route all parsing through a ragel machine; hand-written byte scanners are forbidden.
Never ever do manual parsing in C Never ever do manual parsing in C Never ever do manual parsing in C Never ever do manual parsing in C Use ragel parsers for that
Goal: allocate, map, and open at the top of the call chain; worker functions borrow resources and never own them.
functions receive resources, never own them.
cli-shaped main entry points the canonical pattern iswrapper + worker so a single allocation/free pair covers every exit path the worker takes:
static ok64 xxxcli_inner(cli *c) { /* call(...)/done; throughout */ }
ok64 xxxcli() {
sane(1);
cli c = {};
call(PATHu8bAlloc, c.repo);
try(xxxcli_inner, &c);
PATHu8bFree(c.repo);
done;
}
try runs the worker without short-circuiting; done; returnsthe worker's status after cleanup.
Buffers are not copies (pass a gauge u8g or u8bp instead).
Goal: drive control flow and error propagation through the sane()/call()/try()/done cycle, confined to .c files.
call/done must start with sane(...) —
it declares the implicit __ carrier. Two sane() calls in the same scope re-declare; combine the conditions.
call(f, …) — invoke; on non-OK, propagate via early return.try(f, …) — invoke; capture status without returning. Pair
with then / nedo / on(code) or proceed to cleanup + done;.
fail(code) — return that code with a trace.MAIN(fn) / TEST(fn) / FUZZ(...) declare PRO.h globals;non-MAIN entry points must not use PRO.h macros.
its macros pollute namespaces. .c files only.
abc/ok64 generates the boilerplate
Goal: names follow MOD typ8 VerbStuff, record types end in their bit width, and error codes are RON60-encoded.
MOD typ8 VerbStuff — e.g. HEXu8sFeed, KEEPSync,
URIutf8Drain.
sha256, u64, tok32.Stuff is combinatorial flavor (FeedSome, DrainAll,
FromHex).
abc/ok64.Goal: reuse before you add — check the INDEX.md files first, fix bugs repro-first, and keep the indexes current.
INDEX.md files first (CLAUDE.md §13, §14): a helper
may already exist. Check abc/INDEX.md, dog/INDEX.md, and the module's own INDEX.md before adding anything.
u8cs (consumed
drain target) vs u8csc (immutable input) — choose accordingly.
the failing case first, then fix.
INDEX.md when you add or renameheaders (CLAUDE.md §14).