Add the random builtin: one uniform integer, typed, replacing $RANDOM - #418
Merged
Conversation
kaish has no $RANDOM, and the arithmetic rewrite that follows this change refuses $((RANDOM % 100)) with an error naming a builtin as the fix. That builtin has to exist first, so this adds it: random [--min N] [--max N] prints one integer chosen from min..=max, both inclusive (defaults 0 and 32767, matching bash's $RANDOM range). The output is typed, not stringified. The schema declares with_typed_substitution() and the result carries Value::Int, so $(random --max 6) binds a number and x=$(random --max 6); echo $((x + 1)) works without a cast. --json emits the bare number, not a string-wrapped envelope. Sampling reads 8 bytes from getrandom and maps them onto the range with Lemire's method (widen the multiply into 128 bits, reject the low slice that would otherwise skew one bucket) instead of draw % width, which is measurably biased whenever the range doesn't evenly divide 2^64. The width itself is carried in u128 so the full i64 span (--min i64::MIN --max i64::MAX) doesn't overflow computing it. A getrandom failure is a hard error, not a fallback value - a script that trusted random for a coin flip must never get a predictable substitute silently. Bounds and shape are enforced before any draw happens, each a curated exit-2 error naming the value and the fix: --min greater than --max, a non-integer bound (left to clap's own parse error), and a positional argument (random takes none - the fix always spells out the equivalent --max flag). random registers alphabetically in the builtin table between pwd and read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add random to the README's System category table (alphabetical, between push and read) and a CHANGELOG Unreleased/Added bullet summarizing the contract and the unbiased-sampling method. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sweep_covers_every_registered_builtin failed with random missing from CASES. Add it between pwd and read, using a fixed --min 3 --max 3 range so the draw is deterministic and the case only pins the shape: --json on a typed Int emits a bare JSON number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
random.rs's non-test comments ran 45 lines against 113 lines of code - narrative that belongs in the commit history, which already carries it (the previous commit explains why Lemire's method, why the u128 width, why entropy failure is fatal). Trimmed the module doc, the map_draw_to_range and draw_random doc comments, and the inline // notes down to what a reader needs at the point of the code - 20 comment lines against 112 lines of code. The published /// on --min, --max, and the about text are untouched. Also trims the CHANGELOG bullet: drop the algorithm name and the modulo-bias parenthetical, since that detail lives in the code and the commit, not the release notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
map_draw_is_deterministic called map_draw_to_range twice with the same draw and asserted the two results were equal - trivially true for any pure function, and the chosen draw landed on an accepted value, so no test pinned an actual mapped result or a rejection. An off-by-one or a flipped threshold comparison would have passed unnoticed. Replaced it with map_draw_accepts_and_maps_a_known_draw (draw 0x1234_5678_9abc_def0 over -100..=100 must map to exactly -86) and map_draw_rejects_a_known_biased_draw (draw 0 over 0..=6 must reject, since it falls under the width-7 threshold of 2). Both values were computed independently of the implementation before writing the assertions. Also pinned the full i64 span's boundary outputs (map_draw_full_i64_span_pins_boundary_values): draw 0 must map to i64::MIN and draw u64::MAX to i64::MAX, not just "some value in range" as the existing never_panics test already checked. Verified the new tests actually discriminate: flipping the threshold comparison from < to > failed both map_draw_accepts_and_maps_a_known_draw and map_draw_rejects_a_known_biased_draw before the fix was reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
kaish had no
$RANDOM, and the arithmetic rewrite that follows refuses$((RANDOM % 100))with an error that has to name a real replacement. This is that replacement.The draw is uniform: 8 bytes from the OS CSPRNG mapped onto the range by Lemire's method with rejection, so
draw % widthbias never appears, and the width is carried in 128 bits so the full 64-bit span works. An entropy failure is an error, never a fixed value.Errors exit 2 and name the fix:
The mapper's unit tests pin exact accepted, rejected, and full-span boundary values, checked by mutating the threshold comparison and watching them fail.
🤖 Generated with Claude Code