Skip to content

feat(stdlib): make rand seedable with OTP 29's algorithms - #14

Merged
mremond merged 3 commits into
mainfrom
fm/task-b01e1756
Nov 12, 2026
Merged

mremond merged 3 commits into
mainfrom
fm/task-b01e1756

Conversation

@mremond

@mremond mremond commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

rand:uniform/1 could not be seeded and no other rand function existed, so seeded programs such as PropEr runs drew different numbers on every run.

What changed

  • Programs can seed rand and get the same numbers, states and errors as OTP 29 for the same seed and algorithm.
  • The rest of OTP's rand module works: uniform/0, uniform_s, uniform_real, bytes, normal, shuffle, jump and exported or explicit states, for every algorithm.
  • math:exp/1, math:log/1 and math:pow/2 are available, with OTP's errors.
  • trunc, round, ceil and floor return the exact integer for floats beyond 2^63 instead of a clamped one.
  • Cowlib's seeded date property now runs natively with the same traces as on BEAM.

Before and after

Before: A program calling rand:seed/2 failed to build because the rand module was not available, and two runs of rand:uniform(100) printed different numbers.

After: A seeded program prints the same numbers on every run, equal to OTP 29.1.2's output, and Cowlib's date property passes 6,000 trials per engine with identical traces.

This changes what compiled programs compute and print in a terminal; nothing on screen changed, so the evidence is program output and test results.

Checked

  • A seeded demo program built natively, run twice and compared with OTP 29.1.2: Both runs print the same numbers, equal to OTP's output line for line
  • rand_regression: every algorithm and seed form against values generated on OTP 29: Passes 10 of 10 runs natively and 10 of 10 on BEAM, with the math and float fixtures
  • Cowlib 2.22.0 prop_parse_date with native PropEr, three seeds run twice: 6,000 of 6,000 trials per engine; native and BEAM traces identical
  • Full local gate: clippy, Rust tests, allocator check, end-to-end suite: 2,142 Rust tests and 471 of 473 end-to-end tests pass; two Onecached tests skip as before
  • Cost on a trivial program and on draws: About 0.1 s more build time and 0.57 MB more executable; uniform(100) 145 ns instead of 24 ns
How it was checked

cargo build --release --locked, then cargo clippy --workspace --all-targets --locked -- -D warnings

cargo test --workspace --locked --no-fail-fast (2,142 passed)

python3 scripts/test-allocator-config.py

./target/release/nera test -j 1 --timeout 30 (471 passed, 2 skipped, 0 failed)

nera test rand_regression math_exp_log_pow float_integer_large rand_uniform, ten times in a row, and the same four modules on OTP 29 ten times

python3 tests/integration/cowlib/native_date.py --pin 2.22.0 with the pinned PropEr and OTP 27.3 rand and ordsets sources

After bringing the branch up to date with the atom-quoting fix on main: rebuild, clippy, the runtime tests (755 passed) and the rand, math, float, sqrt and atom-quoting fixtures pass

Risk

Risk above low: Blast radius, medium — Every program compiles the rand module; four float conversions and three math functions change; Behavior, medium — New rand functions are opt-in; uniform/1 now uses OTP's generator and is slower; Novelty, medium — A 2,188-line module ported from OTP beside the existing stdlib

Full risk assessment
Dimension Level Why
Blast radius medium Every program compiles the rand module; four float conversions and three math functions change
Behavior medium New rand functions are opt-in; uniform/1 now uses OTP's generator and is slower
Undo low Revert and done; no saved data or settings move
Evidence low Seeded demo and Cowlib's date property shown matching OTP end to end
Sensitivity low No sign-ins, keys, saved data or releases
Novelty medium A 2,188-line module ported from OTP beside the existing stdlib
Dependencies low No new or updated packages
Not done

A normal draw takes about 2 µs natively (37 ns on BEAM): each draw rebuilds a 256-entry table because Nera has no constant literal storage.

Every program compiles rand, about 0.1 s and 0.57 MB more, even when it never calls it.

@github-actions github-actions Bot added this to the 0.1.1 milestone Nov 12, 2026
trunc/1, round/1, ceil/1 and floor/1 cast the float to a 64-bit integer
first, so any float at or beyond 2^63 in magnitude became 2^63 - 1 or
-2^63 instead of the exact integer OTP returns. rand's exs64, exsplus and
exs1024 algorithms truncate such floats for ranges above 2^58 or 2^64.

The float_integer_large fixture checks the four conversions against OTP 29
through direct, apply and remote fun calls.
rand:uniform/1 was a runtime built-in drawing from an unseedable
generator, and no other rand function existed, so seeded programs such as
PropEr runs drew different numbers on every run.

erlang/stdlib/rand.erl is now OTP 29's rand module ported to Nera: seed/1,2,
seed_s/1,2, export_seed/0 and export_seed_s/1, uniform/0,1 and
uniform_s/1,2, uniform_real, bytes, normal, shuffle, jump and the mwc59 and
splitmix64 utilities, for every OTP algorithm. A seed gives the same
numbers as on OTP 29. The built-in is removed so calls reach the module.

The module needs math:exp/1, math:log/1 and math:pow/2, now runtime
built-ins that raise badarg and badarith as OTP does.

rand_regression compares seeded draws, states, jumps, shuffles and error
reasons for every algorithm and seed form with values generated on OTP 29
by scripts/generate-rand-regression.escript. math_exp_log_pow covers the
math functions. Both run natively and on BEAM.
FEATURES.md lists the rand functions and the new math functions, the
changelog records the change and its build, size and speed cost, and the
test index lists the three new fixtures.

Cowlib 2.22.0's seeded date property was blocked natively because
rand:seed/2 did not control the rand:uniform/1 built-in. With the built-in
gone it passes 6,000 trials per engine with identical traces, so the
compatibility table, known limits and Cowlib notes say so and give the
reproduction command.
@mremond
mremond merged commit 7b32bcf into main Nov 12, 2026
21 checks passed
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.

1 participant