Status: living document. Baseline commit:
0cfa958(main). Everything under "Where we are today" was verified against the source at that commit; everything else is a plan and is negotiable.
Brainrot started as a meme keyword substitution over C. It has since grown a real
pipeline (Flex → Bison → AST → semantic analyzer → tree-walking interpreter), a
dlopen-based standard library (libstdrot.so), pointers, and structs (gang).
This roadmap describes how it becomes a cursed-but-genuinely-useful language: one that can call native C libraries, run a game loop, spawn threads, hold a hashmap, serve HTTP, and roll cryptographically honest dice — without any of those being one-off hacks bolted onto the interpreter.
The organizing idea: we do not port libraries into Brainrot. We build a good enough native ABI that every C library — raylib, SQLite, libcurl — becomes just another binding, most of it generated.
- Where we are today
- Guiding principles
- Phase 1 — The keystone: native calls as expressions
- Phase 2 — A typed native ABI
- Phase 3 — C-compatible aggregates
- Phase 4 — Native modules and
#cooked - Phase 5 — Bindings and the first cursed game
- Phase 6 — Threads (
yeet) - Phase 7 — Hashmaps (
grindset) - Phase 8 — Sockets and web servers
- Phase 9 — Unit testing (
sussybaka) - Phase 10 — File I/O
- Phase 11 — Cryptographically safe
gamba() - Milestones
- Appendix A — Reserved keywords
- Appendix B — Open questions
We already built the beginning of an FFI system, somewhat by accident.
libstdrot.so is loaded with dlopen/dlsym, native functions self-register
through a linker section (STDROT_EXPORT), and every call is dispatched through
one generic signature:
typedef StdrotValue (*StdrotFn)(StdrotValue *args, int argc);That is a real foreign-function substrate in embryo. What is missing is everything that makes it typed and composable.
| # | Limitation | Evidence |
|---|---|---|
| L1 | Native calls are statement-only. In expression position they route to execute_function_call(), which only knows user-defined functions, and fail with Undefined function. |
ast.c:3588, ast.c:2414 |
| L2 | Return values are discarded. The only way a builtin produces a value is a write-back hack: if argument #1 is an identifier, the result is stored into that variable. | stdrot.c:319 |
| L3 | The semantic analyzer types every builtin as NONE — it knows a name exists and nothing more. |
semantic_analyzer.c:352 |
| L4 | The registry carries no signature: StdrotEntry is { name, fn }. |
stdrot/stdrot_api.h:86 |
| L5 | StdrotValue has no pointer, struct, or handle representation — only int/float/double/short/bool/char/String/void. |
stdrot/stdrot_api.h:49 |
| L6 | compute_struct_layout() packs fields sequentially with no alignment or padding, so gang layouts do not match C. |
ast.c:3948 |
| L7 | Struct member access only resolves when the base is a plain identifier; a.b.c is rejected outright. |
ast.c:346 |
| L8 | Functions can take/return a struct by value as a plain struct variable, a by-value member-access sub-expression (take(b.corner), bussin b.corner;), or a struct-returning call result (take(make_point()), bussin make_point();) of the exact matching type, and can return a pointer to a struct (gang Point *f()). Resolved. |
ast.c:6606 (enter_function_scope) |
| L9 | StructField keeps VarType + pointer_level + offset only — no struct type name, no arrays, no modifiers, so nested-struct and fixed-array fields are unrepresentable. |
ast.h:82 |
| L10 | Exactly one native library is ever loaded, hardcoded as libstdrot.so. |
Makefile:52 |
Reproducing L1/L2 takes four lines:
skibidi main {
cap ok = bet(W); /* Error: Undefined function */
bussin 0;
}bet is registered and does return a value; it simply cannot be used as one.
- Brainrot has pointers, pointer levels, address-of and dereference.
gangstruct definitions, instances, and single-level member access work.- The registry is discovery-based — the host hardcodes no function names.
- Arena allocation, a hashmap (
lib/hm.c), andStringare already in-tree. - The build is
-Werrorwith ASan+UBSan, andmake valgrindcovers every test case. That's an unusually strong safety net for the work below, which is almost entirely memory-layout work.
- Don't port, bind. raylib is already a C library. Our job is an ABI.
- Generate, don't hand-write. Hundreds of wrappers written by hand is how this project dies. Generated C adapters give compile-time-checked calls.
- No libffi. Generated C means the C compiler does the ABI work for us. We are not writing a dynamic call machine.
- Layout correctness is testable — so test it. Every ABI claim gets a
_Static_assertor a runtimeoffsetofcheck against the real C header. - Meme integrity is a hard requirement, not a joke. New keywords must be
funny and mnemonic.
yeetfor spawning a thread is both. - Nothing merges without
make testandmake valgrind. Sanitizer output is a build failure, not a suggestion.
Status: merged (#217) · Priority: P0 · Blocks: literally everything else
This is the single highest-leverage change in the entire roadmap. Until a native call is an ordinary expression, none of the rest is worth starting.
NODE_FUNC_CALL must not care whether the callee is Brainrot or native.
goon (WindowShouldClose() == L) { ... } /* today: parse-time OK, runtime error */
rizz n = strlen_native(s); /* today: impossible */- Add
StdrotValue execute_native_call(const String name, ArgumentList *args)instdrot.cthat returns the result instead of swallowing it. - In
handle_function_call()(ast.c:2414), check the native registry before failing withUndefined function, and marshal the returnedStdrotValueintocurrent_return_value. - Delete the argument write-back hack (stdrot.c:319) once
slorp's callers are migrated torizz x = slorp(...)form. - Keep statement-position calls working — a discarded return value is fine.
Removing the write-back changes the meaning of existing slorp(x); programs.
That is a breaking change to a public surface and needs sign-off before it
lands. Suggested path: support both forms for one release, warn on the old one.
test_cases/native_call_expr.brainrotcovering a native call in an initializer, a condition, an argument, and a binary operand.bet(W)usable as acapvalue.make testandmake valgrindgreen.
Status: merged (#223) · Priority: P0 · Depends on: Phase 1
Right now the semantic analyzer cannot check a single builtin argument. Making native exports self-describing fixes that and unlocks generated bindings.
typedef enum {
STDROT_VOID,
STDROT_I8, STDROT_U8,
STDROT_I16, STDROT_U16,
STDROT_I32, STDROT_U32,
STDROT_I64, STDROT_U64,
STDROT_F32, STDROT_F64,
STDROT_BOOL,
STDROT_STRING, /* Brainrot String */
STDROT_CSTRING, /* NUL-terminated */
STDROT_PTR,
STDROT_STRUCT,
STDROT_HANDLE,
} StdrotType;Not every width is needed on day one. STDROT_PTR and honest return values are
mandatory; the rest can land incrementally.
typedef struct {
StdrotType type;
const char *type_name; /* for STDROT_STRUCT / STDROT_HANDLE */
int pointer_level;
} StdrotParam;
typedef struct {
const char *name;
StdrotParam return_type;
const StdrotParam *params;
int param_count;
StdrotFn fn;
} StdrotEntry;Then the analyzer knows WindowShouldClose() -> bool, GetFrameTime() -> float,
LoadTexture(const char *) -> Texture2D, and can type-check native calls exactly
the way it type-checks Brainrot-defined functions — replacing the return NONE
at semantic_analyzer.c:352.
Brainrot uses String { char *data; size_t len; }; C libraries want
const char *. Defined once, in the ABI layer (coerce_arg_to_param(),
stdrot.c), rather than scattering malloc(len+1)/memcpy/'\0' through every
wrapper. Ownership rule, as actually implemented: STDROT_CSTRING arguments
are strictly non-escaping -- adapter-owned scratch, freed immediately
after the call (see STDROT_CSTRING's own comment, stdrot_api.h). Return
values get the mirror treatment: a native's STDROT_STRING return is
materialized (deep-copied) into independent memory before any of that call's
own argument scratch is released, so an identity-style native (T -> T,
STDROT_EXPORT_SIG_IDENTITY) can safely hand back one of its own string
arguments unchanged without triggering a use-after-free once the adapter's
cleanup runs.
Deliberately out of scope for Phase 2: an escaping-string annotation (a
native's C implementation retaining a const char * past its own return --
e.g. a C API like set_global_name(const char *) that keeps the pointer).
Nothing in StdrotParam currently expresses that a parameter escapes, and
none of Phase 2's natives need it. A future phase needs to either add an
explicit escapes flag to StdrotParam with a defined ownership-transfer
mechanism, or otherwise formally support escaping strings, before any native
requiring one is written -- writing one against the current ABI is a
guaranteed use-after-free, not an oversight to work around silently.
STDROT_EXPORT_SIG(...)macro alongside the legacySTDROT_EXPORT.- Existing builtins migrated to signatures.
- Semantic errors for arity and type mismatches on native calls, with tests.
Status: complete (#206) · Priority: P0 · Depends on: Phase 2
All five sub-items landed; the sections below are kept as the design record.
- 3a layout (alignment + trailing padding,
_Static_assert/offsetofABI check) — #242. - 3b unified
TypeDescriptorin use across all five type carriers (StructField, Parameter, Variable, Function return, ReturnValue) — #258, #259, #260, #261. - 3c
gangas a first-class type: struct-typed fields/params/returns, struct arguments and returns including member-access/call-result sub-expressions and pointer-to-struct returns (#193) — #253, #254, #255. - 3d address-based recursive member access, incl. as an assignment
target and through pointer fields (#196/#197) — #248, #252. (Member
access
.precedence bug, surfaced by this work, fixed in #257.) - 3e remaining C field types: unsigned/fixed-width scalars, pointer
fields, nested struct fields, fixed arrays in structs (incl.
lit-alias element types with correct stride), struct aliases — #247, #256. "Fixed arrays in structs" meant SCALAR element types only until #311; a struct/union-typed array field (gang Pool { gang Entity es[8]; };) was a parse error, and is now supported.
Struct assignment is value-copy (C semantics), decided and tested. Arrays of by-value structs as standalone declarations, and arrays as by-value function params/returns, remain out of scope here (the latter is #194).
This is the big one. raylib is struct city: Vector2, Vector3, Color,
Rectangle, Texture2D, Image, Camera2D, Camera3D, Matrix, and further
down Mesh, Shader, Model, Sound, Music.
ast.c:3948 packs fields with no padding. Any gang handed to C
today would be silently misinterpreted. Required:
offset = align_up(offset, alignof(field));
field->offset = offset;
offset += sizeof(field);
struct_size = align_up(offset, max_field_alignment);And prove it, don't assume it:
_Static_assert(sizeof(BrainrotVector2) == sizeof(Vector2), "ABI drift");
_Static_assert(offsetof(BrainrotVector2, y) == offsetof(Vector2, y), "ABI drift");VarType + pointer_level + modifiers is at the end of its rope. Replace it with:
typedef struct TypeDesc {
TypeKind kind;
bool is_unsigned;
int pointer_level;
StructDef *struct_def; /* kind == TYPE_STRUCT */
int array_rank;
size_t dimensions[MAX_DIMENSIONS];
} TypeDesc;This is invasive — it touches the AST, the analyzer, and the interpreter — but
every remaining item on this roadmap gets cheaper afterwards, and the alternative
is stretching VarType until it snaps.
Struct fields, parameters, and return types currently come from the primitive
type grammar, so a struct-typed field cannot even be spelled. Target:
gang Vector2 { chad x; chad y; };
gang Camera2D {
gang Vector2 offset;
gang Vector2 target;
chad rotation;
chad zoom;
};
gang Vector2 GetMousePosition();
skibidi DrawCircleV(gang Vector2 center, chad radius, gang Color color);Requires struct-typed fields/params/returns, struct arguments and returns (removing ast.c:2458), and struct assignment semantics (decision needed: value copy, matching C).
Rewrite member access as base lvalue address + field offset, recursively,
instead of special-casing NODE_IDENTIFIER at ast.c:346. Once
access is address-based, camera.target.x, model.transform.m0, and
renderTexture.texture.width all fall out for free — including as assignment
targets.
For full raylib coverage: unsigned and fixed-width scalars, pointer fields,
nested struct fields, fixed arrays inside structs (char name[32],
float params[4]), struct aliases, and arrays/pointers to structs.
gyatt (enum), lit (typedef), and chungus (union) are not blockers — a
generator can emit CAMERA_PERSPECTIVE, KEY_SPACE, and MOUSE_BUTTON_LEFT as
ordinary Brainrot integer constants. They stay on the wishlist.
Status: mechanism complete · Priority: P1 · Depends on: Phase 2
Today exactly one .so is loaded, by hardcoded name. Generalize to a module
directory, each exporting a discovery entrypoint:
stdrot/ rayrot/ brainsql/ braincurl/
StdrotAPI brainrot_module_init_v3(void);Then #cooked <raylib>, currently listed as unimplemented in the README, means:
locate module → dlopen → fetch metadata → register types, constants, and
functions. That is a far better fate for the directive than textual inclusion.
Landed: the full mechanism. The angle-bracket directive and the module
search path resolve #cooked <name> to either a .brainrot prelude or a
native .so, checking the former before the latter within each directory.
Search order: $BRAINROT_PATH, then exactly one of {the install module
directory, stdrot/ next to the running executable} — never both, decided
by whether the running executable (resolved via
/proc/self/exe/_NSGetExecutablePath, not argv[0]) is itself the
installed binary (Appendix B Q11's resolution has the full reasoning). A
resolved .so is dlopen'd and must export StdrotAPI brainrot_module_init_v3(void) (stdrot_load_module(), stdrot.c) — the exact
signature this phase originally specified, given a collision-free exported
name of its own (see stdrot/registry.c's own comment: a naive same-named
wrapper calling stdrot_get_api_v3() from inside a module is silently
interposed by the always-loaded core library's own copy of that symbol).
Its functions are registered alongside the core library's and any other
already-cooked module's, with a load-time error on any name collision
between them. The first real native module built on this mechanism is
rayrot (raylib), shipped by Phase 5 Road A (#208) — this phase delivered
the loader, not a binding, which was never its DoD. Types/constants
registration is deferred past
this phase entirely: StdrotAPI only carries a function table today, and
nothing in-tree needs more than that yet — see
issue #207.
Status: Road A shipped (#208) · Road B shipped (#208) · Priority: P1
There are two roads here and we should walk both, in order.
Status: shipped (#208). Delivered as a hand-written raylib native module,
rayrot/raylib.so, loaded with #cooked <raylib> and built by the optional
make rayrot target — not linked into libstdrot.so as this section
originally proposed, because Phase 4's native-module mechanism (which landed
after this was written) is the cleaner home and keeps raylib out of the core
library's build. raylib stays an optional dependency: make / make test /
make valgrind never build the module and don't need raylib installed. Ships
~20 primitive wrappers plus examples/raylib/ohio_engine.brainrot; see
docs/rayrot.md. The original design sketch follows.
Hand-write ~20 wrappers over primitives only. Textures become integer handles;
C owns the Texture2D textures[] array and Brainrot holds an ID.
skibidi main {
rl_init_window(1280, 720, "Ohio Engine");
goon (rl_window_should_close() == L) {
rl_begin_drawing();
rl_clear_background(20, 20, 20, 255);
rl_draw_circle(640, 360, 100.0, 255, 0, 255, 255);
rl_draw_text("ABSOLUTE CINEMA", 500, 500, 32, 255, 255, 255, 255);
rl_end_drawing();
}
rl_close_window();
}The joke language runs a game loop. Ships as
examples/raylib/ohio_engine.brainrot.
raylib ships tools/rlparser/output/raylib_api.json, a machine-readable
description of its entire API. Point a generator at it:
raylib_api.json → rayrot-gen → { C adapters, native descriptors,
Brainrot constants/types, docs, ABI tests }
static StdrotValue br_LoadTexture(StdrotValue *args, int argc)
{
Texture2D tex = LoadTexture(to_cstr(args[0]));
return stdrot_struct("Texture2D", &tex, sizeof(tex));
}Generated C is compile-time correct and vastly easier to reason about than a dynamic call machine. Once this works, raylib is merely the first client: SDL, SQLite, libcurl, OpenSSL, PortAudio, Lua, FFmpeg, and libgit2 are the same problem.
Delivered (#208). In two parts.
The ABI. STDROT_STRUCT (stdrot/stdrot_api.h), ABI v3 — the by-value
aggregate the sketch above assumes. A parameter declared
{STDROT_STRUCT, "Vector2", 0} receives the gang's C-ABI byte image,
tag-checked both statically and at the runtime boundary, copied so the call is
genuinely by value. Argument direction only, and the sketch above is
deliberately still not implementable as written: its
stdrot_struct("Texture2D", &tex, sizeof(tex)) returns the address of a local
that dies with the call. Struct returns stay rejected until Appendix B Q6
(ownership of native resources) has an answer.
The generator. rayrot/rayrot_gen.py, reading a vendored
rayrot/raylib_api.json pinned by upstream commit SHA, emitting 378 of
617 functions, 16 of 35 struct types, 305 constants — plus an
_Static_assert/offsetof translation unit whose compilation is the ABI
drift check against real raylib headers. make rayrot-gen-sources
(generate; raylib not required) and make rayrot-gen (also compile and
verify) are opt-in and never prerequisites of all/test/valgrind.
Demonstrated by examples/raylib/ohio_engine_gen.brainrot, which runs a real
game loop passing gang Vector2/gang Color/gang Rectangle by value — the
non-handle-hack demo this phase's DoD asked for.
Two design notes worth carrying forward:
- Type/constant registration in
StdrotAPIturned out to be unnecessary, which is why this phase shipped without it despite the DoD implying it. A module name resolves a<name>.brainrotprelude before a<name>.so, and a prelude may itself#cookeda native module — so the generator emits types and constants as ordinary Brainrot source (gang Vector2 { chad x; chad y; },gyatt KeyboardKey { KEY_SPACE = 32, ... }) and only functions go through the C ABI. Phase 4's deferral of that registration (#207) cost nothing. - Struct returns are the dominant coverage cost, quantified: 113 of the 239 skipped functions. Answering Appendix B Q6 is the single highest-value follow-up for this binding, worth more than the other five skip categories combined.
Road A's hand-written rayrot/raylib.so is untouched and still works;
the generated binding is a separate module (#cooked <raylibgen>). Phase 11's gamba() is not that generated OpenSSL binding — it is
a thin, hand-written RAND_bytes wrapper that ships earlier, the same way
Road A ships a cursed game before Road B generates rayrot.
Status: not started · Priority: P1 · Depends on: Phase 1
pthreads, behind slang. yeet launches work into the void; you mogged it back
when you need the result.
| Brainrot | C equivalent | Mnemonic |
|---|---|---|
yeet |
pthread_create |
launch it into the void |
mogged |
pthread_join |
main mogs the worker back into itself |
gatekeep |
mutex type / pthread_mutex_lock |
nobody else gets in |
letcook |
pthread_mutex_unlock |
let the next one cook |
simp |
pthread_cond_wait |
waiting around for someone to notice you |
yass |
pthread_cond_signal |
hypes exactly one waiter awake |
skibidi worker(rizz id) {
gatekeep(counter_lock);
counter = counter + 1;
letcook(counter_lock);
}
skibidi main {
yeet t1 = yeet worker(1);
yeet t2 = yeet worker(2);
mogged t1;
mogged t2;
bussin 0;
}yass wakes one waiter, matching pthread_cond_signal. If we also want
pthread_cond_broadcast, ratioed is the obvious counterpart — everyone piles
on at once.
It is that the interpreter is built around process-global state: current_scope,
current_return_value, the setjmp jump-buffer stack, and the arena allocator
are all shared and none are thread-safe. Options, in increasing order of effort:
- A. Green threads / cooperative scheduling. No data races by construction, no real parallelism. Cheapest, and honestly quite funny.
- B. Real pthreads + a global interpreter lock. Real threads, real blocking I/O overlap, no parallel compute. The Python road.
- C. Thread-local interpreter state. Actual parallelism. Requires making scope, return value, and jump buffers thread-local and the arena either per-thread or locked.
Recommendation: B first, C as a follow-up. A GIL gets yeet shipping and
correct; the interpreter-state refactor is a large independent project and
should not block the feature landing.
Thread-safety of libstdrot.so (notably g_exec_context, which is a global)
must be settled in the same change.
Status: not started · Priority: P1
The grindset: you put things in, you get things out, it never stops. lib/hm.c
already exists internally and is a plausible starting point, though it will need
generic key/value support.
| Operation | Keyword | Mnemonic |
|---|---|---|
| declare | grindset |
it never stops |
| insert | ship |
shipping = pairing two things (key + value) |
| lookup | blorbo |
go get your favourite little guy by name |
| membership | sus |
you suspect it's in there |
| delete | unalive |
self-explanatory |
| size | bagsize |
how big is the bag |
| iterate | flex over a grindset |
reuses the existing loop keyword |
skibidi main {
grindset<rant, rizz> scores;
ship(scores, "ohio", 100);
ship(scores, "skibidi", 42);
edgy (sus(scores, "ohio")) {
yapping("ohio: %d", blorbo(scores, "ohio"));
}
yapping("entries: %d", bagsize(scores));
bussin 0;
}- Generics.
grindset<K, V>implies a type parameter system Brainrot does not have. Alternative: a dynamically-typed map keyed byrantonly, which is far cheaper and covers most real use. Recommendation: start string-keyed and monomorphic, generalize later. - Memory. Maps outlive statements, so they need clear ownership. Arena is a poor fit for a growing structure; likely needs explicit lifetime tied to scope exit.
- This is the first heap-managed aggregate in the language, so it sets precedent for lists/dynamic arrays later. Worth designing carefully.
Status: not started · Priority: P2 · Depends on: Phases 1, 6
Linux socket.h wrapped in slang. Keywords below are proposals and need
sign-off before they touch lang.l.
| Brainrot | C equivalent | Rationale |
|---|---|---|
drip |
socket() |
the conduit data flows through |
brat |
bind() |
confidently claiming your spot |
lurk |
listen() |
lurking for connections |
snatched |
accept() |
you snatch the incoming client |
stan |
connect() |
you stan a remote server |
dm |
send() / write() |
self-explanatory |
peep |
recv() / read() |
peep the message |
ghost |
close() |
ghosting the connection |
skibidi main {
rizz server = drip();
brat(server, 8080);
lurk(server, 128);
goon (W) {
rizz client = snatched(server);
yeet handle(client);
}
}- L1 — raw sockets. Thin wrappers, blocking, one client at a time.
- L2 — concurrency.
yeetper connection (Phase 6) or anepollloop. - L3 — HTTP. A minimal request parser and router in
brainhttp, built on L1/L2, not on new keywords:
skibidi on_request(gang Request req, gang Response res) {
edgy (req.path == "/") {
res.status = 200;
dm(res, "gm");
}
}L3 wants gang structs with string fields, so it lands after Phase 3.
Note that everything here is ordinary C library surface — sockets need almost no
new language features, which is why this is P2 rather than P0. It is mostly a
consumer of the FFI work.
Status: not started · Priority: P1 · Depends on: Phase 1 · Improves with Phases 2 and 4
Brainrot cannot currently test itself. tests/test_brainrot.py runs every
test_cases/*.brainrot from the outside and string-matches stdout against
tests/expected_results.json; the only in-language assertion is bet, which
aborts the process on failure and reports nothing else. sussybaka is the
missing half: an in-language unit testing library where you register tests,
assert things, and get a report instead of a corpse.
The name is the whole design brief. You are suspicious of your code, so you check it.
This phase is P1 rather than P2 because it is not a side quest: the project's own test suite gets rewritten on top of it (§9c), which makes it load-bearing infrastructure rather than a nice-to-have.
Three lettered work items — 9a delivery, 9b mocking, 9c the suite rewrite — with the design they share described in the unlettered sections between them.
Almost every operation here is "a call with arguments", which is exactly the case
Appendix A says should stay an ordinary library function — keyword status would
buy no syntax and cost a reserved identifier forever. The one exception is npc,
the function-reference type that larping needs (§9b); that is a genuine type,
so it is a genuine keyword.
#cooked <sussybaka>
skibidi sus_adds_two_numbers() {
fr(add(2, 2), 4);
nah(add(2, 2), 5);
}
skibidi sus_area_is_roughly_pi() {
lowkey(circle_area(1.0), 3.14159, 0.0001);
}
skibidi sus_divide_by_zero_explodes() {
ragebait(divide(1, 0));
}
skibidi main {
sussybaka("adds two numbers", sus_adds_two_numbers);
sussybaka("area is roughly pi", sus_area_is_roughly_pi);
sussybaka("divide by zero blows", sus_divide_by_zero_explodes);
bussin roasted();
}sussybaka is not globally available the way yapping and bet are. You
opt in by cooking it, which keeps twelve common words out of every program's
namespace and gives the library a place to live that isn't "more builtins".
That requires the angle-bracket form of the directive, which landed as part of
#207 (lib/module_path.c, lang.l's #cooked <name> rule) rather than in this
phase — Phase 4 needed the same search path Phase 9a was going to build, so it
built it first. What remains for this phase specifically is sussybaka itself
existing as a .brainrot prelude the resolver can find, not the resolver:
| Form | Meaning | Status |
|---|---|---|
#cooked "path/to/file.brainrot" |
textual include, resolved relative to the including file | implemented |
#cooked <name> |
resolve name on the module search path |
implemented (#207) |
Search-path resolution tries, in order:
$BRAINROT_PATHentries, if set.- Exactly ONE of the install module directory (
/usr/local/lib/brainrot) or the in-treestdrot/directory next to the running executable — never both; see Appendix B Q11's resolution for why the choice between them is decided by which binary is actually running, not by trying both in a fixed order. The in-tree half is what lets#cooked <sussybaka>resolvestdrot/sussybaka.brainrotfrom a build with no install step, which the test-suite rewrite in §9c depends on.
A resolved name may be either a .brainrot prelude (implemented) or, once
the rest of Phase 4 lands, a native .so module. That is deliberate: one
syntax, one search path, two possible artifact kinds. It also settles what the
roadmap previously left vague — Phase 4's #cooked <raylib> and this phase's
#cooked <sussybaka> are the same mechanism.
How v1 splits:
stdrot/sussybaka.c— the primitives, inlibstdrot.so: the test registry, result recording, comparison helpers, the timer, and report formatting. Not intended to be called directly.stdrot/sussybaka.brainrot— the surface, resolved by#cooked <sussybaka>: the twelve names in the vocabulary table as thinskibidiwrappers, plus suite bookkeeping.
Programs only ever see the prelude. When Phase 4 arrives, prelude and primitives
collapse into a real module and not one line of user code changes — the
#cooked <sussybaka> line already says the right thing.
| Op | Function | Mnemonic |
|---|---|---|
| register a test | sussybaka |
you're suspicious of it, so you check it |
| assert equal | fr |
fr fr — it really is that |
| assert not equal | nah |
it's not that |
| assert condition holds | zesty |
it's giving correct |
| assert condition fails | capping |
it's cap, and you called it |
| assert near (floats) | lowkey |
approximately, roughly |
| expect an abort/crash | ragebait |
you provoked it on purpose |
| skip a test | touchgrass |
not today |
| fail unconditionally | mogg |
you just lost, unconditionally |
| install a mock | larping |
this function is pretending to be another one |
| setup hook | lockin |
lock in before each test |
| teardown hook | logoff |
log off after each test |
| suite summary | roasted |
your year in review |
capping asserts a condition is false and zesty asserts it is true —
the polarity follows the slang, where capping is lying.
None of these collide with a keyword in lang.l, with a registered builtin
(yapping, yappin, baka, bet, chill, ragequit, slorp), or with a
name proposed elsewhere in this roadmap. mogg is deliberately distinct from
Phase 6's mogged, and sussybaka from Phase 7's sus.
bet is untouched. It stays the bare runtime assertion that aborts, and
test_cases/bet.brainrot / bet_fail.brainrot keep passing unchanged.
sussybaka's assertions are a separate family with different semantics: they
record a result and abort only the current test. No compatibility surface
moves in this phase.
./brainrot --sus tests/sus_math.brainrotmain() at lang.y:748 hard-rejects argc != 2, so generalizing
argument parsing is part of this phase.
There is a tension to resolve up front: library-only registration means
something has to execute the sussybaka(...) calls, but test bodies should
carry no harness boilerplate. The resolution is that in --sus mode main is a
manifest, not a driver — the runner executes main purely to collect
registrations, then takes over and drives the tests itself, applying isolation,
timing, output formatting, and the exit code. The test functions themselves stay
plain skibidi functions.
Registration takes a function reference (npc, §9b), so a typo is a semantic
error at analysis time rather than a runtime lookup miss. If npc slips, the
fallback is registration by name string — sussybaka("adds two", "sus_add")
resolved through the existing function table — which works today but moves typos
to run time.
Semantics:
- Parse and analyze as usual.
- Run
mainto collect the manifest. - For each registered test:
lockin→ body →logoff. - Print the summary; exit
0if and only if every test passed.
An assertion failure aborts the current test and the runner continues with the
next one. Mechanically this reuses the existing setjmp machinery: the runner
pushes a jump buffer (ast.h:547) before each test body and a
failing assertion longjmps back into it.
Two hazards, both of which need explicit handling and tests:
- The jump-buffer stack is shared with
bruh/loop handling, so a test that aborts from inside a loop leaves frames behind. The runner must unwind the stack back to its own frame, not merely pop one entry. - Scope and arena state from an aborted test must not leak into the next one. This is the part most likely to produce ASan findings, so it gets tests before it gets features.
Human-readable by default; TAP version 13 behind a flag, so CI and the existing pytest harness can consume results without parsing prose.
sussybaka: sus_math.brainrot
✓ adds two numbers 0.4ms
✗ area is roughly pi 0.3ms
lowkey: expected 3.14159 ± 0.0001, got 3.14158999
at sus_math.brainrot:12
~ divide by zero blows skipped (touchgrass)
roasted: 1 passed, 1 failed, 1 skipped in 1.2ms
- Core. Registration, the assertion family above, per-test isolation, summary, exit code.
lockin/logoffhooks. Run before/after each test in a suite.- Suites. Grouping registered tests under a named suite, so hooks and the summary have a scope smaller than "the file".
- Skip and expected-failure.
touchgrassmarks a test skipped;ragebaitasserts that an expression aborts, which is how you testragequit,baka, and division by zero. - Parameterized tests. Same body, a table of inputs, one result line each. Cheapest form is an array of cases the body indexes; a real table syntax wants Phase 3's aggregates.
- Per-test timing. Monotonic clock around each body, reported in the summary.
- Mocking.
larpingplus first-class functions — see §9b, which is the largest single piece of work in this phase.
Mocking means substituting one function for another, which means a function has to be a value. Brainrot has no such thing today, so this phase introduces one.
npc /* a function reference: hand someone a script and they run it */npc is the mnemonic pair to larping — an NPC runs whatever script it was
given, and larping is how you hand it a different one. Minimum viable feature
set:
npcas a type in declarations and parameters.- A bare function name in value position evaluates to a reference to it.
- Calling through an
npc-typed variable:f(args)wherefis annpc. - Assignment and equality comparison of references.
Not in scope: closures, capture, anonymous functions, returning npc from
native code. Those are a language-design project; this is a pointer to a
FunctionDef.
#cooked <sussybaka>
skibidi rizz fake_roll() {
bussin 4; /* chosen by fair dice roll, guaranteed random */
}
skibidi sus_player_moves_four_spaces() {
larping(roll_dice, fake_roll); /* roll_dice now LARPs as fake_roll */
fr(take_turn(), 4);
} /* restored when the test ends */Mechanism. larping(real, stub) rebinds the callee real resolves to, for
the duration of the current test, and the runner restores it afterwards. Two
constraints that are easy to get wrong:
- Restoration must survive an abort. A failing assertion
longjmps out of the test body, so restoration cannot live at the end of the body. The runner owns a per-test list of installed mocks and unwinds it in the same place it unwinds the jump-buffer stack — the one code path that runs whether the test passed, failed, or aborted. - Signature compatibility.
larpingshould reject a stub whose signature does not match the target. Fully checkable once Phase 2 lands; before that it is an arity check plus a runtime type check at the call.
This is bigger than testing. Function references unlock raylib callbacks,
yeet worker taking a function rather than a syntactic call, socket handlers in
Phase 8, and comparator arguments for any future sort. That is an argument for
promoting npc out of Phase 9 into its own phase — see Appendix B.
The end state is that test_cases/ stops being a set of programs whose
correctness lives in a separate JSON file, and becomes a set of programs that
state their own expectations.
Today:
/* test_cases/add_two_numbers.brainrot */
skibidi main { yapping("%d", 2 + 2); }"add_two_numbers": "4"After:
#cooked <sussybaka>
skibidi sus_addition() { fr(2 + 2, 4); }
skibidi main { sussybaka("addition", sus_addition); bussin roasted(); }The expectation moves next to the code, a failure says which assertion broke instead of diffing two blobs of stdout, and a test file is runnable on its own.
Migration must be incremental — 97 files do not move in one commit.
- Land Phase 9 core and add
test_cases/sussybaka_*.brainrotalongside the existing suite. Nothing migrates yet. - Teach
tests/test_brainrot.pya second mode: a case with noexpected_results.jsonentry is run under--sus --tapand asserted on exit code and TAP output instead of a stdout string. The two styles coexist. - Migrate in batches by area (arrays, structs,
#cooked, string builtins), deleting eachexpected_results.jsonentry as its case converts. - When the JSON is empty, delete it and simplify the harness.
Two things that must keep working throughout:
run_valgrind_tests.shiteratestest_cases/*.brainrotand runs each under valgrind. Migrated tests are still ordinary.brainrotfiles, so this keeps working unchanged — but it needs to pass--sus, and a deliberately failing sussybaka case must stay leak-clean ormake valgrindgoes red for the wrong reason.- Error-path cases (
bet_fail,division_by_zero,cooked_missing,cooked_circular, and friends) assert on stderr and a non-zero exit code. Those do not become sussybaka tests — the process is supposed to die. They stay string-matched, so the JSON harness may never fully disappear, and step 4 above should be treated as aspirational rather than committed.
AGENTS.md's testing requirement ("a .brainrot file in test_cases/, a
corresponding entry in tests/expected_results.json") is written against the
current layout and must be updated in the same PR as step 2.
- Leak assertions. "Assert this block allocates nothing it doesn't free" fits the project's ASan/valgrind-first culture perfectly, but needs allocator instrumentation that doesn't exist yet.
- Closures and anonymous functions. Once
npcexists, someone will wantlarping(roll_dice, skibidi() { bussin 4; }). Deliberately excluded from §9b.
The library surface is buildable today in a degraded, statement-only form. The phase as scoped needs three things it does not have:
- Angle-bracket
#cooked+ a search path (§9a). New, small, owned here. npcfunction references (§9b). New, not small, and arguably its own phase.- Phase 1 — assertions become expressions, so
zesty(fr(a, b))composes and an assertion can be used as a value rather than only as a statement.
It gets better with Phase 2 (typed signatures let the analyzer reject
fr("string", 42) at analysis time, and make larping's signature check exact)
and with Phase 4, which upgrades the prelude to a real module without any
program changing.
Nothing in Phases 1–8 depends on this, and it does not block them.
#cooked <sussybaka>resolves through the module search path, with tests for a missing module, a search-path hit, and the in-tree fallback.stdrot/sussybaka.cprimitives registered throughSTDROT_EXPORT, withstdrot/sussybaka.brainrotas the cooked surface.npcin the grammar, the analyzer, and the interpreter;larpinginstalling and restoring mocks, including on the abort path.--susrunner mode with human and TAP output.test_cases/sussybaka_pass.brainrot,sussybaka_fail.brainrot,sussybaka_skip.brainrot,sussybaka_larping.brainrot, andsussybaka_abort_isolation.brainrot(a test that fails inside a loop with a mock installed, proving both that the next test still runs and that the mock was restored), each with an entry intests/expected_results.json.- One migrated batch of the existing suite, proving the §9c path end to end.
docs/reference page for the assertion vocabulary; README keyword table updated fornpc.make testandmake valgrindgreen — including under a deliberately failing suite, since the abort path is the one most likely to leak.
Status: not started · Priority: P2 · Depends on: Phase 1
stdio.h, rebranded. Like sussybaka, this is a library, not keywords —
every operation here is "a call with arguments", so it belongs in stdrot
alongside yapping, slorp, and chill, not in lang.l. Unlike sussybaka
it is not gated behind #cooked — file I/O is common enough to stay globally
available, the same way slorp is today. stdrot/file.c self-registers the
functions below the same way baka.c and yapping.c do.
Type: FILE * → SAUCE *
| C | Brainrot | Why |
|---|---|---|
fopen |
crackopen |
you crack open the file |
fclose |
peaceout |
perfect opposite lifecycle |
fread |
doomscroll |
consuming data endlessly |
fwrite |
shitpost |
putting content into the file |
fgets |
skim |
silently reads a line |
fputs |
yapto |
yapping into a file |
fseek |
zoink |
literally moving through the file |
ftell |
whereami |
cursor position |
rewind |
throwback |
self-explanatory |
feof |
itsjoever |
this one is mandatory |
ferror |
bricked |
file operation got bricked |
fflush |
bustcache |
flush buffered output |
yapto is deliberately distinct from raw shitpost: shitpost is
fwrite-shaped (size/count, binary-safe), yapto is fprintf-shaped (a
format string). That mirrors the existing split between yapping (stdout,
formatted, newline) and yappin (stdout, formatted, no newline). None of
these twelve names collide with lockin/logoff (Phase 9's sussybaka hooks)
or lurk (Phase 8's listen()) — an earlier draft of this table did use
those three names, which is why the family was renamed wholesale rather than
resolved name-by-name.
skibidi main {
SAUCE *f = crackopen("classified_lore.txt", "r");
edgy (!f) {
yapping("file got negative aura");
ragequit(1);
}
goon (!itsjoever(f)) {
rant line = skim(f);
yapping("%s", line);
}
peaceout(f);
bussin 0;
}SAUCE *manifesto = crackopen("schizo.txt", "w");
yapto(manifesto, "aura = %d", aura);
peaceout(manifesto);- A
SAUCE *handle type wrappingFILE *, following the same handle pattern the ABI work in Phase 2 formalizes (STDROT_HANDLE`) — this phase can ship ahead of Phase 2 with a narrower, file-only handle representation and adopt the general one once it lands. stdrot/file.cimplementing the twelve functions above viaSTDROT_EXPORT.- Since
SAUCE *f = crackopen(...)needs the return value in an initializer, this phase is blocked on Phase 1 exactly the waysussybakais.
test_cases/file_io.brainrotcovering open/read/write/close and theitsjoeverloop idiom above, plus a missing-file case (crackopenreturning a negative/falsy handle) exercised throughedgy (!f).- No leaked
FILE *on any exit path —make valgrindis the relevant gate, since an unclosed handle is exactly the kind of bug it exists to catch. docs/reference page for the file I/O vocabulary; README keyword table is not touched, since none of these are grammar keywords.
Status: not started · Priority: P2 · Depends on: Phase 1 · OpenSSL
C's rand() is not a random number generator anyone should gamble with. It is
seeded from the clock, has a tiny state, and rand() % n is biased on top of
that. Brainrot does not wrap it. gamba() is the CSPRNG: OpenSSL
RAND_bytes under the hood, unbiased ranges, and a hard abort if the
generator fails — never a silent 0 that looks like a roll.
gamba is a standard-library builtin, in the same class as yapping,
slorp, bet, chill, and ragequit. It lives in stdrot/gamba.c,
self-registers through STDROT_EXPORT into libstdrot.so, and is globally
available in every program with no #cooked and no extra link step. It is
not a keyword (lang.l is untouched), not an optional module, and
not the generated OpenSSL binding from Phase 5 Road B — it is one
function family that happens to need libcrypto, the same way yapping
happens to need stdio.
Because it is stdlib, OpenSSL is a required native build dependency of
libstdrot.so. A #cooked <gamba> split that keeps the default stdlib
OpenSSL-free is out of scope.
gamba does not collide with a keyword in lang.l, with a registered
builtin (yapping, yappin, baka, bet, chill, ragequit, slorp),
or with any name proposed elsewhere in this roadmap.
| Form | Meaning | Why it exists |
|---|---|---|
gamba() |
unbiased rizz in [0, INT_MAX] |
the rand() shape, but honest |
gamba(n) |
unbiased rizz in [0, n) |
so nobody writes gamba() % n |
gamba(lo, hi) |
unbiased rizz in [lo, hi] inclusive |
dice, damage rolls, closed ranges |
gamba_bytes(buf, n) |
fill n bytes from the CSPRNG |
tokens, nonces, keys |
The ranged forms are not optional. Shipping only the no-arg shape
guarantees that the first game in Phase 5 will do gamba() % 6 and throw
the cryptographic guarantee in the trash. Rejection sampling against
RAND_bytes; no modulo, no libc rand/srand/random, no clock seed.
There is no srand equivalent. OpenSSL seeds itself. A gamba_seed
function would be a security bug dressed as an API.
skibidi main {
rizz roll = gamba(1, 6);
yapping("you rolled %d", roll);
rizz nonce = gamba();
yapping("nonce %d", nonce);
bussin 0;
}stdrot/gamba.cimplementing the four forms viaSTDROT_EXPORT(orSTDROT_EXPORT_SIGif Phase 2 has landed), calling OpenSSLRAND_bytes.RAND_bytesreturning anything other than1is a fatal error, the same way a failedbetis — a CSPRNG failure is not a value.- Unbiased range reduction.
gamba(n)andgamba(lo, hi)rejectn <= 0andhi < lowith a semantic or runtime error, not a wrap. - Link
libcryptointo the nativelibstdrot.so.pkg-config --libs libcrypto(or-lcrypto) on thestdrotlink line; CI installslibssl-dev. Nativemakewithout OpenSSL is a failed build, not agamba-less interpreter — the function is part of the stdlib, so the library does not ship without it. - No libc fallback. A
#ifdefthat silently swaps inrand()is how this phase fails. Missing OpenSSL fails the link; it does not compile a weakergamba. - The wasm target (
brainrot.wasm, issue #175) does not pick up OpenSSL.gambais either absent there or a documented stub that errors; do not draglibcryptointo the browser artifact. A Web Crypto /getentropybackend is a follow-up, not this phase. - Since
rizz roll = gamba(1, 6);needs the return value in an initializer, this phase is blocked on Phase 1 exactly the way file I/O andsussybakaare.
A CSPRNG cannot be string-matched against tests/expected_results.json
the way 2 + 2 can. The fixtures assert properties, not values:
gamba(1, 1)is1;gamba(5)is in[0, 5); a few hundred draws ofgamba(1, 6)are not all the same face (catches "always return 0").- Invalid ranges (
gamba(0),gamba(5, 3)) error. - No fixture stores a concrete roll as expected stdout.
test_cases/gamba.brainrotcovering the no-arg,[0, n), and[lo, hi]forms, plus the "not all the same" smoke check.test_cases/gamba_range_fail.brainrot(or asemantic_error_*/*_failsibling) for invalid ranges.gamba_bytescovered, or explicitly deferred with a reason.- Native
makefails to linklibstdrot.sowithout OpenSSL rather than compiling arand()stub or omittinggamba. - wasm build still succeeds without OpenSSL;
gambais absent or errors there only, because the browser artifact cannot takelibcrypto. Native stdlib always hasgamba. - Documented with the other builtins in
docs/the-brainrot-programming-language.mdanddocs/brainrot-user-guide.md. README keyword table is not touched. make testandmake valgrindgreen on native withlibssl-dev.
| Milestone | Contents | Unlocks |
|---|---|---|
| M1 — Values flow | Phase 1 | Native calls in expressions. Road A raylib demo. |
| M2 — Types flow | Phase 2 | Type-checked native calls; pointers/handles in the ABI. |
| M3 — Layout is honest | Phase 3a + 3d | ABI-correct gang; a.b.c works. |
| M4 — Aggregates are first-class | Phase 3b, 3c, 3e | gang Vector2 GetMousePosition(). |
| M5 — Modules | Phase 4 | #cooked <raylib> means something. |
| M6 — Generated bindings | Phase 5 Road B | raylib as first client; other libraries follow. |
| M7 — Concurrency | Phase 6 | yeet / mogged. |
| M8 — Data structures | Phase 7 | grindset. |
| M9 — Network | Phase 8 | Brainrot serves HTTP. |
| M10 — Brainrot tests itself | Phase 9a/9b | #cooked <sussybaka>, npc, larping, ./brainrot --sus. |
| M11 — The suite is self-describing | Phase 9c | test_cases/ states its own expectations. |
| M12 — Files | Phase 10 | crackopen/peaceout/itsjoever and the rest of the file I/O family. |
| M13 — Honest dice | Phase 11 | gamba() / gamba(n) / gamba(lo, hi) via OpenSSL RAND_bytes. |
M1–M4 are strictly ordered. M7, M8, M10, M12, and M13 are independent of M2–M6 and can proceed in parallel by anyone who wants them. M11 depends only on M10.
M10 quietly delivers two things the rest of the roadmap wants: the module search path that Phase 4 builds on, and the function references that Phases 5, 6, and 8 all need for callbacks. That is the real reason it is P1.
Make StdrotFn results flow through the ordinary expression and type system.
Once rizz x = native_func(); works and the analyzer knows
native_func() -> rizz, everything above becomes incremental. Then fix
C-compatible gang layout. Then point a generator at raylib_api.json. Then
commit the first game as something appropriately cursed.
Proposed additions. Nothing here conflicts with a keyword currently in lang.l.
| Keyword | Meaning | Phase | Status |
|---|---|---|---|
yeet |
spawn thread | 6 | proposed |
mogged |
join thread | 6 | proposed |
gatekeep |
mutex / lock | 6 | proposed |
letcook |
unlock | 6 | proposed |
simp |
condition wait | 6 | proposed |
yass |
condition signal | 6 | proposed |
ratioed |
condition broadcast | 6 | optional |
grindset |
hashmap | 7 | proposed |
ship |
map insert | 7 | proposed |
blorbo |
map lookup | 7 | proposed |
sus |
map contains | 7 | proposed |
unalive |
map delete | 7 | proposed |
bagsize |
map size | 7 | proposed |
drip |
socket | 8 | proposed |
brat |
bind | 8 | proposed |
lurk |
listen | 8 | proposed |
snatched |
accept | 8 | proposed |
stan |
connect | 8 | proposed |
dm |
send | 8 | proposed |
peep |
recv | 8 | proposed |
ghost |
close | 8 | proposed |
npc |
function reference type | 9 | proposed |
Every keyword here is a single word. Brainrot does have precedent for multi-word
keywords — "sigma rule" lexes as case at lang.l:72 — but
phrases read badly in call position (soft launch(server, 8080)), so the
vocabulary stays single-token throughout.
None of these collide with a keyword currently in lang.l, with a registered
builtin (yapping, yappin, baka, bet, chill, ragequit, slorp),
with Phase 11's gamba / gamba_bytes, or with #cooked — which is the
only preprocessor directive there is, and the only one planned (see the
README's own note on why the #ifdef/#define/#pragma family was dropped
rather than left as a TODO). letcook is deliberately one word so it cannot
be confused with the #cooked directive.
bruh was considered and rejected: it is already break
(lang.l:71).
Per AGENTS.md, the README keyword table is a public compatibility surface.
These are additive, but the table must be updated in the same PR that implements
each keyword, and any change to an existing keyword needs explicit sign-off.
npc is the only Phase 9 addition in this table. The rest of the sussybaka
vocabulary — sussybaka, fr, nah, zesty, capping, lowkey, ragebait,
touchgrass, mogg, larping, lockin, logoff, roasted — is deliberately
absent: it is a cooked library, so those are ordinary function names, not
reserved words. Phase 11's gamba / gamba_bytes are absent for the same
reason as Phase 10's file I/O family: they are stdrot functions, they never
appear in the README keyword table, and they cost the grammar nothing. They still must not collide, and all thirteen were checked
against lang.l, the registered builtins, and the proposals above — but they
cost the grammar nothing, they only exist in files that say
#cooked <sussybaka>, and they never appear in the README keyword table. They
are the worked example of the rule in the next paragraph; npc is the worked
example of the exception, because a type needs a spelling and no amount of
library design will give it one.
Several of these could reasonably be library functions instead of keywords —
ship/blorbo/dm/peep in particular. Keyword status buys syntax; it costs
grammar complexity and a reserved identifier forever. A useful split: operations
that need statement syntax (yeet, mogged) earn keyword status, while anything
that is just a call with arguments (ship, blorbo, dm, peep) can stay an
ordinary library function and keep the joke without touching the grammar. Decide
per keyword, and default to "library function" when in doubt.
-
Write-back removal (Phase 1). Does
slorp(x);keep working? Deprecation window, or clean break? -
TypeDescmigration (Phase 3b).One large refactor, or incremental with both representations alive?Resolved: incremental, one carrier per PR (StructField, Parameter, Variable, Function return, ReturnValue — #258–#261), each a mechanical, compiler-verified, behavior-neutral rename. -
Struct assignment.
Value copy (C semantics) or reference?Resolved: value copy, matching C — deep-copied on assignment, argument passing, and return; tested. -
Threading model (Phase 6). Green threads, GIL, or thread-local state? Recommendation above is GIL-first, but this is a real fork in the road.
-
grindsetgenerics (Phase 7). String-keyed monomorphic, or a real type parameter system? The latter is a language-design project of its own. -
Ownership of native resources. Textures, sockets, and map entries all outlive statements. Brainrot has no destructors and no GC. Handles sidestep this by keeping ownership in C — is that the general answer?
-
Generated code in-tree or built?
Resolved (#208): the rule extends to bindings — generator output is never committed — but the question conflates two artifacts that get opposite answers, and separating them dissolves most of the dependency worry.raylib_api.jsonoutput could be committed or generated at build time.AGENTS.mdforbids committing generated files; does that rule extend to bindings, and if so, does raylib become a build dependency?raylib_api.jsonis a committed source input, not generated output. It is raylib's own published description of its API, vendored and pinned to a specific raylib version, the same way a schema or a.protois vendored. Nothing in this repo derives it; it is regenerated upstream by raylib'stools/rlparser, which we deliberately do not want to run at build time. Committing it is what makes the generator's output reproducible from a clean checkout and reviewable as a diff when the pinned version moves.- The C adapters the generator emits are derived, and are not
committed. They are
lang.tab.cby another name: produced into the build tree (rayrot/generated/) by therayrot-gen-sourcesandrayrot-gentargets only -- NOT byrayrot, which builds Road A's hand-written module and never runs the generator -- gitignored, never hand-edited, and excluded frommake format-checkexactly as the Bison/Flex output already is (a generator that has to satisfy clang-format is a generator nobody wants to change).
So the answer to "does raylib become a build dependency?" is: no more than it already is.
make rayrothas required a pkg-config-visible raylib since Road A, and Road B does not widen that — it needs raylib's headers at adapter-compile time, which any installed raylib already provides, and the pinned JSON for the generation step, which is in-tree.make,make test,make valgrind, andmake format-checkcontinue to neither run the generator nor require raylib, which is the constraint that actually matters and is a Road B definition-of-done item in its own right.Rejected alternatives: committing the adapters (option 1) buys a raylib-free
rayrotbuild that cannot exist anyway — the adapters#include <raylib.h>and link against it — in exchange for carving a permanent exception into a rule whose whole value is being absolute. Generating in CI and committing nothing at all, not even the JSON (option 3 as originally sketched), makes a clean checkout unable to buildrayrotwithout network access and turns every upstream API change into an invisible, unreviewable build-time surprise. -
Should
npcbe its own phase (Phase 9b)? Function references are a language feature that Phases 5, 6, and 8 all want for callbacks, and they are the largest item inside a phase otherwise made of library code. Leaving them in Phase 9 means the testing library is gated on a grammar change; hoisting them out means Phase 9 ships name-string registration first and gainslarpinglater. Recommendation: hoist, if anyone other thansussybakaasks for them first. -
Test discovery (Phase 9). Registration-by-manifest in
mainversus convention-based auto-discovery (--susruns every zero-argskibidifunction namedsus_*). The manifest is explicit and needs no new machinery; auto-discovery removes the last piece of boilerplate but makes the runner depend on a naming convention. -
Mock restoration on abort (Phase 9b).
larpingstubs must be restored even when a testlongjmps out mid-way. Does that need a general "unwind actions" mechanism in the interpreter, which nothing else currently has, or is a runner-owned restore list enough? The general mechanism would also servelogoff,gatekeep/letcook(Phase 6), andghost(Phase 8), which all have the same "must run even on abnormal exit" shape. -
Module search path (Phase 4/9a).
Resolved (#207): not "always, in a fixed order" — exactly ONE of {install module directory, in-tree$BRAINROT_PATH+ install prefix + in-treestdrot/is proposed. Does the in-tree fallback apply always, or only for an uninstalled build?stdrot/} is ever consulted for a given run, chosen by which binary is actually running, so the two structurally cannot shadow each other in either direction. The running executable's own directory is compared against the install bin directory (/usr/local/bin); a match means this IS the installed binary, so only the install module directory applies; otherwise this is a source/dev build, so onlystdrot/next to the executable applies. "The running executable" is resolved via/proc/self/exe(Linux) or_NSGetExecutablePath(macOS) — deliberately notargv[0], which for a bare$PATH-resolved command name (typingbrainrotaftermake install) has no directory component at all and would resolve against cwd instead of the binary that's actually running.$BRAINROT_PATHremains the highest-precedence tier, checked before either half of this split. -
Prelude versus builtins (Phase 9a). The split is "primitives in
libstdrot.so, surface in a cooked.brainrotprelude". The primitives are still globally visible builtins even when nobody cookssussybaka— do they need a naming convention (__sus_record) to signal "not for you", or does the language need real module-private names, which nothing else has? -
How far does the §9c migration actually go? Error-path cases assert on stderr and a non-zero exit code, so they cannot become sussybaka tests. That means
tests/expected_results.jsonprobably survives forever in reduced form. Is a permanently two-mode harness acceptable, or should the error-path cases get their own mechanism (ragebaitat file scope?) so the JSON can genuinely be deleted? -
AGENTS.mdis a contract. It currently requires anexpected_results.jsonentry for every test. Step 2 of §9c makes that false. Update it in the same PR, or add the sussybaka mode alongside it and remove the JSON requirement only when the migration completes? -
OpenSSL as a build dependency (Phase 11). Settled:
gambais part of the Brainrot standard library, solibcryptois a required native dependency oflibstdrot.so.#cooked <gamba>as a separate.sois rejected. wasm still must not link OpenSSL; that is a platform limitation of the browser artifact, not an optional stdlib. -
gamba()no-arg range.[0, INT_MAX]matchesrand()'s shape. Full[0, 2^32)wantsnonut rizz/ a wider type. Requiring an explicit range and dropping the no-arg form is the safest API and the worst meme. Recommendation: keepgamba()as[0, INT_MAX], but shipgamba(n)/gamba(lo, hi)in the same PR. -
wasm
gamba(Phase 11). Absent, a stub that errors, or a later Web Crypto /getentropybackend? Recommendation for this phase: absent or erroring stub. Do not block the wasm playground on OpenSSL.