From beb552eb947198f993a0cba9f248441325f6ee57 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Fri, 7 Nov 2025 14:52:53 +0100 Subject: [PATCH 1/2] Add Random#split and #split_internal API - #split: splits the instance by allocating a new instance - #split_internal: splits the instance using an allocated instance, for example with ReferenceStorage(T) --- spec/std/random/pcg32_spec.cr | 34 ++++++++++++++++++++++++++++++++++ spec/std/random_spec.cr | 4 ++++ src/random.cr | 28 ++++++++++++++++++++++++++++ src/random/pcg32.cr | 11 +++++++++++ 4 files changed, 77 insertions(+) diff --git a/spec/std/random/pcg32_spec.cr b/spec/std/random/pcg32_spec.cr index a231749716fd..dedbd972f987 100644 --- a/spec/std/random/pcg32_spec.cr +++ b/spec/std/random/pcg32_spec.cr @@ -232,6 +232,40 @@ describe "Random::PCG32" do m1.next_u.should eq m2.next_u end + it "#split" do + rng0 = Random::PCG32.new + rng1 = rng0.split + rng2 = rng0.split + rng3 = rng1.split # split of split + + seq0 = 5.times.map { rng0.next_u }.to_a + seq1 = 5.times.map { rng1.next_u }.to_a + seq2 = 5.times.map { rng2.next_u }.to_a + seq3 = 5.times.map { rng3.next_u }.to_a + + seq1.should_not eq(seq0) + seq1.should_not eq(seq2) + seq1.should_not eq(seq3) + seq2.should_not eq(seq0) + seq2.should_not eq(seq3) + seq3.should_not eq(seq0) + end + + it "#split_internal" do + rng0 = Random::PCG32.new(123_u64, 456_u64) + rng1 = + {% if compare_versions(Crystal::VERSION, "1.12.0") >= 0 %} + buf = uninitialized ReferenceStorage(Random::PCG32) + Random::PCG32.unsafe_construct(pointerof(buf), rng0) + buf.to_reference + {% else %} + rng0.dup + {% end %} + rng0.split_internal(rng1) + rng0.next_u.should eq(3152259133_u64) + rng1.next_u.should eq(2489095755_u64) + end + it "can be initialized without explicit seed" do Random::PCG32.new.should be_a Random::PCG32 end diff --git a/spec/std/random_spec.cr b/spec/std/random_spec.cr index 9e599612fc70..69afba8ca264 100644 --- a/spec/std/random_spec.cr +++ b/spec/std/random_spec.cr @@ -362,4 +362,8 @@ describe "Random" do typeof(array).should eq(StaticArray({{type}}, 4)) {% end %} end + + it "fails to split" do + expect_raises(NotImplementedError) { TestRNG(Int32).new([0]).split } + end end diff --git a/src/random.cr b/src/random.cr index 35adcaf30a5a..104a735b53be 100644 --- a/src/random.cr +++ b/src/random.cr @@ -68,6 +68,34 @@ module Random # the maximal value for the chosen type. abstract def next_u + # Splits the current instance into two seemingly independent instances that + # will return distinct sequences of random numbers. Returns a new instance. + # + # ``` + # random = Random.new + # split1 = random.split + # split2 = random.split + # + # 5.times.map { random.rand(99) }.to_a # => [79, 42, 54, 17, 52] + # 5.times.map { split1.rand(99) }.to_a # => [90, 37, 15, 74, 61] + # 5.times.map { split2.rand(99) }.to_a # => [6, 87, 5, 73, 71] + # ``` + def split : self + copy = dup + split_internal(copy) + copy + end + + # The internal implementation for `#split` where *self* is the original + # instance and *other* the duplicated instance to be returned. + # + # The default `Random` implementation in stdlib is splittable, but not every + # PRNG algorithm is splittable, so the method raises a `NotImplementedError` + # exception by default. + def split_internal(other : self) : Nil + raise NotImplementedError.new("{{@type}}#split") + end + # Generates a random `Bool`. # # ``` diff --git a/src/random/pcg32.cr b/src/random/pcg32.cr index bc34f736b282..2c86bb836ee5 100644 --- a/src/random/pcg32.cr +++ b/src/random/pcg32.cr @@ -43,6 +43,12 @@ class Random::PCG32 new(Random::Secure.rand(UInt64::MIN..UInt64::MAX), Random::Secure.rand(UInt64::MIN..UInt64::MAX)) end + # :nodoc: + def initialize(other : self) + @state = other.@state + @inc = other.@inc + end + def initialize(initstate : UInt64, initseq = 0_u64) # initialize to zeros to prevent compiler complains @state = 0_u64 @@ -87,4 +93,9 @@ class Random::PCG32 end @state = acc_mult &* @state &+ acc_plus end + + def split_internal(other : self) : Nil + @inc = ((@inc &+ 1) << 1) | 1 + other.next_u + end end From 6053151308102522a20a5eb5349de7925ccbd1fb Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Fri, 7 Nov 2025 18:32:02 +0100 Subject: [PATCH 2/2] Fix: not implemented error message --- spec/std/random_spec.cr | 4 +++- src/random.cr | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/std/random_spec.cr b/spec/std/random_spec.cr index 69afba8ca264..a550c591b34a 100644 --- a/spec/std/random_spec.cr +++ b/spec/std/random_spec.cr @@ -364,6 +364,8 @@ describe "Random" do end it "fails to split" do - expect_raises(NotImplementedError) { TestRNG(Int32).new([0]).split } + expect_raises(NotImplementedError, "TestRNG(Int32)#split") do + TestRNG(Int32).new([0]).split + end end end diff --git a/src/random.cr b/src/random.cr index 104a735b53be..73a72b00a3d6 100644 --- a/src/random.cr +++ b/src/random.cr @@ -93,7 +93,7 @@ module Random # PRNG algorithm is splittable, so the method raises a `NotImplementedError` # exception by default. def split_internal(other : self) : Nil - raise NotImplementedError.new("{{@type}}#split") + raise NotImplementedError.new("#{self.class}#split") end # Generates a random `Bool`.