Current section
Files
Jump to
Current section
Files
src/metamon@generator@seed.erl
-module(metamon@generator@seed).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/metamon/generator/seed.gleam").
-export([state/1, original_input/1, next_int/1, next_int_in/3, split/1, seed/1, random_seed/0]).
-export_type([seed/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Deterministic pseudo-random seed.\n"
"\n"
" The implementation is the 32-bit \"Marsaglia xorshift\" PRNG. It\n"
" uses only shifts and xors — no multiplication — so BEAM (bignum\n"
" integers) and JavaScript (53-bit safe doubles) produce\n"
" bit-identical streams. An LCG-style multiplier would overflow\n"
" JavaScript's double precision well before the 32-bit mask and\n"
" cause subtle distribution drift.\n"
"\n"
" Statistical quality is not the goal here — determinism and\n"
" portability are. Property-based testing is well-served even by\n"
" modest PRNGs because the failure search is shrinker-driven, not\n"
" statistical.\n"
).
-opaque seed() :: {seed, integer(), gleam@option:option(integer())}.
-file("src/metamon/generator/seed.gleam", 59).
?DOC(
" The raw integer state. Used by the regression-file format to\n"
" serialise a seed into a reproduction key.\n"
).
-spec state(seed()) -> integer().
state(S) ->
erlang:element(2, S).
-file("src/metamon/generator/seed.gleam", 69).
?DOC(
" The integer the user passed to `seed/1`, if any.\n"
"\n"
" `None` for seeds derived from the system clock, from `next_int` /\n"
" `next_int_in` advancement, or from `split`. `Some(n)` for seeds\n"
" constructed via `seed(n)`. Failure reports compare this against\n"
" `state/1` to decide whether to annotate \"originally seed(n)\".\n"
).
-spec original_input(seed()) -> gleam@option:option(integer()).
original_input(S) ->
erlang:element(3, S).
-file("src/metamon/generator/seed.gleam", 119).
-spec step(integer()) -> integer().
step(State) ->
After_first_shift = erlang:'bxor'(
State,
erlang:'band'(erlang:'bsl'(State, 13), 16#FFFFFFFF)
),
After_second_shift = erlang:'bxor'(
After_first_shift,
erlang:'bsr'(After_first_shift, 17)
),
After_third_shift = erlang:'bxor'(
After_second_shift,
erlang:'band'(erlang:'bsl'(After_second_shift, 5), 16#FFFFFFFF)
),
erlang:'band'(After_third_shift, 16#FFFFFFFF).
-file("src/metamon/generator/seed.gleam", 75).
?DOC(
" Advance the seed once and return the next non-negative integer\n"
" alongside the advanced seed.\n"
).
-spec next_int(seed()) -> {integer(), seed()}.
next_int(S) ->
Next_state = step(erlang:element(2, S)),
{Next_state, {seed, Next_state, none}}.
-file("src/metamon/generator/seed.gleam", 85).
?DOC(
" Return an integer uniformly in the closed interval `[lo, hi]`.\n"
"\n"
" If `lo > hi` the bounds are swapped. If `lo == hi` the bound is\n"
" returned without consuming randomness (the seed is still advanced\n"
" for determinism).\n"
).
-spec next_int_in(seed(), integer(), integer()) -> {integer(), seed()}.
next_int_in(S, Lo, Hi) ->
{Low, High} = case Lo > Hi of
true ->
{Hi, Lo};
false ->
{Lo, Hi}
end,
case Low =:= High of
true ->
{_, S2} = next_int(S),
{Low, S2};
false ->
{Raw, S2@1} = next_int(S),
Span = (High - Low) + 1,
{Low + (case Span of
0 -> 0;
Gleam@denominator -> Raw rem Gleam@denominator
end), S2@1}
end.
-file("src/metamon/generator/seed.gleam", 108).
?DOC(
" Split a seed into two statistically independent seeds. The\n"
" implementation derives the second seed by xor-ing the first with a\n"
" constant before stepping, which on a 32-bit LCG produces a stream\n"
" that does not align with the original — sufficient for shrinking\n"
" independent generator components without correlation artefacts.\n"
).
-spec split(seed()) -> {seed(), seed()}.
split(S) ->
{Left_state, _} = next_int(S),
Xored = erlang:'band'(erlang:'bxor'(Left_state, 16#A5A5A5A5), 16#FFFFFFFF),
{Right_state, _} = next_int({seed, Xored, none}),
{{seed, Left_state, none}, {seed, Right_state, none}}.
-file("src/metamon/generator/seed.gleam", 141).
-spec normalise(integer()) -> integer().
normalise(Value) ->
Positive = case Value < 0 of
true ->
0 - Value;
false ->
Value
end,
Masked = erlang:'band'(Positive, 16#FFFFFFFF),
case Masked of
0 ->
16#DEADBEEF;
N ->
N
end.
-file("src/metamon/generator/seed.gleam", 47).
?DOC(
" Construct a seed from an integer.\n"
"\n"
" The integer is masked to a 32-bit non-negative window so the\n"
" stream stays target-portable. A masked-to-zero value is silently\n"
" replaced with `0xDEADBEEF` because the xorshift family has `0` as\n"
" a fixed point — emitting it would degenerate the stream.\n"
"\n"
" Both normalisation steps mean `seed(0)`, `seed(0x100000000)` (= 2^32),\n"
" and any other `value` whose 32-bit-masked form is `0` collapse to\n"
" the same canonical state. Failure reports annotate the canonical\n"
" state with the original input when normalisation kicked in (see\n"
" `original_input/1`).\n"
).
-spec seed(integer()) -> seed().
seed(Value) ->
{seed, normalise(Value), {some, Value}}.
-file("src/metamon/generator/seed.gleam", 53).
?DOC(
" Construct a seed from the system clock. Useful for ad-hoc local\n"
" runs; CI should pin a value via `metamon.with_seed(metamon.seed(_))`.\n"
).
-spec random_seed() -> seed().
random_seed() ->
{seed, normalise(metamon_ffi:now_microseconds()), none}.