feat(stdrot): cryptographically safe gamba() RNG (#215) - #279
Conversation
`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>
There was a problem hiding this comment.
Stale comment
REQUEST CHANGES
The CSPRNG core is not the problem.
gamba_uniformis the OpenBSDarc4random_uniformconstruction on 64-bit draws, which is the right width:gamba(INT_MIN, INT_MAX)has span2^32, and(int64_t)lo + drawlands exactly on[INT_MIN, INT_MAX].RAND_bytes != 1aborts instead of returning a look-alike0. There is norand()fallback.gamba_bytesis deferred with a real ABI reason, not a shrug. wasm stays-DSTDROT_STATICand never seesCRYPTO_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 cannotmake test. The darwin artifacts this same PR taught tobrew install openssl@3will not load the standard library on a machine that is not the builder.CI lint is green. I compiled
libstdrot.sohere;lddshowsNEEDED libcrypto.so.3. I did not runmake test/make valgrindin 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
makewithout OpenSSL is a failed build, not agamba-less interpreter. CIbuild/test/tidyandnative-release-buildon Linux now installlibssl-dev. Then the rollout ends.
.github/workflows/release.ymlversionstill installsgcc flex bison libfl-dev ...with nolibssl-dev, then runsmake test. That job is the gate for every GitHub Release.gamba.cdoes#include <openssl/rand.h>. Without the -dev package that is a hard compile error. This PR addedlibssl-devtoci.ymlbecause 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.mdanddocs/the-brainrot-programming-language.md§4 still sayapt-get install gcc flex bison libfl-dev/brew install gcc flex bisonmake check-depsstill does not look forlibcryptolibssl3is now a runtimeDT_NEEDEDoflibstdrot.soon 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.sois glued to the builder's Homebrew OpenSSLBLOCKING.
native-release-buildbrew install openssl@3and setsPKG_CONFIG_PATHso the runner can compile. Homebrewlibcryptois keg-only. Its install name is an absolute prefix (/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylibor/usr/local/...on Intel). Linking viapkg-config --libs libcryptocopies that path intolibstdrot.soasLC_LOAD_DYLIB.Every Brainrot program dlopens
libstdrot.so— includingexamples/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, andyappingis as dead asgamba. 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
libcryptointolibstdrot.so(still fails the native link if OpenSSL is missing; wasm still never sees it) or ship the dylib next tolibstdrot.sowith@loader_path.PKG_CONFIG_PATHon the builder is not a distribution strategy.3.
gamba()error path cannot report a line numberMAJOR.
gamba_dieprintsat line %dfromg_exec_context.line_numberand claims to matchbet().execute_native_call()sets that field from the first argument node's line, then zeroes it when there are no arguments and callsentry->fn(NULL, 0).
gamba()is a zero-arg form this PR ships. On wasm, and on a nativeRAND_bytesfailure, that form printsat line 0.betalways has an argument, so it never hit this. The wasm override is anchored ongamba(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 makegamba()the first zero-arg native that aborts and then pretendg_exec_contextis 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_bytesdeferral match #215. The OpenSSL dependency the issue made non-optional was applied toci.ymland the Linux side ofnative-release-buildand 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 canmake, and a darwinlibstdrot.socopied off the builder candlopenwithout the builder's Homebrew prefix. The dice are fine. The library they live in is not shippable like this.Sent by Cursor Automation: Code Reviewer
…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>
|
/cursor-review |
There was a problem hiding this comment.
Stale comment
COMMENT
dfbb1e7closed the two packaging blockers. OpenSSL is on the GitHub Releaseversionjob, the public install recipes,make check-deps, andflake.nix. Darwin now feeds the linker a full path tolibcrypto.ainstead of-lcrypto, which is the actual way to keep Homebrew's keg-only dylib out ofLC_LOAD_DYLIB. I builtlibstdrot.sohere:lddisNEEDED libcrypto.so.3,RAND_bytesis unresolved from that soname. Nativegamba/gamba_noarg/gamba_range_fail/gamba_zero_failmatchexpected_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 0MAJOR.
execute_native_callnow takescall_lineand uses it when there is no argument node.native_call_peek/native_call_consumepassnode->line_number. That path is real: against a-DSTDROT_STATICstub I getat line 2forrizz n = gamba();.
execute_func_callis the other caller. It passesg_exec_context.line_numberand claimsast.c'sNODE_FUNC_CALLcase already wrote the call node's line. The live interpreter does not go through that case. Statement-list entries and for-loop init/incr runinterpreter_execute_call_statement→execute_builtin_function→execute_func_call. That helper never touchesg_exec_context.line_number. The global is still0(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 0forskibidi main { gamba(); bussin 0; }against a stub
libstdrot.so. Same binary,rizz n = gamba();, reports line 2.gamba_noarg.brainrotis an initializer. The wasm override cannot see this.betstill has an argument, so it still cannot see this.Pass
node->line_numberin frominterpreter_execute_call_statement. Do not document a fallback that reads a global the live dispatcher does not populate. A comment that names the deadexecute_statementpath 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_bytesdeferral 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 runsgamba();.That is not a reason to rip the sampler out. It is a reason not to treat
dfbb1e7as 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 tidystill does not passCRYPTO_CFLAGS; darwin verify still does nototool -Lthe artifact for a Homebrewlibcryptoload command;native-release-build's comment still describespkg-config --libs libcryptoafter 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 valgrindsuite.Sent by Cursor Automation: Code Reviewer
…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>
|
/cursor-review |
There was a problem hiding this comment.
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_function → execute_func_call → execute_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.brainrot→at line 8skibidi main { gamba(); ... }→at line 2(this wasat line 0ondfbb1e7)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.
Sent by Cursor Automation: Code Reviewer


Description
Implements Phase 11 — the cryptographically safe
gamba()(issue #215).gambais astdrotstandard-library builtin (no#cooked, not a keyword,lang.luntouched) backed by OpenSSLRAND_bytes. It is not a wrapper around C'srand()/random()/srand(). It ships all three integer forms in one PR so nobody reaches forgamba() % n:gamba()rizzin[0, INT_MAX]gamba(n)rizzin[0, n)gamba(lo, hi)rizzin[lo, hi], inclusiveDesign decisions, all per the issue:
arc4random_uniformalgorithm) — no modulo bias.RAND_bytesreturn other than1is a hard abort, never a look-alike0.gamba(n)rejectsn <= 0,gamba(lo, hi)rejectshi < lo— a runtime error, not a wrap.gamba_seed(OpenSSL seeds itself).libcryptois now a required native dependency oflibstdrot.so(Makefile viapkg-config/-lcrypto; CI installslibssl-dev). A missing OpenSSL fails the native link rather than compiling a weakergamba.-DSTDROT_STATIC,gambais a documented erroring stub, handled by aWASM_EXPECTED_OVERRIDESentry 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) anddocs/brainrot-user-guide.md(§10.8). README keyword table not touched.Related Issue
Fixes #215
Type of Change
Checklist
make format-checklocally (ormake formatto fix)Testing notes
Property-style fixtures (a CSPRNG can't be string-matched against a fixed roll):
test_cases/gamba.brainrot—gamba(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.brainrot—gamba(5, 3)aborts.test_cases/gamba_zero_fail.brainrot—gamba(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 callsgamba). The wasm erroring-stub path was verified by buildinglibstdrot.sowith-DSTDROT_STATIC.🤖 Generated with Claude Code