Skip to content

math.random does not follow Lua 5.4's xoshiro256** PRNG #4

@pgundlach

Description

@pgundlach

go-lua ships with the math library bound to Go's math/rand (v1), which uses
a Lagged-Fibonacci generator. Reference Lua 5.4 specifies xoshiro256** as the
PRNG backing math.random / math.randomseed. The two algorithms produce
different output streams for the same seed, and Lua 5.4 also exposes a richer
math.randomseed API that go-lua does not implement.

The PUC-Rio Lua 5.4 test suite (lua-tests/math.lua) carries 39 lines of
-- SKIP (go-lua uses different PRNG): markers around the random-related
sections that depend on these differences.

What the SKIPs cover

Block A — bit-exact stream after a known seed (~lua-tests/math.lua:815-841)

math.randomseed(1007)
-- the first call after seed 1007 should return 0x7a7040a5a323c9d6 in Lua 5.4
assert(math.random(0) == 0x7a7040a5a323c9d6)

This is impossible to satisfy with Go's math/rand (v1) — different algorithm,
different bits. Requires implementing xoshiro256** to pass.

Block B — Lua 5.4 randomseed() 0-arg and 2-arg signatures (~lua-tests/math.lua:843-853)

local x, y = math.randomseed()        -- no args: pick a random seed, return it
local res = math.random(0)
math.randomseed(x, y)                  -- restore from two-word state
assert(math.random(0) == res)          -- should reproduce the stream

go-lua's current math.randomseed (math.go:~245) takes exactly one argument
and returns nothing. Lua 5.4 accepts 0/1/2 args and returns the two 64-bit
state words that seeded the generator.

Suggested fix

Implement xoshiro256** as the backing PRNG and extend the math.randomseed
binding. Rough sketch:

type xoshiro256ss struct{ s [4]uint64 }

func (x *xoshiro256ss) Uint64() uint64 {
    result := bits.RotateLeft64(x.s[1]*5, 7) * 9
    t := x.s[1] << 17
    x.s[2] ^= x.s[0]; x.s[3] ^= x.s[1]
    x.s[1] ^= x.s[2]; x.s[0] ^= x.s[3]
    x.s[2] ^= t
    x.s[3] = bits.RotateLeft64(x.s[3], 45)
    return result
}

Plus a splitmix64-based seed-expansion routine that mirrors Lua 5.4's
setseed exactly (see lmathlib.c setseed in upstream Lua 5.4 source),
so that randomseed(1007) produces the spec-mandated 0x7a7040a5a323c9d6.

The two-word state exposed by randomseed() corresponds to two of the four
xoshiro256 state words (Lua 5.4 returns s[0] and s[1]).

What this would enable

  • Drop all 39 -- SKIP (go-lua uses different PRNG): markers in
    lua-tests/math.lua and run those tests for real coverage.
  • Scripts that move between PUC-Rio Lua and go-lua get bit-identical
    math.random behaviour.
  • Lua 5.4's standard math.randomseed() 0-arg / 2-arg signatures work,
    which is what scripts that need to snapshot-and-restore PRNG state expect.

Out of scope for this issue

The regression where math.randomseed(N) was a no-op against Go 1.20+'s
autoseeded global source was a separate functional bug — fixed in commit
f694f4c ("math: route random/randomseed through a package-local
*rand.Rand"). This issue is purely about algorithm and API conformance,
which is independent of that fix.

Related

  • Lua 5.4 reference manual: §6.7 Mathematical Functions (math.random /
    math.randomseed).
  • Upstream PUC-Rio Lua source: src/lmathlib.c — the setseed,
    nextrand, and randomseed C functions are the reference implementation.
  • xoshiro256** reference C code: https://prng.di.unimi.it/xoshiro256starstar.c
  • Existing skip markers: grep -n "SKIP (go-lua uses different PRNG)" lua-tests/math.lua

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions