Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,7 @@ tdf_flag_out.txt
# consumer. Same paths locally if you follow examples/cmake/CMakeLists.txt.
/_prefix/
/_example/

# Mutation harness scratch copy. Removed on exit, but an interrupted run
# can leave it behind.
/.mutscratch/
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,52 @@ Booth — Changelog

## Unreleased

### Frontend

- `kath --mlir` reads MLIR text, no LLVM in the path. Čertík's pure-C
reader vendored under `src/mlir/vendor` (mlir 826b69c9, corec a160199d),
reached only through `src/mlir/mlir_fe.c` (Zane Hambly, 2026-08-11)

- `src/mlir/lower.c` walks the parsed module into BIR: `func.func`, `return`,
`arith.constant` and every arith binop, compare and conversion the reader
classifies. From there it is the pipeline CUDA and Triton already use, and
MLIR reaches all four backends. `--mlir --pp` reprints instead
(Zane Hambly, 2026-08-11)

- an op outside the subset stops the lowering and names itself. Skipping it
would leave a function that compiles and computes something else
(Zane Hambly, 2026-08-11)

- five fixes to the vendored reader, all worth upstreaming, and four of them
are `func.func` being unfinished where `tt.func` is not: `parser_init`
renamed off Booth's own, `parser_error`'s `exit(1)` replaced by a
`mlir_parse_fail()` the linker supplies, `func.func` binding its arguments
before parsing the body rather than after, `func.func` accepting the
`attributes` clause where MLIR actually writes it, and `arith.xori`,
`shli` and `shrsi` added to `op_string_to_type`, which the printer could
already write but the parser could not read back
(Zane Hambly, 2026-08-11)

- `ml_parse` resets the reader's process-wide type interning, which upstream
assumes one context per process. Without it a closed context left the next
parse in freed memory (Zane Hambly, 2026-08-11)

- the Triton lowering records pool overflow through `bir_pfull`, which the C99
one already did and it never has. It answered a full block pool with index 0,
a live block, so `bir_pchk` could not see a Triton arena exhaustion at all
(Zane Hambly, 2026-08-11)

- Triton blocks are named. String offset 0 is a live string, so a nameless
block printed as whatever went into the table first, and all four blocks of
a loop kernel were labelled with the kernel's own name
(Zane Hambly, 2026-08-11)

### Architecture

- BIR arena writers record a `pool_full` bit rather than returning index 0,
which is a live entry and not a sentinel. A full pool emitted wrong
immediates under exit 0; `bir_pchk` now refuses (Zane Hambly, 2026-08-11)

- #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
Expand All @@ -18,6 +62,19 @@ Booth — Changelog

### CI and tests

- `make mutate` bends one line of Booth at a time in a scratch copy and checks
the suite notices, from a table in `tests/mutants.tbl`. Ported from Kahu's
(Zane Hambly, 2026-08-12)

- six tests that were not testing what they looked like they were. The `cfd`
family could not tell a reversed subtraction from an addition, because both
fixtures folded to 7. The only SOP2 encoding test used `s_add_u32`, whose
opcode is 0x00, so the opcode field could sit anywhere in the word. The GFX9
SMEM branch had no test at all, on a shipping target. `rss` accepted a clean
rejection everywhere, so an allocator that rejected everything would have
passed. Nothing checked a memory wait waits on the memory counter, or that a
plain `func.func` is not a kernel (Zane Hambly, 2026-08-12)

- #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
Expand Down
69 changes: 63 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,52 @@ SOURCES = src/main.c src/kauri_impl.c \
src/nvidia/isel.c src/nvidia/emit.c src/nvidia/nv_be.c \
src/metal/emit.c src/metal/metal_be.c \
src/intel/emit.c src/intel/intel_be.c \
src/triton/lex.c src/triton/parse.c src/triton/sema.c src/triton/lower.c
OBJECTS = $(SOURCES:%.c=$(OBJDIR)/%.o)
src/triton/lex.c src/triton/parse.c src/triton/sema.c src/triton/lower.c \
src/mlir/mlir_fe.c src/mlir/lower.c

