Skip to content

feat(stdrot): cryptographically safe gamba() RNG (#215) - #279

Merged
leo-aa88 merged 3 commits into
mainfrom
feat/215-gamba-csprng
Aug 27, 2026
Merged

feat(stdrot): cryptographically safe gamba() RNG (#215)#279
leo-aa88 merged 3 commits into
mainfrom
feat/215-gamba-csprng

Conversation

@leo-aa88

Copy link
Copy Markdown
Member

Description

Implements Phase 11 — the cryptographically safe gamba() (issue #215).

gamba is a stdrot standard-library builtin (no #cooked, not a keyword, lang.l untouched) backed by OpenSSL RAND_bytes. It is not a wrapper around C's rand()/random()/srand(). It ships all three integer forms in one PR so nobody reaches for gamba() % n:

Form Meaning
gamba() unbiased rizz in [0, INT_MAX]
gamba(n) unbiased rizz in [0, n)
gamba(lo, hi) unbiased rizz in [lo, hi], inclusive

Design decisions, all per the issue:

  • Unbiased ranges via rejection sampling (OpenBSD arc4random_uniform algorithm) — no modulo bias.
  • Honest failures: a RAND_bytes return other than 1 is a hard abort, never a look-alike 0.
  • Invalid ranges abort: gamba(n) rejects n <= 0, gamba(lo, hi) rejects hi < lo — a runtime error, not a wrap.
  • No seed: there is no gamba_seed (OpenSSL seeds itself).
  • No libc fallback: libcrypto is now a required native dependency of libstdrot.so (Makefile via pkg-config/-lcrypto; CI installs libssl-dev). A missing OpenSSL fails the native link rather than compiling a weaker gamba.
  • wasm stays OpenSSL-free (issue build: add WebAssembly target for the browser playground #175): under -DSTDROT_STATIC, gamba is a documented erroring stub, handled by a WASM_EXPECTED_OVERRIDES entry in the wasm test runner.
  • gamba_bytes(buf, n) is explicitly deferred with a documented reason: it needs a mutable output-buffer ABI contract this boundary doesn't cleanly express yet. The three integer forms are the crypto-safe surface this phase guarantees.

Documented alongside the other builtins in docs/the-brainrot-programming-language.md (§8.8) and docs/brainrot-user-guide.md (§10.8). README keyword table not touched.

Related Issue

Fixes #215

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Refactor

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have documented my changes in the code or documentation
  • I have added tests that prove my changes work (if applicable)
  • I have run make format-check locally (or make format to fix)
  • I have run the unit tests locally
  • I have run the valgrind memory tests locally
  • All new and existing tests pass

Testing notes

Property-style fixtures (a CSPRNG can't be string-matched against a fixed roll):

  • test_cases/gamba.brainrotgamba(1,1) == 1, gamba(5) ∈ [0,5), gamba(1,6) bounds hold across 500 draws and are not all the same face, gamba() non-negative.
  • test_cases/gamba_range_fail.brainrotgamba(5, 3) aborts.
  • test_cases/gamba_zero_fail.brainrotgamba(0) aborts.

make test (402 passed) is green. Valgrind was run against a sanitizer-free build (RAND_bytes adds zero lost bytes — the only "still reachable" is baseline interpreter state, identical to a fixture that never calls gamba). The wasm erroring-stub path was verified by building libstdrot.so with -DSTDROT_STATIC.

🤖 Generated with Claude Code

`gamba` is Brainrot's CSPRNG: a stdrot builtin backed by OpenSSL
`RAND_bytes`, not a wrapper around C's rand()/random()/srand(). It ships
the three integer forms so nobody reaches for `gamba() % n`:

  - gamba()          unbiased rizz in [0, INT_MAX]
  - gamba(n)         unbiased rizz in [0, n)
  - gamba(lo, hi)    unbiased rizz in [lo, hi], inclusive

Ranges use rejection sampling (OpenBSD arc4random_uniform), so no modulo
bias. A RAND_bytes return other than 1 is a hard abort -- a CSPRNG
failure is never a look-alike 0. Invalid ranges (n <= 0, hi < lo) abort
rather than wrap. There is no gamba_seed (OpenSSL seeds itself) and no
libc fallback: a missing OpenSSL fails the native link, it never compiles
a weaker gamba.

libcrypto is now a required native dependency of libstdrot.so (Makefile
via pkg-config/-lcrypto; CI installs libssl-dev). The wasm build stays
OpenSSL-free (issue #175): under -DSTDROT_STATIC gamba is a documented
erroring stub, handled by a WASM_EXPECTED_OVERRIDES entry.

gamba_bytes(buf, n) is deferred with a documented reason (needs a
mutable output-buffer ABI not yet expressed).

Tests: property-style gamba.brainrot (fixed value, bounds, "not all the
same" smoke check, no-arg non-negativity) plus gamba_range_fail /
gamba_zero_fail. Documented alongside the other builtins in docs/ (README
keyword table untouched). make test / valgrind / format-check green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@leo-aa88
leo-aa88 requested a review from Mazinger677 August 27, 2026 22:06

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

REQUEST CHANGES

The CSPRNG core is not the problem. gamba_uniform is the OpenBSD arc4random_uniform construction on 64-bit draws, which is the right width: gamba(INT_MIN, INT_MAX) has span 2^32, and (int64_t)lo + draw lands exactly on [INT_MIN, INT_MAX]. RAND_bytes != 1 aborts instead of returning a look-alike 0. There is no rand() fallback. gamba_bytes is deferred with a real ABI reason, not a shrug. wasm stays -DSTDROT_STATIC and never sees CRYPTO_LIBS. That part of #215 is actually implemented.

What is not implemented is the dependency the issue made load-bearing. OpenSSL is now a required native dependency of libstdrot.so. This PR wired that into the jobs that were in front of the author and then stopped. The public install recipe still cannot build the tree. The GitHub Release gate still cannot make test. The darwin artifacts this same PR taught to brew install openssl@3 will not load the standard library on a machine that is not the builder.

CI lint is green. I compiled libstdrot.so here; ldd shows NEEDED libcrypto.so.3. I did not run make test / make valgrind in this environment (no bison). That does not matter. The failures below are not happy-path fixture failures.

1. The required OpenSSL dependency was not actually rolled out

BLOCKING.

The issue's contract: native make without OpenSSL is a failed build, not a gamba-less interpreter. CI build/test/tidy and native-release-build on Linux now install libssl-dev. Then the rollout ends.

.github/workflows/release.yml version still installs gcc flex bison libfl-dev ... with no libssl-dev, then runs make test. That job is the gate for every GitHub Release. gamba.c does #include <openssl/rand.h>. Without the -dev package that is a hard compile error. This PR added libssl-dev to ci.yml because the headers are not on the image. The release workflow was not updated. Tag a release after merge and packaging dies before any artifact is built.

Same lie in the install surface people actually follow:

  • README.md and docs/the-brainrot-programming-language.md §4 still say apt-get install gcc flex bison libfl-dev / brew install gcc flex bison
  • make check-deps still does not look for libcrypto
  • libssl3 is now a runtime DT_NEEDED of libstdrot.so on Linux and is not mentioned anywhere outside the gamba subsection of the user guide

§8.8 telling the truth does not make §4 true. "README keyword table not touched" was about not adding a keyword. It was not permission to leave the documented build recipe as a failed link.

2. macOS release libstdrot.so is glued to the builder's Homebrew OpenSSL

BLOCKING.

native-release-build brew install openssl@3 and sets PKG_CONFIG_PATH so the runner can compile. Homebrew libcrypto is keg-only. Its install name is an absolute prefix (/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib or /usr/local/... on Intel). Linking via pkg-config --libs libcrypto copies that path into libstdrot.so as LC_LOAD_DYLIB.

Every Brainrot program dlopens libstdrot.so — including examples/hello_world.brainrot, which is the only thing the verify step runs. On the builder the brew path exists, so verify is green. On a user's Mac that file is not there, dyld cannot load the stdlib, and yapping is as dead as gamba. Requiring OpenSSL at build time does not mean "the shipped dylib only works inside the GHA image that compiled it."

CI is green. This is not a failing-test problem. The current tests do not exercise this contract.

Static-link libcrypto into libstdrot.so (still fails the native link if OpenSSL is missing; wasm still never sees it) or ship the dylib next to libstdrot.so with @loader_path. PKG_CONFIG_PATH on the builder is not a distribution strategy.

3. gamba() error path cannot report a line number

MAJOR.

gamba_die prints at line %d from g_exec_context.line_number and claims to match bet(). execute_native_call() sets that field from the first argument node's line, then zeroes it when there are no arguments and calls entry->fn(NULL, 0).

gamba() is a zero-arg form this PR ships. On wasm, and on a native RAND_bytes failure, that form prints at line 0. bet always has an argument, so it never hit this. The wasm override is anchored on gamba(1, 1) at line 7 of the happy-path fixture — a call that has args — so the suite cannot see it.

THE CODE SAYS: errors report the call site.

THE RUNTIME DOES: the no-arg form reports line 0.

Pass the call node's line into execute_native_call (it already exists on the AST). Do not make gamba() the first zero-arg native that aborts and then pretend g_exec_context is populated.

VERDICT

The problem is scope / packaging, not the sampler. The integer API, the rejection sampling, the hard abort, the wasm stub, and the gamba_bytes deferral match #215. The OpenSSL dependency the issue made non-optional was applied to ci.yml and the Linux side of native-release-build and then treated as done. release.yml, the install docs, and the darwin artifact's actual load-time linkage were left as they were when OpenSSL was not in the tree.

Do not merge this until a GitHub Release can make test, the documented install recipe can make, and a darwin libstdrot.so copied off the builder can dlopen without the builder's Homebrew prefix. The dice are fine. The library they live in is not shippable like this.

Open in Web View Automation 

Sent by Cursor Automation: Code Reviewer

Comment thread Makefile
Comment thread .github/actions/native-release-build/action.yml
Comment thread .github/workflows/ci.yml
Comment thread stdrot/gamba.c
…215 review)

Addresses the PR #279 review. The sampler was fine; the OpenSSL dependency
and the zero-arg error path were not.

1. OpenSSL rollout was incomplete (BLOCKING). Made the required dependency
   real everywhere the tree is built, not just the jobs already touched:
   - release.yml `version` (the GitHub Release gate, runs `make test`) now
     installs libssl-dev, so tagging a release no longer dies on
     `#include <openssl/rand.h>`.
   - README.md and docs §4 install recipes add libssl-dev / openssl /
     openssl@3, and document libcrypto as a build+runtime dependency.
   - flake.nix gains openssl + pkg-config (package and devShell).
   - `make check-deps` now verifies libcrypto via pkg-config.

2. macOS release libstdrot.so was glued to the builder's keg-only Homebrew
   OpenSSL (BLOCKING). A dynamic link baked the absolute
   /opt/homebrew/.../libcrypto.3.dylib install_name into the dylib as an
   LC_LOAD_DYLIB, so the shipped stdlib -- every program, not just gamba --
   would fail to dlopen on any Mac without that exact prefix. The Makefile
   now STATICALLY links libcrypto.a on Darwin (self-contained dylib, still
   fails the link when OpenSSL is absent, wasm untouched) and auto-locates
   keg-only openssl@3 via `brew --prefix` so a plain `make` works. Linux
   keeps the portable soname dependency (DT_NEEDED libcrypto.so.3).

3. gamba() could not report a line number (MAJOR). execute_native_call()
   derived g_exec_context.line_number from the first argument node, which a
   zero-arg call doesn't have -- so gamba() on a CSPRNG failure / the wasm
   stub printed "at line 0". Thread the call node's line through
   execute_native_call() as the zero-arg fallback (arg-derived line still
   wins when present, so every existing diagnostic is byte-identical). New
   test_cases/gamba_noarg.brainrot guards it (line 8, not 0, on the wasm
   stub); native prints its property. make test 403 green, valgrind-clean,
   format-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@leo-aa88

Copy link
Copy Markdown
Member Author

/cursor-review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

COMMENT

dfbb1e7 closed the two packaging blockers. OpenSSL is on the GitHub Release version job, the public install recipes, make check-deps, and flake.nix. Darwin now feeds the linker a full path to libcrypto.a instead of -lcrypto, which is the actual way to keep Homebrew's keg-only dylib out of LC_LOAD_DYLIB. I built libstdrot.so here: ldd is NEEDED libcrypto.so.3, RAND_bytes is unresolved from that soname. Native gamba / gamba_noarg / gamba_range_fail / gamba_zero_fail match expected_results.json. The sampler was never the problem.

The line-number fix stopped one call site short of the dispatcher that actually runs statement-position natives. Expression-position rizz n = gamba() is fixed and tested. gamba(); as a statement is not.

1. Statement-position gamba() still reports line 0

MAJOR.

execute_native_call now takes call_line and uses it when there is no argument node. native_call_peek / native_call_consume pass node->line_number. That path is real: against a -DSTDROT_STATIC stub I get at line 2 for rizz n = gamba();.

execute_func_call is the other caller. It passes g_exec_context.line_number and claims ast.c's NODE_FUNC_CALL case already wrote the call node's line. The live interpreter does not go through that case. Statement-list entries and for-loop init/incr run interpreter_execute_call_statementexecute_builtin_functionexecute_func_call. That helper never touches g_exec_context.line_number. The global is still 0 (or a previous native's line).

THE CODE SAYS: a zero-arg abort reports the call site, including statement position.

THE RUNTIME DOES: Error: gamba: CSPRNG unavailable in this build (no OpenSSL) at line 0 for

skibidi main {
    gamba();
    bussin 0;
}

against a stub libstdrot.so. Same binary, rizz n = gamba();, reports line 2. gamba_noarg.brainrot is an initializer. The wasm override cannot see this. bet still has an argument, so it still cannot see this.

Pass node->line_number in from interpreter_execute_call_statement. Do not document a fallback that reads a global the live dispatcher does not populate. A comment that names the dead execute_statement path is not a contract.

VERDICT

The remaining issue is implementation, not the CSPRNG and not the OpenSSL rollout. The integer API, rejection sampling, hard abort, wasm stub, Darwin static archive path, and gamba_bytes deferral match #215. The zero-arg diagnostic contract this follow-up added is true for expression-position calls and false for the statement-list path that actually runs gamba();.

That is not a reason to rip the sampler out. It is a reason not to treat dfbb1e7 as having finished the line-number bug it named. Fix the dispatcher (or stop claiming statement-position aborts report a line) and this is otherwise mergeable.

Leftovers, not the merge question: make tidy still does not pass CRYPTO_CFLAGS; darwin verify still does not otool -L the artifact for a Homebrew libcrypto load command; native-release-build's comment still describes pkg-config --libs libcrypto after the Makefile stopped using that.

Lint is green. I ran the four gamba fixtures plus the stub reproduction above. I did not run the full make test / make valgrind suite.

Open in Web View Automation 

Sent by Cursor Automation: Code Reviewer

Comment thread stdrot.c Outdated
…eview)

The previous line-number fix covered only expression-position native calls
(native_call_peek / native_call_consume pass node->line_number). Statement-
position calls -- a bare `gamba();`, or a for-loop init/incr -- run through
interpreter_execute_call_statement -> execute_builtin_function ->
execute_func_call, which derived the line from a global
(g_exec_context.line_number) the live statement dispatcher never populates.
So `gamba();` on the wasm stub / a CSPRNG failure still printed "at line 0".

Thread call_line through execute_builtin_function() and execute_func_call()
too, and pass node->line_number from every statement-position call site
(interpreter_execute_call_statement, the internal yapping/baka helpers, and
ast.c's two dispatch cases). execute_func_call() no longer reads the global
back as a fallback -- the comment that claimed ast.c populated it described a
path the live interpreter doesn't take.

New test_cases/gamba_statement.brainrot covers the bare-statement form: native
draws and discards then prints "drew"; the wasm build errors at the line 8
call (override added), guarding line 8 != 0 for the dispatcher the reviewer
identified. gamba_noarg (initializer) still guards the expression path.

Also the review's non-blocking leftovers:
- `make tidy` now passes CRYPTO_CFLAGS so clang-tidy can find <openssl/rand.h>.
- native-release-build verifies (otool -L) the Darwin libstdrot.so carries no
  external OpenSSL load command -- the "works only on the builder" regression.
- corrected native-release-build's stale comment: Darwin statically links
  libcrypto.a, it no longer uses `pkg-config --libs libcrypto`.

make test 404 green, valgrind-clean, format-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@leo-aa88

Copy link
Copy Markdown
Member Author

/cursor-review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE

54ac138 closed the hole dfbb1e7 left open. The sampler, the OpenSSL rollout, and the Darwin archive link were already done. The remaining defect was a dispatcher that documented a line-number contract and then read a global the live interpreter never writes.

That path now takes call_line all the way through execute_builtin_functionexecute_func_callexecute_native_call. interpreter_execute_call_statement (statement list and for-loop init/incr), ast.c's NODE_FUNC_CALL / void-bussin arms, and the yapping/baka helpers pass node->line_number. execute_func_call no longer falls back to g_exec_context.line_number. The comment that named the dead execute_statement path is gone with it.

I compiled libstdrot.so with -DSTDROT_STATIC and ran the stub:

  • test_cases/gamba_statement.brainrotat line 8
  • skibidi main { gamba(); ... }at line 2 (this was at line 0 on dfbb1e7)
  • rizz n = gamba();at line 2
  • for-init gamba();at line 3

gamba_statement.brainrot plus the wasm override is the test that would have caught dfbb1e7. gamba_noarg.brainrot still covers the initializer. Both are required; one dispatcher is not a substitute for the other.

VERDICT

The problem on this SHA is none that I can demonstrate. Issue #215's integer surface, rejection sampling, hard abort, wasm stub, gamba_bytes deferral, required native libcrypto, and Darwin static libcrypto.a (no keg-only LC_LOAD_DYLIB) match the claimed contract. The zero-arg diagnostic contract is now true for both expression-position and statement-position calls, including the for-loop clause that shares interpreter_execute_call_statement.

If the next engineer treats execute_native_call's call_line comment, stdrot.h's execute_builtin_function comment, and the wasm override for gamba_statement as true, the runtime does what those comments say.

Merge.

Second-look leftovers are in this commit: make tidy passes CRYPTO_CFLAGS; Darwin verify otool -L | grep -Ei 'libcrypto|openssl' fail-closes; the native-release-build comment no longer describes pkg-config --libs libcrypto.

Checked here: sanitizer make; ldd NEEDED libcrypto.so.3 / RAND_bytes undefined from that soname; five gamba pytest cases; format-check; valgrind on a sanitizer-free binary over the five gamba fixtures (definite/possible 0 bytes); stub reproduction above. Lint, static-analysis, and build are green on 54ac138. I did not re-run the full make test / make valgrind suite.

Open in Web View Automation 

Sent by Cursor Automation: Code Reviewer

@leo-aa88
leo-aa88 merged commit 721adfb into main Aug 27, 2026
10 checks passed
@leo-aa88
leo-aa88 deleted the feat/215-gamba-csprng branch August 27, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Roadmap] Phase 11 — Cryptographically safe gamba()

1 participant