#!/usr/bin/env perl
use strict;
use warnings;
use v5.16;
use Test::More;
################################################################################
# 2025-10-23: Pure Perl implementation of PRNG splitmix64 PRNG
# Scott Baker / https://www.perturb.org/
################################################################################
# A lot of work was done here to mimic how C handles overflow multiplication
# on large uint64_t numbers. Perl converts scalars that are larger than 2^64 - 1
# to floating point on the backend. We do *NOT* want that, because splitmix
# (and most PRNGs) rely on overflow math to do their magic. We utilize
# 'use integer' to force Perl to do all math with integer 64bit values. When
# overflow occurs Perl likes to convert those values to signed numbers. In
# the original C all math is done with uint64_t, so we have to convert the
# IV/signed numbers back into UV/unsigned (positive) values.
################################################################################
#uint64_t splitmix64::rand64() {
# uint64_t z;
#
# z = (x += 0x9e3779b97f4a7c15);
# z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
# z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
#
# return z ^ (z >> 31);
#}
################################################################################
use Getopt::Long;
my $seed = [11]; # Default 64bit seed
my $random_seed = 0;
GetOptions(
'seed=i' => \$seed->[0],
'random_seed' => \$random_seed,
'unit-tests' => \&run_unit_tests,
);
my $iters = int($ARGV[0] || 8);
if ($random_seed) {
print color
('yellow', "Using random seed\n");
$seed->[0] = perl_rand64();
}
print color
(123, "Using seed: $seed->[0]\n\n");
for my $x (1 .. $iters) {
my $num1 = splitmix64_perl($seed);
printf("%2d: %20u\n", $x, $num1);
}
###############################################################################
#my $seed = [10293820198];
#my $num = splitmix_64_perl($seed);
sub splitmix64_perl {
# Seed must be passed as a array reference so we can update it
my $seed = $_[0];
use integer;
# We bitwise or with zero to convert a signed int (IV) to an unsigned int (UV)
# This is a weird hack that mauke taught me. It works so *shrug*
my $z = ($seed->[0] += 11400714819323198485) | 0;
$seed->[0] |= 0;
$z = shift_xor_multiply($z, 30, 13787848793156543929);
$z = shift_xor_multiply($z, 27, 10723151780598845931);
$z = ($z ^ ($z >> 31));
}
# Splitmix does a lot of bitshifting, xoring, and multiplying so we
# create one function to simplify that. We utilize `use integer` to
# make sure all math is done using integers and preserve the rollover
sub shift_xor_multiply {
my ($x, $shift, $mult) = @_;
# This needs to be done with `no integer`
$x = ($x ^ ($x >> $shift));
# Use integer math for the overflow
use integer;
$x = ($x * $mult) | 0;
}
#################################################################################
## Alternate single function for copy and paste (no other function dependencies #
#################################################################################
#my $seed = [10293820198];
#my $num = splitmix_64_perl_single($seed);
sub splitmix64_perl_single {
# Seed must be passed as a array reference so we can update it
my $seed = $_[0];
use integer;
# We bitwise or with zero to convert a signed int (IV) to an unsigned int (UV)
# This is a weird hack that mauke taught me. It works so *shrug*
my $z = ($seed->[0] += 11400714819323198485) | 0;
$seed->[0] |= 0;
$z = ($z ^ ($z >> 30));
use integer;
$z = ($z * 13787848793156543929) | 0;
$z = ($z ^ ($z >> 27));
use integer;
$z = ($z * 10723151780598845931) | 0;
$z = ($z ^ ($z >> 31));
}
###############################################################################
###############################################################################
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
my ($str, $txt) = @_;
# If we're NOT connected to a an interactive terminal don't do color
if (-t
STDOUT == 0) { return $txt || ""; }
# No string sent in, so we just reset
# Some predefined colors
my %color_map = qw(red
160 blue
27 green
34 yellow
226 orange
214 purple
93 white
15 black
0);
$str =~ s|([A
-Za
-z
]+)|$color_map{$1} // $1|eg
;
# Get foreground/background and any commands
my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
my ($bc) = $str =~ /on_(\d{1,3})$/g;
if (defined($fc) && int($fc) > 255) { $fc = undef; } # above 255 is invalid
# Some predefined commands
my %cmd_map = qw(bold
1 italic
3 underline
4 blink
5 inverse
7);
my $cmd_num = $cmd_map{$cmd // 0};
my $ret = '';
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
if (defined($txt)) { $ret .= $txt . "\e[0m"; }
}
# Run a test with a given seed and return a string of the results
sub quick_test {
my $seed = [$_[0]];
my @data = ();
for (my $i = 0; $i < 4; $i++) {
my $num = splitmix64_perl($seed);
}
my $ret = join(", ", @data);
}
sub run_unit_tests {
# Seeds < 2**32
cmp_ok(quick_test(11) , 'eq', '5833679380957638813, 4839782808629744545, 11769803791402734189, 9308485889748266480');
cmp_ok(quick_test(22) , 'eq', '14415425345905102346, 17264975761475716686, 1412077619021228083, 12404402112097020482');
cmp_ok(quick_test(100) , 'eq', '2532601429470541124, 269152572843532260, 4491231873834608077, 4673566422923057776');
cmp_ok(quick_test(123456789), 'eq', '2466975172287755897, 8832083440362974766, 3534771765162737125, 9592110948284743397');
cmp_ok(quick_test(9999) , 'eq', '6117204470161645077, 15966700211956150513, 15034308290212886683, 7774926710803868520');
# Seeds > 2**32
cmp_ok(quick_test(7774926710803868520) , 'eq', '9605346004387840742, 17435495358832388828, 12684084655726398219, 9795402745067826113');
cmp_ok(quick_test(9795402745067826113) , 'eq', '13110559830617540027, 13626988459271143897, 846014752197971904, 13956522239222304255');
cmp_ok(quick_test(846014752197971904) , 'eq', '17051223190671778754, 12943043929365758946, 17796463379074244041, 16028253299916138813');
cmp_ok(quick_test(12943043929365758946) , 'eq', '13152169664619309884, 10188724118650338133, 13259243310153093243, 12185650234802439251');
cmp_ok(quick_test(16028253299916138813) , 'eq', '17201533047954400773, 3347092783829409799, 2118253649191891459, 15494166571380546778');
done_testing();
}
sub perl_rand64 {
my $high = int(rand() * (2**32-1));
my $ret = ($high << 32) | $low;
}
sub get_64bit_seed {
open my $urandom, '<:raw', '/dev/urandom' or croak
("Couldn't open /dev/urandom: $!");
sysread($urandom, my $buf, 8) or croak
("Couldn't read from csprng: $!");
}
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
*k = sub { Data::Dump::Color::dd(@_) };
} else {
*k = sub { print Data
::Dumper::Dumper(\
@_) };
}
sub kd {
k(@_);
}
}
# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4