# Certik's pure-C MLIR reader, vendored under src/mlir/vendor. It carries his
# corec base library and a syscall shim per host, so only one of the three
# platform files is ever built.
#
# c2x rather than c99 because corec's format.h dispatches on _Generic and
# needs __VA_OPT__, and the ~100 format() call sites through it are not worth
# rewriting. -Wno-switch-enum because these switch over a 140-value op enum
# with a default label and upstream keeps adding ops. Nothing else in the
# warning set is relaxed. PLATFORM_SKIP_ENTRY leaves main to Booth,
# COREC_STDLIB_PROVIDES_MEM stops corec defining memcpy and memset when a real
# libc is already doing it.
VDIR = src/mlir/vendor
VPLAT = platform_windows.c
ifeq ($(UNAME_S),Linux)
VPLAT = platform_linux.c
endif
ifeq ($(UNAME_S),Darwin)
VPLAT = platform_macos.c
endif
VSOURCES = $(VDIR)/tokenizer.c $(VDIR)/mlir_parser.c $(VDIR)/op_parsers.c \
$(VDIR)/mlir_api_impl.c $(VDIR)/mlir_op_names.c \
$(VDIR)/mlir_classic_printer.c $(VDIR)/mlir_lift_cf_to_scf.c \
$(VDIR)/base/io.c $(VDIR)/base/buddy.c $(VDIR)/base/arena.c \
$(VDIR)/base/scratch.c $(VDIR)/base/format.c $(VDIR)/base/math.c \
$(VDIR)/base/string.c $(VDIR)/base/strbuf.c $(VDIR)/base/mem.c \
$(VDIR)/base/numconv.c $(VDIR)/base/assert.c $(VDIR)/base/exit.c \
$(VDIR)/platform/$(VPLAT)
# Simply expanded, so the target-specific assignment below is a plain string
# rather than something that re-expands CFLAGS into itself.
VCFLAGS := $(subst -std=c99,-std=c2x,$(CFLAGS)) -Wno-switch-enum \
-DPLATFORM_SKIP_ENTRY -DCOREC_STDLIB_PROVIDES_MEM -I$(VDIR)

OBJECTS = $(SOURCES:%.c=$(OBJDIR)/%.o) $(VSOURCES:%.c=$(OBJDIR)/%.o)

# Everything under src/mlir compiles on VCFLAGS, vendored or not. mlir_fe.c and
# lower.c are ours but they speak corec types, so they want the same flags.
# A target-specific variable rather than a pattern rule, because two patterns
# match these objects and make 3.81, which is what macOS ships, does not
# resolve that the way make 4 does. It took the generic rule and the build lost
# its include path.
MLOBJECTS = $(VSOURCES:%.c=$(OBJDIR)/%.o) \
$(OBJDIR)/src/mlir/mlir_fe.o $(OBJDIR)/src/mlir/lower.o
$(MLOBJECTS): CFLAGS := $(VCFLAGS)
TARGET = kath

all: $(TARGET) $(ALT_RT)
Expand All @@ -88,7 +132,7 @@ $(OBJDIR)/%.o: %.c
# ---- Test Suite ----
TCFLAGS = -std=c99 -MMD -MP -D_POSIX_C_SOURCE=200809L -Wall -Wextra -O0 -g \
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/backend -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime \
-Iruntime $(COVFLAGS)
-Isrc/mlir -Iruntime $(COVFLAGS)
TSRC = tests/tmain.c tests/tsmoke.c tests/tcomp.c tests/tenc.c \
tests/ttabs.c tests/ttypes.c tests/terrs.c tests/tphase.c \
tests/tdce.c \
Expand All @@ -112,7 +156,8 @@ TSRC = tests/tmain.c tests/tsmoke.c tests/tcomp.c tests/tenc.c \
tests/tsysprint.c \
tests/tbackend.c \
tests/tordr.c \
tests/trpi.c
tests/trpi.c \
tests/tmlir.c

TOBJS = $(TSRC:%.c=$(OBJDIR)/%.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 \
Expand All @@ -130,13 +175,22 @@ COBJS = $(OBJDIR)/src/kauri_impl.o $(OBJDIR)/src/ir/bir.o $(OBJDIR)/src/ir/bir
$(OBJDIR)/src/cpu/cpu_emit.o $(OBJDIR)/src/cpu/cpu_elf.o \
$(OBJDIR)/src/cpu/rv64_emit.o $(OBJDIR)/src/cpu/rv64_elf.o \
$(OBJDIR)/src/tensix/isel.o $(OBJDIR)/src/tensix/coarsen.o $(OBJDIR)/src/tensix/datamov.o \
$(OBJDIR)/src/metal/emit.o $(OBJDIR)/src/intel/emit.o
$(OBJDIR)/src/metal/emit.o $(OBJDIR)/src/intel/emit.o \
$(OBJDIR)/src/mlir/mlir_fe.o $(OBJDIR)/src/mlir/lower.o $(VSOURCES:%.c=$(OBJDIR)/%.o)

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.
# Bends one line at a time in a scratch copy and checks the suite notices.
# Never touches the working tree. See tests/mutants.tbl.
mutate: $(TARGET) trunner
sh tests/mutate.sh

mutate-discover: $(TARGET) trunner
sh tests/mutate.sh --discover

repro: $(TARGET)
tests/reprocheck.sh

Expand Down Expand Up @@ -230,8 +284,11 @@ coverage:
-./trunner --all
@command -v gcovr >/dev/null 2>&1 || { echo "gcovr not found. pip install gcovr"; exit 1; }
@mkdir -p coverage-html
@# gcovr snuffles through the object dir like a skaven after warp tokens, so vendored data has to be gone rather than filtered.
find $(COVDIR)/src/mlir \( -name '*.gcda' -o -name '*.gcno' \) -delete 2>/dev/null || true
gcovr --root . --object-directory $(COVDIR) \
--filter 'src/' --filter 'runtime/' \
--exclude 'src/mlir/vendor/' \
--exclude-unreachable-branches \
--print-summary --txt coverage.txt --html-details coverage-html/index.html
rm -f $(TARGET) $(TARGET).exe trunner trunner.exe
Expand All @@ -246,4 +303,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 repro install uninstall coverage
.PHONY: all clean test repro mutate mutate-discover install uninstall coverage
77 changes: 69 additions & 8 deletions src/ir/bir.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "bir.h"
#include <string.h>
#include <stdio.h>

/* ---- Name Tables ---- */

Expand Down Expand Up @@ -187,6 +188,45 @@ const char *bir_order_name(int ord)
return "???";
}

