Files
mdg-filius/packages/core/src/runtime/random.ts
T
julian.kiedaischandClaude Opus 5.5 37f5d77477 test(core): RIP measured with Java FILIUS
RipRun runs rip-chain's three routers in Java FILIUS, each router's random
set by reflection to the core's ripSeed, and writes the frames, every
change of each RIP table and the calls. converge: the tables fill, a ping
at 45 s. expire: R3 falls silent at 10 s; R2 marks its network 16 hops only
at its own next beacon, R1 passes it on at once, the ping gets ICMP 3/0.
Both match: per sending card the same messages at the same times, every
table through the same states. The first 5 s are compared by the state the
tables reach, since Java's routers start one after the other.

Measured on the JDK: Java's pause divides nextFloat() by 3 in float; the
core does too now (a millisecond off in one draw of about 10,000 before).
Scenarios name their RIP routers in the new field rip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:41:56 +00:00

68 lines
2.2 KiB
TypeScript

// Ported from filius-ipad (GPLv2/GPLv3):
// State/TopologyNetworkRuntimeEngine.swift (TopologyJavaCompatibleRandom)
// A bit-exact java.util.Random: the 48-bit linear congruential generator
// Java FILIUS draws its RIP beacon jitter and ephemeral ports from. BigInt
// keeps the 48-bit arithmetic exact; a double would lose the low bits.
const MULTIPLIER = 0x5deece66dn;
const ADDEND = 0xbn;
const MASK = (1n << 48n) - 1n;
const INT32_MAX = 2147483647n;
export class JavaCompatibleRandom {
private state: bigint;
constructor(seed: bigint | number) {
this.state = (BigInt.asUintN(64, BigInt(seed)) ^ MULTIPLIER) & MASK;
}
/** The internal 48-bit state, for tests and for copying a generator. */
get seed(): bigint {
return this.state;
}
clone(): JavaCompatibleRandom {
const copy = new JavaCompatibleRandom(0);
copy.state = this.state;
return copy;
}
next(bits: number): number {
if (!(bits > 0 && bits <= 32)) throw new RangeError(`bits out of range: ${bits}`);
this.state = (this.state * MULTIPLIER + ADDEND) & MASK;
return Number(this.state >> BigInt(48 - bits));
}
nextInt(upperBound: number): number {
if (!(upperBound > 0)) throw new RangeError(`upperBound must be positive: ${upperBound}`);
const bound = BigInt(upperBound);
if ((upperBound & -upperBound) === upperBound) {
return Number((bound * BigInt(this.next(31))) >> 31n);
}
for (;;) {
const bits = BigInt(this.next(31));
const value = bits % bound;
if (bits - value + (bound - 1n) <= INT32_MAX) return Number(value);
}
}
nextFloat(): number {
return Math.fround(this.next(24) / (1 << 24));
}
/** Java's nextDouble (what Math.random draws): 53 random bits over 2^53. */
nextDouble(): number {
return (this.next(26) * 2 ** 27 + this.next(27)) / 2 ** 53;
}
/**
* Java RIP: 30 s · (f/3 + 0.84), truncated — somewhere between 25.2 s and
* 35.2 s. f/3 divides a float by an int, in float (JLS 5.6.2): one draw in
* about 10,000 comes out a millisecond apart from dividing in double
* (measured on OpenJDK 21).
*/
nextRipJitterMilliseconds(): number {
return Math.trunc(30_000 * (Math.fround(this.nextFloat() / 3) + 0.84));
}
}