From 5475f7f6f340352e0725dffeb1f601966eca1dd7 Mon Sep 17 00:00:00 2001 From: ZaneHam Date: Sun, 9 Aug 2026 00:11:22 +1200 Subject: [PATCH 1/3] bir: keep line numbers with their instructions --- src/ir/bir_dce.c | 9 ++++++++- src/ir/bir_mem2reg.c | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/ir/bir_dce.c b/src/ir/bir_dce.c index 7209fd3..7466d50 100644 --- a/src/ir/bir_dce.c +++ b/src/ir/bir_dce.c @@ -205,8 +205,13 @@ static void compact(opt_t *S) for (uint32_t j = 0; j < B->num_insts; j++) { uint32_t ii = B->first_inst + j; if ((S->dead[ii / 32] >> (ii % 32)) & 1) continue; - if (wr != ii) + if (wr != ii) { M->insts[wr] = M->insts[ii]; + /* The line table is indexed by instruction position, so it + * has to move with them or every line past the first dead + * instruction points at the wrong source. */ + M->inst_lines[wr] = M->inst_lines[ii]; + } wr++; } M->blocks[abs_b].first_inst = block_start; @@ -299,6 +304,8 @@ int bir_dce(bir_module_t *M) memmove(&M->insts[dst], &M->insts[src], count * sizeof(bir_inst_t)); + memmove(&M->inst_lines[dst], &M->inst_lines[src], + count * sizeof(M->inst_lines[0])); for (uint16_t bi = 0; bi < F->num_blocks; bi++) { uint32_t abs_b = F->first_block + bi; diff --git a/src/ir/bir_mem2reg.c b/src/ir/bir_mem2reg.c index 2d73cd0..eb56976 100644 --- a/src/ir/bir_mem2reg.c +++ b/src/ir/bir_mem2reg.c @@ -746,11 +746,15 @@ static void step7_compact(m2r_t *S) /* Second pass: copy instructions into new positions. * Use a static scratch buffer to avoid overlap issues. */ static bir_inst_t scratch[BIR_MAX_INSTS]; + /* Lines travel with their instructions or they end up describing + * whatever landed in the slot instead. */ + static uint32_t scratch_lines[BIR_MAX_INSTS]; uint32_t si = 0; for (uint32_t bi = 0; bi < S->num_blocks; bi++) { for (int pi = 0; pi < S->num_phis; pi++) { if (S->phis[pi].block != bi) continue; + scratch_lines[si] = M->inst_lines[S->phis[pi].inst]; scratch[si++] = M->insts[S->phis[pi].inst]; } uint32_t abs_b = S->base_block + bi; @@ -758,12 +762,15 @@ static void step7_compact(m2r_t *S) for (uint32_t j = 0; j < B->num_insts; j++) { uint32_t ii = B->first_inst + j; if (S->dead[ii]) continue; + scratch_lines[si] = M->inst_lines[ii]; scratch[si++] = M->insts[ii]; } } /* Copy scratch back */ memcpy(&M->insts[S->base_inst], scratch, si * sizeof(bir_inst_t)); + memcpy(&M->inst_lines[S->base_inst], scratch_lines, + si * sizeof(scratch_lines[0])); /* Update block boundaries */ uint32_t cursor = S->base_inst; @@ -921,6 +928,8 @@ int bir_mem2reg(bir_module_t *M) /* Move instructions */ memmove(&M->insts[dst], &M->insts[src], count * sizeof(bir_inst_t)); + memmove(&M->inst_lines[dst], &M->inst_lines[src], + count * sizeof(M->inst_lines[0])); /* Update block boundaries */ for (uint16_t bi = 0; bi < F->num_blocks; bi++) { From d1f55a82c5840289fe1683f9c02a0b94e00d56cf Mon Sep 17 00:00:00 2001 From: ZaneHam Date: Sun, 9 Aug 2026 00:11:23 +1200 Subject: [PATCH 2/3] vendor kauri, and lock in reproducible output --- CONTRIBUTING.md | 26 ++ Makefile | 11 +- src/barracuda.h | 4 + src/kauri.h | 717 ++++++++++++++++++++++++++++++++++++++++++++ src/kauri_impl.c | 3 + tests/reprocheck.sh | 40 +++ 6 files changed, 798 insertions(+), 3 deletions(-) create mode 100644 src/kauri.h create mode 100644 src/kauri_impl.c create mode 100644 tests/reprocheck.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a5f2ea..2e82fe1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,32 @@ Booth is written in a defensive C99 style. I spent too much time staring at NASA **No floats where integers will do.** If you're comparing ratios, cross-multiply. Floating point is for GPU shader maths, not compiler internals. +### Kauri + +Three of those rules used to be things I just tried to remember. Now there are macros for them. `barracuda.h` pulls in [Kauri](https://github.com/Zaneham/kauri), so they're already available anywhere in the tree and you don't need to include anything. + +```c +/* Bounded loop. g counts down, so a cone that won't converge stops + * instead of hanging. Pick a bound you can justify. */ +KA_GUARD(g, 64); +while (changed && g--) { + changed = fold_once(M); +} + +/* Untrusted index. Returns 1 when out of bounds, so it reads as + * "if that's rubbish, refuse". Internal bookkeeping doesn't need it. */ +if (KA_CHK(idx, M->num_insts)) return BC_ERR_VERIFY; + +/* Pool allocation. Index 0 is the sentinel, so 0 means the pool is + * full. Pass the sentinel upwards rather than winding the counter back. */ +uint32_t ni = KA_PNEW(M->num_insts, BIR_MAX_INSTS); +if (ni == 0) return 0; +``` + +Most of the existing code was written before these existed, so there's plenty that could use them. If you're reading a file and spot a loop that could take a guard, or an index coming in from outside that isn't checked, a PR is very welcome. If you're not sure whether a particular one wants it, raise an issue and we can work it out, that's a more interesting conversation than it sounds. + +Two things worth knowing. `KA_PNEW` evaluates its counter twice, so give it a plain lvalue and nothing with side effects. And Kauri has an arena allocator and a result type that Booth doesn't use, since allocation happens once per phase and errors come back as `BC_ERR_*`, so there's no need to reach for `KA_TRY`. + ### Naming Function and variable names are short, 4-7 characters. Think of it like reading a motorway sign at 100km/h, you want "SH1 NORTH" not "STATE_HIGHWAY_ONE_NORTHBOUND_DIRECTION". When you're reading a thousand lines of instruction selector at 2am you want `ra_gc` not `regalloc_graphcolor`. Look at the newer code for the pattern: `isel_emit`, `mk_hash`, `enc_vop3`, `xt_meta`, `dce_copy`. diff --git a/Makefile b/Makefile index aea7b07..1ae4475 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ ifeq ($(UNAME_S),Linux) ALT_RT = $(OBJDIR)/src/runtime/lf_gpu_hsa.o endif -SOURCES = src/main.c \ +SOURCES = src/main.c src/kauri_impl.c \ src/fe/bc_err.c src/fe/bc_render.c src/fe/preproc.c src/fe/lexer.c src/fe/parser.c src/fe/sema.c \ src/ir/bir.c src/ir/bir_print.c src/ir/bir_lower.c src/ir/bir_mem2reg.c src/ir/bir_cfold.c src/ir/bir_dce.c src/ir/bir_struct.c src/ir/bir_insert.c src/ir/bir_sroa.c src/ir/bir_inline.c \ src/tdf/tdf.c src/tdf/tdf_lower.c src/tdf/tdf_fission.c src/tdf/tdf_place.c src/tdf/tdf_noc.c \ @@ -113,7 +113,7 @@ TSRC = tests/tmain.c tests/tsmoke.c tests/tcomp.c tests/tenc.c \ tests/tbackend.c TOBJS = $(TSRC:%.c=$(OBJDIR)/%.o) -COBJS = $(OBJDIR)/src/ir/bir.o $(OBJDIR)/src/ir/bir_print.o $(OBJDIR)/src/ir/bir_lower.o $(OBJDIR)/src/ir/bir_mem2reg.o $(OBJDIR)/src/ir/bir_cfold.o $(OBJDIR)/src/ir/bir_dce.o $(OBJDIR)/src/ir/bir_struct.o $(OBJDIR)/src/ir/bir_insert.o $(OBJDIR)/src/ir/bir_sroa.o $(OBJDIR)/src/ir/bir_inline.o \ +COBJS = $(OBJDIR)/src/kauri_impl.o $(OBJDIR)/src/ir/bir.o $(OBJDIR)/src/ir/bir_print.o $(OBJDIR)/src/ir/bir_lower.o $(OBJDIR)/src/ir/bir_mem2reg.o $(OBJDIR)/src/ir/bir_cfold.o $(OBJDIR)/src/ir/bir_dce.o $(OBJDIR)/src/ir/bir_struct.o $(OBJDIR)/src/ir/bir_insert.o $(OBJDIR)/src/ir/bir_sroa.o $(OBJDIR)/src/ir/bir_inline.o \ $(OBJDIR)/src/tdf/tdf.o $(OBJDIR)/src/tdf/tdf_lower.o $(OBJDIR)/src/tdf/tdf_fission.o $(OBJDIR)/src/tdf/tdf_place.o $(OBJDIR)/src/tdf/tdf_noc.o \ $(OBJDIR)/src/tensix/rv_enc.o $(OBJDIR)/src/tensix/rv_buf.o $(OBJDIR)/src/tensix/rv_elf.o $(OBJDIR)/src/tensix/rv_isel.o $(OBJDIR)/src/tensix/noc.o $(OBJDIR)/src/tensix/emit.o \ $(OBJDIR)/runtime/soft_fp.o $(OBJDIR)/runtime/sysprint.o \ @@ -133,6 +133,11 @@ COBJS = $(OBJDIR)/src/ir/bir.o $(OBJDIR)/src/ir/bir_print.o $(OBJDIR)/src/ir/b test: $(TARGET) trunner ./trunner --all +# bir.h claims a deterministic layout. This makes that a property rather +# than an intention, and it only stays cheap if it runs from now on. +repro: $(TARGET) + tests/reprocheck.sh + trunner: $(TOBJS) $(COBJS) $(CC) $(TCFLAGS) -o $@ $^ $(LIBS) $(DL_LIB) @@ -239,4 +244,4 @@ clean: # linked in and the build silently disagrees with the source. -include $(OBJECTS:.o=.d) $(TOBJS:.o=.d) $(HOSTRT:.o=.d) -.PHONY: all clean test install uninstall coverage +.PHONY: all clean test repro install uninstall coverage diff --git a/src/barracuda.h b/src/barracuda.h index a2c4c60..255548b 100644 --- a/src/barracuda.h +++ b/src/barracuda.h @@ -5,6 +5,10 @@ #include #include +/* Bounded loops and checked indices. The arena goes unused here, since Booth + * allocates once per phase and never in a hot path. */ +#include "kauri.h" + /* ---- Version ---- * The 0.5 release was tagged v5.01, which was a typo for 0.5.1 and made the * compiler look four major versions further along than it is. Corrected here; diff --git a/src/kauri.h b/src/kauri.h new file mode 100644 index 0000000..91cdce8 --- /dev/null +++ b/src/kauri.h @@ -0,0 +1,717 @@ +/* MIT licensed, not Apache like the rest of this tree. + * Vendored from https://github.com/Zaneham/kauri + */ + +/* + * kauri.h: Memory safety for C99 + * + * Single-header library (stb-style). In ONE .c file, before #include: + * #define KAURI_IMPL + * #include "kauri.h" + * + * Named for the kauri tree: grows slowly, lives millennia, doesn't fall over. + * Unlike your heap allocator. + * + * Thread safety: none. One arena per thread, like one pen per astronaut. + * If you share arenas across threads you deserve what happens. + * + * (c) 2026. Zane's school of wonderful curiosities and maybe a compiler. + */ + +#ifndef KAURI_H +#define KAURI_H + +#include +#include +#include + +/* ---- Config ---- */ + +#ifndef KAURI_DEBUG +#define KAURI_DEBUG 0 +#endif + +#ifndef KAURI_ABORT +#define KAURI_ABORT 0 +#endif + +/* ---- Error Codes + ka_res_t ---- + * Negative = bad day. Zero = everything's fine. + * Much like altitude readings in aviation. */ + +#define KA_OK 0 +#define KA_OOB -1 /* out of bounds. the array has edges, respect them */ +#define KA_OOM -2 /* out of memory. the arena is not infinite, sorry */ +#define KA_OVFL -3 /* overflow. numbers have limits, who knew */ +#define KA_INVAL -4 /* invalid argument. rubbish in, rubbish out */ + +typedef struct { + int code; + const char *msg; /* static string, never allocated. we're not animals */ +#if KAURI_DEBUG + const char *file; + int line; +#endif +} ka_res_t; + +/* Build a result. Debug mode tattoos the source location onto it, + * like a black box recorder for your allocator. */ +#if KAURI_DEBUG +#define KA_RES(c, m) ((ka_res_t){ (c), (m), __FILE__, __LINE__ }) +#else +#define KA_RES(c, m) ((ka_res_t){ (c), (m) }) +#endif + +#define KA_RES_OK KA_RES(KA_OK, "ok") + +/* Optional early return on error. Some people prefer to stare at the + * error code and contemplate their life choices. */ +#define KA_TRY(expr) do { \ + ka_res_t _r = (expr); \ + if (_r.code != KA_OK) return _r; \ +} while (0) + +/* ---- Utility Macros ---- + * The safety nets. Deploy them or enjoy the splat. */ + +/* Alignment: __alignof__ if the compiler is civilised, fallback for C99. + * The fallback uses the struct-padding trick. Not pretty, but correct, + * which is more than can be said for most C alignment code. */ +#ifdef __GNUC__ +#define KA_ALIGN(T) __alignof__(T) +#else +#define KA_ALIGN(T) offsetof(struct { char _c; T _t; }, _t) +#endif + +/* Bounds check. In debug+abort mode this stops the program dead, + * which is better than silently scribbling over adjacent memory + * like a toddler with a permanent marker. + * Returns 1 if out of bounds, 0 if fine. */ +#if KAURI_DEBUG && KAURI_ABORT +#define KA_CHK(i, max) ka__chk((uint32_t)(i), (uint32_t)(max), __FILE__, __LINE__) +#elif KAURI_DEBUG +#define KA_CHK(i, max) ((uint32_t)(i) >= (uint32_t)(max) \ + ? (ka__oob(__FILE__, __LINE__, (uint32_t)(i), (uint32_t)(max)), 1) : 0) +#else +#define KA_CHK(i, max) ((uint32_t)(i) >= (uint32_t)(max)) +#endif + +/* Pool allocate: index 0 is the sentinel (the "nobody's home" value). + * If the pool is full, returns 0. If not, hands you the next slot. + * Evaluates cnt twice. Fine for simple lvalues, catastrophic for + * expressions with side effects. Don't be clever. */ +#define KA_PNEW(cnt, max) ((uint32_t)(cnt) >= (uint32_t)(max) ? 0u : (uint32_t)(cnt)++) + +/* Guard counter for bounded loops. Declares a uint32_t that counts down, + * preventing infinite loops like a responsible adult. + * Usage: KA_GUARD(g, 1000); while (cond && g--) { ... } */ +#define KA_GUARD(g, max) uint32_t g = (uint32_t)(max) + +/* Overflow-safe multiply. Returns 0 if the product wraps past 32 bits, + * which ka_alloc rejects. Turning "allocated 12 bytes for 4 billion + * structs" into a polite refusal. */ +static inline uint32_t +ka__smul(uint32_t a, uint32_t b) +{ + uint64_t r = (uint64_t)a * (uint64_t)b; + return r > 0xFFFFFFFFu ? 0u : (uint32_t)r; +} + +/* Typed arena alloc. Saves you from writing the cast and sizeof. + * Returns NULL if the arena says no. */ +#define KA_NEW(A, T) ((T *)ka_alloc((A), (uint32_t)sizeof(T), (uint32_t)KA_ALIGN(T))) +#define KA_NEWN(A, T, n) ((T *)ka_alloc((A), ka__smul((uint32_t)sizeof(T), (uint32_t)(n)), (uint32_t)KA_ALIGN(T))) + +/* ---- Arena ---- + * A bump allocator. Goes forward, never backward (except on reset). + * Like time, but useful. + * + * The first block is inline: no malloc for the common case where + * you hand it a stack buffer. Chaining is opt-in because malloc in + * a safety library is like a fire extinguisher that's also flammable. */ + +#define KA_CHAIN 0x01u /* allow chaining additional blocks via malloc */ + +/* Internal flag: head block was heap-allocated (ka_init with buf=NULL) */ +#define KA_F_HEAP 0x80000000u + +#if KAURI_DEBUG +#define KA__DEAD 0xDE /* poison byte. if you see this in production, repent */ +#define KA__CVAL 0xDEADCA75u /* canary sentinel. "dead cats", because coal mines are passé */ +#define KA__CSZ 4 /* canary size in bytes */ +#define KA__LMAX 256 /* max tracked allocs. if you need more, rethink your life */ + +typedef struct { uint8_t *ptr; uint32_t size; } ka__log_t; +#endif + +typedef struct ka_blk_t { + struct ka_blk_t *next; + uint8_t *base; /* data region */ + uint32_t cap; + uint32_t pos; +} ka_blk_t; + +/* Snapshot for mark/rewind. Like a save point in a video game, + * except the stakes are higher. */ +typedef struct { + ka_blk_t *blk; + uint32_t pos; +#if KAURI_DEBUG + uint32_t n_log; /* saved canary log position */ +#endif +} ka_mark_t; + +typedef struct { + ka_blk_t head; /* inline first block. no malloc needed */ + ka_blk_t *cur; /* current active block */ + uint32_t flags; + uint32_t max_blk; /* chain limit, default 64 */ + uint32_t n_blk; /* blocks in chain (including head) */ +#if KAURI_DEBUG + uint32_t n_alloc; /* total allocations. for the curious */ + uint32_t peak; /* high-water n_alloc across resets */ + uint32_t n_log; /* entries in canary log */ + ka__log_t log[KA__LMAX]; /* the post-mortem ledger. 3KB well spent. */ +#endif +} ka_arena_t; + +/* Initialise arena. buf=user buffer (stack or static), cap=size in bytes. + * If buf is NULL, allocates cap bytes from the heap (requires free later). + * flags: KA_CHAIN to allow overflow into malloc'd blocks. */ +void ka_init (ka_arena_t *A, void *buf, uint32_t cap, uint32_t flags); + +/* Bump-allocate size bytes with given alignment (must be power of 2). + * Returns NULL if no room (and chaining is off or chain limit reached). */ +void *ka_alloc(ka_arena_t *A, uint32_t size, uint32_t align); + +/* Reset arena: free chain blocks, rewind head to pos 0. + * Does NOT free the head block itself. That's ka_free's job. */ +void ka_rst (ka_arena_t *A); + +/* Free everything. If the head was heap-backed, frees that too. + * After this, the arena is a husk. Don't touch it. */ +void ka_free (ka_arena_t *A); + +/* How many bytes are currently occupied / total capacity. + * Walks the chain, bounded by max_blk. */ +uint32_t ka_used (const ka_arena_t *A); +uint32_t ka_cap (const ka_arena_t *A); + +/* Save/restore point. Rewind frees any chain blocks allocated after + * the mark. Does NOT zero the memory. Ghosts of old data remain. + * (In debug mode it does, actually. 0xDE everywhere. Trust nothing.) */ +ka_mark_t ka_mark (ka_arena_t *A); +void ka_rwind(ka_arena_t *A, ka_mark_t m); + +/* Peak allocation count across resets. Debug only. Tells you how + * big your arena actually needs to be, since guessing is for gamblers. */ +uint32_t ka_peak (ka_arena_t *A); + +/* Allocate and copy. Like strdup but for arbitrary blobs. + * Returns NULL on OOM. */ +void *ka_dup (ka_arena_t *A, const void *src, uint32_t size, uint32_t align); + +/* String duplicate into arena. Adds NUL terminator. + * len=0 means "measure it for me" (strlen). If you pass len=0 + * on a non-NUL-terminated string, that's on you. Returns NULL on OOM. */ +char *ka_sdup (ka_arena_t *A, const char *s, uint32_t len); + + +/* ---- String Builder ---- + * A non-owning string buffer. You provide the backing memory + * (stack, arena, whatever), it provides the safety. + * Truncates gracefully. Always NUL-terminated, never overflows. */ + +typedef struct { + char *ptr; /* NOT owned. caller manages lifetime */ + uint32_t len; + uint32_t cap; +} ka_str_t; + +/* Init string builder with user-supplied buffer. + * cap must be >= 1 (need room for the NUL). */ +int ka_sinit(ka_str_t *S, char *buf, uint32_t cap); + +/* Append slen bytes from src. Returns 0 on success, -1 if truncated. */ +int ka_scat (ka_str_t *S, const char *src, uint32_t slen); + +/* Printf into the string. Returns 0 on success, -1 if truncated. */ +#ifdef __GNUC__ +__attribute__((format(printf, 2, 3))) +#endif +int ka_sfmt (ka_str_t *S, const char *fmt, ...); + +/* Append a single character. Returns 0 on success, -1 if truncated. */ +int ka_schr (ka_str_t *S, char c); + +/* Clear the string to empty (len=0, ptr[0]='\0'). */ +void ka_sclr (ka_str_t *S); + +/* Compare two string builders lexicographically. Returns <0, 0, >0. */ +int ka_scmp (const ka_str_t *a, const ka_str_t *b); + + +/* ---- Debug Internals ---- */ + +void ka__oob(const char *file, int line, uint32_t idx, uint32_t max); + +#if KAURI_DEBUG && KAURI_ABORT +static inline int ka__chk(uint32_t i, uint32_t max, const char *file, int line) +{ + if (i >= max) { ka__oob(file, line, i, max); } + return i >= max; +} +#endif + +#endif /* KAURI_H */ + + +/* ================================================================ + * IMPLEMENTATION + * ================================================================ + * Define KAURI_IMPL in exactly one .c file before including this. + * Two definitions and the linker will express its displeasure. */ + +#ifdef KAURI_IMPL + +#include +#include +#include + +/* ---- Debug ---- */ + +void +ka__oob(const char *file, int line, uint32_t idx, uint32_t max) +{ + /* The pilot's last words: "that index looked fine to me" */ + fprintf(stderr, "kauri: OOB %s:%d idx=%u max=%u\n", + file, line, (unsigned)idx, (unsigned)max); +#if KAURI_ABORT + abort(); +#endif +} + +/* ---- Arena ---- */ + +void +ka_init(ka_arena_t *A, void *buf, uint32_t cap, uint32_t flags) +{ + memset(A, 0, sizeof(*A)); + A->flags = flags; + A->max_blk = 64; + A->n_blk = 1; + + if (buf) { + A->head.base = (uint8_t *)buf; + A->head.cap = cap; + } else { + /* Heap-backed: malloc the data region separately. + * Because sometimes the stack just isn't big enough + * for your ambitions. */ + uint8_t *p = (uint8_t *)malloc(cap); + if (!p) { + /* Well. This is embarrassing. A safety library that + * can't allocate. At least we fail gracefully. */ + A->head.base = NULL; + A->head.cap = 0; + return; + } + A->head.base = p; + A->head.cap = cap; + A->flags |= KA_F_HEAP; + } + A->head.pos = 0; + A->head.next = NULL; + A->cur = &A->head; +} + +/* Align pos upward to `align` (must be power of 2). */ +static uint32_t +ka__aup(uint32_t pos, uint32_t align) +{ + return (pos + align - 1u) & ~(align - 1u); +} + +/* Try to allocate from a specific block. Returns NULL if no room. */ +static void * +ka__blka(ka_blk_t *b, uint32_t size, uint32_t align) +{ + uint32_t apos, end; + if (!b || !b->base) return NULL; + apos = ka__aup(b->pos, align); + end = apos + size; + if (end < apos) return NULL; /* overflow. nice try */ + if (end > b->cap) return NULL; + b->pos = end; + return b->base + apos; +} + +/* Grow the chain by one block. Returns the new block or NULL. + * NOTE: assumes fresh blocks start at pos=0, so ka__blka won't + * waste space re-aligning. If you ever add block headers, revisit. */ +static ka_blk_t * +ka__grow(ka_arena_t *A, uint32_t need) +{ + ka_blk_t *nb; + uint32_t cap; + + if (A->n_blk >= A->max_blk) return NULL; + + /* New block is at least as big as what we need, or the head cap, + * whichever is larger. No point adding a tiny block. */ + cap = A->head.cap; + if (need > cap) cap = need; + + nb = (ka_blk_t *)malloc(sizeof(ka_blk_t) + cap); + if (!nb) return NULL; + + nb->base = (uint8_t *)(nb + 1); + nb->cap = cap; + nb->pos = 0; + nb->next = NULL; + + /* Append to chain after current block */ + A->cur->next = nb; + A->cur = nb; + A->n_blk++; + return nb; +} + +#if KAURI_DEBUG +/* Log an allocation for canary tracking. The ledger has a fixed + * size because unbounded logs are an oxymoron in safety code. */ +static void +ka__alog(ka_arena_t *A, uint8_t *ptr, uint32_t size) +{ + uint32_t cval = KA__CVAL; + memcpy(ptr + size, &cval, KA__CSZ); + if (A->n_log < KA__LMAX) { + A->log[A->n_log].ptr = ptr; + A->log[A->n_log].size = size; + A->n_log++; + } else if (A->n_log == KA__LMAX) { + fprintf(stderr, "kauri: canary log full (%u). further allocs untracked\n", + (unsigned)KA__LMAX); + A->n_log++; /* only warn once */ + } +} + +/* Walk the canary log [from..to) and verify each sentinel. + * If something scribbled past an allocation, we want to know + * before it scribbles past something important. */ +static void +ka__cchk(ka_arena_t *A, uint32_t from, uint32_t to) +{ + uint32_t i; + for (i = from; i < to; i++) { + uint32_t v; + memcpy(&v, A->log[i].ptr + A->log[i].size, KA__CSZ); + if (v != KA__CVAL) { + fprintf(stderr, "kauri: CANARY CORRUPT ptr=%p size=%u\n", + (void *)A->log[i].ptr, (unsigned)A->log[i].size); + } + } +} +#endif + +void * +ka_alloc(ka_arena_t *A, uint32_t size, uint32_t align) +{ + void *p; + uint32_t rsz; + if (!A || !size || !align) return NULL; + +#if KAURI_DEBUG + rsz = size + KA__CSZ; /* room for the canary's perch */ +#else + rsz = size; +#endif + + /* Try current block first */ + p = ka__blka(A->cur, rsz, align); + if (p) { +#if KAURI_DEBUG + ka__alog(A, (uint8_t *)p, size); + A->n_alloc++; +#endif + return p; + } + + /* No room. If chaining is allowed, grow. */ + if (A->flags & KA_CHAIN) { + ka_blk_t *nb = ka__grow(A, ka__aup(rsz, align)); + if (nb) { + p = ka__blka(nb, rsz, align); +#if KAURI_DEBUG + if (p) { + ka__alog(A, (uint8_t *)p, size); + A->n_alloc++; + } +#endif + return p; + } + } + + return NULL; +} + +void +ka_rst(ka_arena_t *A) +{ + ka_blk_t *b, *next; + KA_GUARD(g, 64); + + if (!A) return; + +#if KAURI_DEBUG + /* Check the dead cats before clearing the ledger */ + ka__cchk(A, 0, A->n_log); + if (A->n_alloc > A->peak) A->peak = A->n_alloc; + + /* Poison the head block. Stale data is the enemy */ + if (A->head.base && A->head.pos > 0) + memset(A->head.base, KA__DEAD, A->head.pos); +#endif + + /* Free chain blocks (everything after head) */ + b = A->head.next; + while (b && g--) { + next = b->next; +#if KAURI_DEBUG + if (b->base && b->pos > 0) memset(b->base, KA__DEAD, b->pos); +#endif + free(b); + b = next; + } + A->head.next = NULL; + A->head.pos = 0; + A->cur = &A->head; + A->n_blk = 1; +#if KAURI_DEBUG + A->n_alloc = 0; + A->n_log = 0; +#endif +} + +void +ka_free(ka_arena_t *A) +{ + if (!A) return; + ka_rst(A); + + if (A->flags & KA_F_HEAP) { +#if KAURI_DEBUG + if (A->head.base && A->head.cap > 0) + memset(A->head.base, KA__DEAD, A->head.cap); +#endif + free(A->head.base); + } + memset(A, 0, sizeof(*A)); +} + +uint32_t +ka_used(const ka_arena_t *A) +{ + const ka_blk_t *b; + uint32_t total = 0; + KA_GUARD(g, 65); + + if (!A) return 0; + b = &A->head; + while (b && g--) { + total += b->pos; + b = b->next; + } + return total; +} + +uint32_t +ka_cap(const ka_arena_t *A) +{ + const ka_blk_t *b; + uint32_t total = 0; + KA_GUARD(g, 65); + + if (!A) return 0; + b = &A->head; + while (b && g--) { + total += b->cap; + b = b->next; + } + return total; +} + +uint32_t +ka_peak(ka_arena_t *A) +{ +#if KAURI_DEBUG + if (!A) return 0; + return A->peak; +#else + (void)A; + return 0; +#endif +} + +ka_mark_t +ka_mark(ka_arena_t *A) +{ + ka_mark_t m; + m.blk = A->cur; + m.pos = A->cur->pos; +#if KAURI_DEBUG + m.n_log = A->n_log; +#endif + return m; +} + +void +ka_rwind(ka_arena_t *A, ka_mark_t m) +{ + ka_blk_t *b, *next; + KA_GUARD(g, 64); + + if (!A) return; + +#if KAURI_DEBUG + /* Check canaries for allocations since the mark */ + ka__cchk(A, m.n_log, A->n_log); + A->n_log = m.n_log; + + /* Poison the rewound region of the marked block */ + if (m.blk->base && m.blk->pos > m.pos) + memset(m.blk->base + m.pos, KA__DEAD, m.blk->pos - m.pos); +#endif + + /* Free blocks after the marked block */ + b = m.blk->next; + while (b && g--) { + next = b->next; + A->n_blk--; +#if KAURI_DEBUG + if (b->base && b->pos > 0) memset(b->base, KA__DEAD, b->pos); +#endif + free(b); + b = next; + } + m.blk->next = NULL; + m.blk->pos = m.pos; + A->cur = m.blk; +} + +void * +ka_dup(ka_arena_t *A, const void *src, uint32_t size, uint32_t align) +{ + void *p; + if (!src || !size) return NULL; + p = ka_alloc(A, size, align); + if (p) memcpy(p, src, size); + return p; +} + +char * +ka_sdup(ka_arena_t *A, const char *s, uint32_t len) +{ + char *p; + if (!s) return NULL; + if (len == 0) len = (uint32_t)strlen(s); + p = (char *)ka_alloc(A, len + 1, 1); + if (!p) return NULL; + memcpy(p, s, len); + p[len] = '\0'; + return p; +} + +/* ---- String Builder ---- */ + +int +ka_sinit(ka_str_t *S, char *buf, uint32_t cap) +{ + if (!S || !buf || cap < 1) return -1; + S->ptr = buf; + S->cap = cap; + S->len = 0; + S->ptr[0] = '\0'; + return 0; +} + +int +ka_scat(ka_str_t *S, const char *src, uint32_t slen) +{ + uint32_t avail, cpy; + int trunc = 0; + + if (!S || !src) return -1; + avail = S->cap - S->len - 1; + cpy = slen; + if (cpy > avail) { + cpy = avail; + trunc = -1; + } + if (cpy > 0) { + memcpy(S->ptr + S->len, src, cpy); + S->len += cpy; + } + S->ptr[S->len] = '\0'; + return trunc; +} + +int +ka_sfmt(ka_str_t *S, const char *fmt, ...) +{ + va_list ap; + int n; + uint32_t avail; + + if (!S || !fmt) return -1; + avail = S->cap - S->len; + if (avail == 0) return -1; + + va_start(ap, fmt); + n = vsnprintf(S->ptr + S->len, avail, fmt, ap); + va_end(ap); + + if (n < 0) return -1; + if ((uint32_t)n >= avail) { + /* Truncated. vsnprintf already NUL-terminated at the boundary. */ + S->len = S->cap - 1; + return -1; + } + S->len += (uint32_t)n; + return 0; +} + +int +ka_schr(ka_str_t *S, char c) +{ + if (!S) return -1; + if (S->len + 1 >= S->cap) return -1; + S->ptr[S->len++] = c; + S->ptr[S->len] = '\0'; + return 0; +} + +void +ka_sclr(ka_str_t *S) +{ + if (!S || !S->ptr) return; + S->len = 0; + S->ptr[0] = '\0'; +} + +int +ka_scmp(const ka_str_t *a, const ka_str_t *b) +{ + uint32_t mlen; + int r; + + if (!a || !b) return 0; + mlen = a->len < b->len ? a->len : b->len; + r = memcmp(a->ptr, b->ptr, mlen); + if (r != 0) return r; + if (a->len < b->len) return -1; + if (a->len > b->len) return 1; + return 0; +} + +#endif /* KAURI_IMPL */ diff --git a/src/kauri_impl.c b/src/kauri_impl.c new file mode 100644 index 0000000..cf3cc3d --- /dev/null +++ b/src/kauri_impl.c @@ -0,0 +1,3 @@ +/* The one translation unit that instantiates Kauri. */ +#define KAURI_IMPL +#include "kauri.h" diff --git a/tests/reprocheck.sh b/tests/reprocheck.sh new file mode 100644 index 0000000..e17c60c --- /dev/null +++ b/tests/reprocheck.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Same input twice, same bytes out. Separate processes, so ASLR and any +# pointer-ordered container shows up as a mismatch. +set -u + +root=$(git rev-parse --show-toplevel) +cd "$root" + +kath=./kath.exe +[ -x "$kath" ] || kath=./kath +[ -x "$kath" ] || { echo "reprocheck: no kath binary, run make first" >&2; exit 2; } + +modes="--amdgpu --nvidia-ptx --ir" +fail=0 +checked=0 +skipped=0 + +for f in tests/*.cu; do + for m in $modes; do + a=$("$kath" $m "$f" 2>/dev/null | sha256sum 2>/dev/null | cut -d' ' -f1) + # A file the backend refuses is not a reproducibility failure. + if [ -z "$a" ]; then skipped=$((skipped + 1)); continue; fi + b=$("$kath" $m "$f" 2>/dev/null | sha256sum 2>/dev/null | cut -d' ' -f1) + checked=$((checked + 1)) + if [ "$a" != "$b" ]; then + fail=$((fail + 1)) + echo "NOT REPRODUCIBLE: $m $f" >&2 + echo " $a" >&2 + echo " $b" >&2 + fi + done +done + +if [ "$fail" -ne 0 ]; then + echo >&2 + echo "reprocheck: $fail of $checked runs differed" >&2 + exit 1 +fi + +echo "reprocheck: $checked runs reproducible ($skipped skipped)" From 0cfd29de054912e1b4f5c8e9bf3346c57612ffbc Mon Sep 17 00:00:00 2001 From: ZaneHam Date: Sun, 9 Aug 2026 12:42:27 +1200 Subject: [PATCH 3/3] changelog for #160 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd26204..0997dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ Booth — Changelog ## Unreleased +### Architecture + +- #160: DCE and mem2reg move instructions without moving `inst_lines[]` + with them, so every line number past the first deleted instruction + pointed at the wrong source. Four sites fixed + (Zane Hambly, 2026-08-09) + +### Build + +- #160: vendor Kauri (MIT) as `src/kauri.h`, included from `barracuda.h`, + so `KA_GUARD`, `KA_CHK` and `KA_PNEW` are available tree-wide + (Zane Hambly, 2026-08-09) + +### CI and tests + +- #160: `make repro` compiles every test file twice under `--amdgpu`, + `--nvidia-ptx` and `--ir` and compares the bytes, so the deterministic + layout `bir.h` claims is checked rather than assumed + (Zane Hambly, 2026-08-09) + ## 2026-08-07 Version 0.5.2.