/* ---- Pool overflow ---- */

/* Fixed order, so the report reads the same whichever pool filled first. */
static const struct { uint32_t bit; const char *name; uint32_t cap; }
pool_tab[] = {
{ BIR_P_TYPES, "type", BIR_MAX_TYPES },
{ BIR_P_TFIELDS, "type field", BIR_MAX_TYPE_FIELDS },
{ BIR_P_STRINGS, "string table", BIR_MAX_STRINGS },
{ BIR_P_CONSTS, "constant", BIR_MAX_CONSTS },
{ BIR_P_INSTS, "instruction", BIR_MAX_INSTS },
{ BIR_P_BLOCKS, "block", BIR_MAX_BLOCKS },
{ BIR_P_FUNCS, "function", BIR_MAX_FUNCS },
{ BIR_P_GLOBALS, "global", BIR_MAX_GLOBALS },
{ BIR_P_EXTRAOPS, "extra operand", BIR_MAX_EXTRA_OPS },
{ BIR_P_PHIS, "mem2reg phi", 0u },
};

void bir_pfull(bir_module_t *M, uint32_t bit)
{
if (M != NULL) M->pool_full |= bit;
}

int bir_pchk(const bir_module_t *M, const char *phase)
{
if (M == NULL || M->pool_full == 0u) return BC_OK;

for (uint32_t i = 0; i < sizeof(pool_tab) / sizeof(pool_tab[0]); i++) {
if (!(M->pool_full & pool_tab[i].bit)) continue;
if (pool_tab[i].cap != 0u)
fprintf(stderr, "E120: BIR %s pool exhausted during %s "
"(capacity %u). Raise the matching BIR_MAX_* and "
"rebuild.\n", pool_tab[i].name, phase, pool_tab[i].cap);
else
fprintf(stderr, "E120: BIR %s pool exhausted during %s.\n",
pool_tab[i].name, phase);
}
return BC_ERR_OVERFLOW;
}

/* ---- Module Init ---- */

void bir_module_init(bir_module_t *M)
Expand Down Expand Up @@ -219,8 +259,10 @@ static uint32_t intern_type(bir_module_t *M, const bir_type_t *t)
if (type_eq_simple(&M->types[i], t))
return i;
}
if (M->num_types >= BIR_MAX_TYPES)
if (M->num_types >= BIR_MAX_TYPES) {
bir_pfull(M, BIR_P_TYPES);
return 0;
}
uint32_t idx = M->num_types++;
M->types[idx] = *t;
return idx;
Expand All @@ -244,10 +286,14 @@ static uint32_t intern_compound(bir_module_t *M, uint8_t kind,
}
if (match) return i;
}
if (M->num_type_fields + (uint32_t)nfields > BIR_MAX_TYPE_FIELDS)
if (M->num_type_fields + (uint32_t)nfields > BIR_MAX_TYPE_FIELDS) {
bir_pfull(M, BIR_P_TFIELDS);
return 0;
if (M->num_types >= BIR_MAX_TYPES)
}
if (M->num_types >= BIR_MAX_TYPES) {
bir_pfull(M, BIR_P_TYPES);
return 0;
}

uint32_t start = M->num_type_fields;
for (int i = 0; i < nfields; i++)
Expand Down Expand Up @@ -343,8 +389,11 @@ uint32_t bir_type_func(bir_module_t *M, uint32_t ret,

uint32_t bir_add_string(bir_module_t *M, const char *s, uint32_t len)
{
if (M->string_len + len + 1 > BIR_MAX_STRINGS)
/* Offset 0 is a live string, not a sentinel. */
if (M->string_len + len + 1 > BIR_MAX_STRINGS) {
bir_pfull(M, BIR_P_STRINGS);
return 0;
}
uint32_t offset = M->string_len;
memcpy(&M->strings[offset], s, len);
M->strings[offset + len] = '\0';
Expand All @@ -354,6 +403,9 @@ uint32_t bir_add_string(bir_module_t *M, const char *s, uint32_t len)

/* ---- Constants ---- */

/* Nothing is pinned at const 0 the way void is at type 0, so a refusal
here is indistinguishable from a real index. Hence the bit. */

uint32_t bir_const_int(bir_module_t *M, uint32_t type, int64_t val)
{
uint32_t guard = M->num_consts;
Expand All @@ -363,8 +415,10 @@ uint32_t bir_const_int(bir_module_t *M, uint32_t type, int64_t val)
&& M->consts[i].d.ival == val)
return i;
}
if (M->num_consts >= BIR_MAX_CONSTS)
if (M->num_consts >= BIR_MAX_CONSTS) {
bir_pfull(M, BIR_P_CONSTS);
return 0;
}
uint32_t idx = M->num_consts++;
M->consts[idx].kind = BIR_CONST_INT;
memset(M->consts[idx].pad, 0, sizeof(M->consts[idx].pad));
Expand All @@ -384,7 +438,10 @@ uint32_t bir_const_int(bir_module_t *M, uint32_t type, int64_t val)
uint32_t bir_const_bytes(bir_module_t *M, uint32_t type,
uint32_t off, uint32_t len)
{
if (M->num_consts >= BIR_MAX_CONSTS) return 0;
if (M->num_consts >= BIR_MAX_CONSTS) {
bir_pfull(M, BIR_P_CONSTS);
return 0;
}
uint32_t idx = M->num_consts++;
M->consts[idx].kind = BIR_CONST_BYTES;
memset(M->consts[idx].pad, 0, sizeof(M->consts[idx].pad));
Expand Down Expand Up @@ -414,8 +471,10 @@ uint32_t bir_const_float(bir_module_t *M, uint32_t type, double val)
&& M->consts[i].d.fval == val)
return i;
}
if (M->num_consts >= BIR_MAX_CONSTS)
if (M->num_consts >= BIR_MAX_CONSTS) {
bir_pfull(M, BIR_P_CONSTS);
return 0;
}
uint32_t idx = M->num_consts++;
M->consts[idx].kind = BIR_CONST_FLOAT;
memset(M->consts[idx].pad, 0, sizeof(M->consts[idx].pad));
Expand All @@ -431,8 +490,10 @@ uint32_t bir_const_null(bir_module_t *M, uint32_t type)
if (M->consts[i].kind == BIR_CONST_NULL && M->consts[i].type == type)
return i;
}
if (M->num_consts >= BIR_MAX_CONSTS)
if (M->num_consts >= BIR_MAX_CONSTS) {
bir_pfull(M, BIR_P_CONSTS);
return 0;
}
uint32_t idx = M->num_consts++;
M->consts[idx].kind = BIR_CONST_NULL;
memset(M->consts[idx].pad, 0, sizeof(M->consts[idx].pad));
Expand Down
22 changes: 22 additions & 0 deletions src/ir/bir.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ typedef struct {
uint8_t is_const;
} bir_global_t; /* 16 bytes */

/* ---- Pool overflow ---- */

/* One bit per arena, set when a writer refuses for want of room. Sticky and
OR-ed, so the mask doesn't depend on which pool filled first. */
#define BIR_P_TYPES 0x001u
#define BIR_P_TFIELDS 0x002u
#define BIR_P_STRINGS 0x004u
#define BIR_P_CONSTS 0x008u
#define BIR_P_INSTS 0x010u
#define BIR_P_BLOCKS 0x020u
#define BIR_P_FUNCS 0x040u
#define BIR_P_GLOBALS 0x080u
#define BIR_P_EXTRAOPS 0x100u
#define BIR_P_PHIS 0x200u

/* ---- Module ---- */

/* The whole program in one struct. No malloc. Deterministic layout. */
Expand Down Expand Up @@ -296,12 +311,19 @@ typedef struct {

char strings[BIR_MAX_STRINGS];
uint32_t string_len;

uint32_t pool_full; /* BIR_P_* bits; zeroed by bir_module_init */
} bir_module_t;

/* ---- API ---- */

void bir_module_init(bir_module_t *M);

/* bir_pfull records a refusal; bir_pchk reports them and answers
BC_ERR_OVERFLOW if the module is unsafe to emit. */
void bir_pfull(bir_module_t *M, uint32_t bit);
int bir_pchk(const bir_module_t *M, const char *phase);

/* Type interning — returns index of existing or newly created type */
uint32_t bir_type_void(bir_module_t *M);
uint32_t bir_type_int(bir_module_t *M, int width_bits);
Expand Down
Loading
Loading