Skip to content

Commit 6225db5

Browse files
IMax153mikearnaldi
authored andcommitted
feat(@tsplus/stdlib): update Random implementation
1 parent 6411fc9 commit 6225db5

20 files changed

+892
-65
lines changed

.changeset/wet-carpets-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tsplus/stdlib": patch
3+
---
4+
5+
update Random implementation

packages/stdlib/_src/global.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ import { Exception } from "@tsplus/stdlib/exceptions/Exception"
149149
* @tsplus global
150150
*/
151151
import { Eval } from "@tsplus/stdlib/io/Eval/definition"
152+
/**
153+
* @tsplus global
154+
*/
155+
import { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt"
156+
/**
157+
* @tsplus global
158+
*/
159+
import { MutableRandom } from "@tsplus/stdlib/io/Random/MutableRandom"
160+
/**
161+
* @tsplus global
162+
*/
163+
import { PCGRandom } from "@tsplus/stdlib/io/Random/PCGRandom"
152164
/**
153165
* @tsplus global
154166
*/
@@ -435,10 +447,6 @@ import {
435447
* @tsplus global
436448
*/
437449
import { lazy } from "@tsplus/stdlib/utilities/Lazy"
438-
/**
439-
* @tsplus global
440-
*/
441-
import { RandomPCG } from "@tsplus/stdlib/utilities/RandomPCG"
442450
/**
443451
* @tsplus global
444452
*/

packages/stdlib/_src/io.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * as eval from "@tsplus/stdlib/io/Eval"
2+
export * as random from "@tsplus/stdlib/io/Random"

packages/stdlib/_src/io/Random.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// codegen:start {preset: barrel, include: ./Random/*.ts, prefix: "@tsplus/stdlib/io"}
2+
export * from "@tsplus/stdlib/io/Random/Distribution"
3+
export * from "@tsplus/stdlib/io/Random/Generator"
4+
export * from "@tsplus/stdlib/io/Random/MutableRandom"
5+
export * from "@tsplus/stdlib/io/Random/PCGRandom"
6+
// codegen:end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// codegen:start {preset: barrel, include: ./Distribution/*.ts, prefix: "@tsplus/stdlib/io/Random"}
2+
export * from "@tsplus/stdlib/io/Random/Distribution/definition"
3+
export * from "@tsplus/stdlib/io/Random/Distribution/operations"
4+
// codegen:end
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/**
2+
* An ArrayInt represents an integer larger than what can be represented in
3+
* classical JavaScript.
4+
*
5+
* The values stored in data must be in the range `[0, 0xffffffff]`.
6+
*
7+
* ```ts
8+
* { sign: 1, data: [ 42 ] } // = 42
9+
* { sign: -1, data: [ 42 ] } // = -42
10+
* { sign: -1, data: [ 5, 42 ] } // = -1 * (5 * 2**32 + 42)
11+
* { sign: -1, data: [ 1, 5, 42 ] } // = -1 * (1 * 2**64 + 5 * 2**32 + 42)
12+
* ```
13+
*/
14+
export interface ArrayInt {
15+
/**
16+
* Sign of the represented number
17+
*/
18+
sign: -1 | 1
19+
/**
20+
* Value of the number, must only contain numbers in the range [0, 0xffffffff]
21+
*/
22+
data: number[]
23+
}
24+
25+
/**
26+
* Add two ArrayInt
27+
* @internal
28+
*/
29+
export function addArrayIntToNew(arrayIntA: ArrayInt, arrayIntB: ArrayInt): ArrayInt {
30+
if (arrayIntA.sign !== arrayIntB.sign) {
31+
return substractArrayIntToNew(arrayIntA, {
32+
sign: -arrayIntB.sign as -1 | 1,
33+
data: arrayIntB.data
34+
})
35+
}
36+
const data = []
37+
let reminder = 0
38+
const dataA = arrayIntA.data
39+
const dataB = arrayIntB.data
40+
for (let indexA = dataA.length - 1, indexB = dataB.length - 1; indexA >= 0 || indexB >= 0; --indexA, --indexB) {
41+
const vA = indexA >= 0 ? dataA[indexA]! : 0
42+
const vB = indexB >= 0 ? dataB[indexB]! : 0
43+
const current = vA + vB + reminder
44+
data.push(current >>> 0)
45+
reminder = ~~(current / 0x100000000)
46+
}
47+
if (reminder !== 0) {
48+
data.push(reminder)
49+
}
50+
return { sign: arrayIntA.sign, data: data.reverse() }
51+
}
52+
53+
/**
54+
* Add one to a given positive ArrayInt
55+
* @internal
56+
*/
57+
export function addOneToPositiveArrayInt(arrayInt: ArrayInt): ArrayInt {
58+
arrayInt.sign = 1 // handling case { sign: -1, data: [0,...,0] }
59+
const data = arrayInt.data
60+
for (let index = data.length - 1; index >= 0; --index) {
61+
if (data[index] === 0xffffffff) {
62+
data[index] = 0
63+
} else {
64+
data[index] += 1
65+
return arrayInt
66+
}
67+
}
68+
data.unshift(1)
69+
return arrayInt
70+
}
71+
72+
/** @internal */
73+
function isStrictlySmaller(dataA: number[], dataB: number[]): boolean {
74+
const maxLength = Math.max(dataA.length, dataB.length)
75+
for (let index = 0; index < maxLength; ++index) {
76+
const indexA = index + dataA.length - maxLength
77+
const indexB = index + dataB.length - maxLength
78+
const vA = indexA >= 0 ? dataA[indexA]! : 0
79+
const vB = indexB >= 0 ? dataB[indexB]! : 0
80+
if (vA < vB) return true
81+
if (vA > vB) return false
82+
}
83+
return false
84+
}
85+
86+
/**
87+
* Substract two ArrayInt
88+
* @internal
89+
*/
90+
export function substractArrayIntToNew(arrayIntA: ArrayInt, arrayIntB: ArrayInt): ArrayInt {
91+
if (arrayIntA.sign !== arrayIntB.sign) {
92+
return addArrayIntToNew(arrayIntA, { sign: -arrayIntB.sign as -1 | 1, data: arrayIntB.data })
93+
}
94+
const dataA = arrayIntA.data
95+
const dataB = arrayIntB.data
96+
if (isStrictlySmaller(dataA, dataB)) {
97+
const out = substractArrayIntToNew(arrayIntB, arrayIntA)
98+
out.sign = -out.sign as -1 | 1
99+
return out
100+
}
101+
const data = []
102+
let reminder = 0
103+
for (let indexA = dataA.length - 1, indexB = dataB.length - 1; indexA >= 0 || indexB >= 0; --indexA, --indexB) {
104+
const vA = indexA >= 0 ? dataA[indexA]! : 0
105+
const vB = indexB >= 0 ? dataB[indexB]! : 0
106+
const current = vA - vB - reminder
107+
data.push(current >>> 0)
108+
reminder = current < 0 ? 1 : 0
109+
}
110+
return { sign: arrayIntA.sign, data: data.reverse() }
111+
}
112+
113+
/**
114+
* Trim uneeded zeros in ArrayInt
115+
* and uniform notation for zero: {sign: 1, data: [0]}
116+
*/
117+
export function trimArrayIntInplace(arrayInt: ArrayInt) {
118+
const data = arrayInt.data
119+
let firstNonZero = 0
120+
// eslint-disable-next-line no-empty
121+
for (; firstNonZero !== data.length && data[firstNonZero] === 0; ++firstNonZero) {}
122+
if (firstNonZero === data.length) {
123+
// only zeros
124+
arrayInt.sign = 1
125+
arrayInt.data = [0]
126+
return arrayInt
127+
}
128+
data.splice(0, firstNonZero)
129+
return arrayInt
130+
}
131+
132+
// Helpers specific to 64 bits versions
133+
134+
/** @internal */
135+
export type ArrayInt64 = Required<ArrayInt> & { data: [number, number] }
136+
137+
/**
138+
* We only accept safe integers here
139+
* @internal
140+
*/
141+
export function fromNumberToArrayInt64(out: ArrayInt64, n: number): ArrayInt64 {
142+
if (n < 0) {
143+
const posN = -n
144+
out.sign = -1
145+
out.data[0] = ~~(posN / 0x100000000)
146+
out.data[1] = posN >>> 0
147+
} else {
148+
out.sign = 1
149+
out.data[0] = ~~(n / 0x100000000)
150+
out.data[1] = n >>> 0
151+
}
152+
return out
153+
}
154+
155+
/**
156+
* Substract two ArrayInt of 64 bits on 64 bits.
157+
* With arrayIntA - arrayIntB >= 0
158+
* @internal
159+
*/
160+
export function substractArrayInt64(out: ArrayInt64, arrayIntA: ArrayInt64, arrayIntB: ArrayInt64): ArrayInt64 {
161+
const lowA = arrayIntA.data[1]
162+
const highA = arrayIntA.data[0]
163+
const signA = arrayIntA.sign
164+
const lowB = arrayIntB.data[1]
165+
const highB = arrayIntB.data[0]
166+
const signB = arrayIntB.sign
167+
168+
// Requirement: arrayIntA - arrayIntB >= 0
169+
out.sign = 1
170+
171+
if (signA === 1 && signB === -1) {
172+
// Operation is a simple sum of arrayIntA + abs(arrayIntB)
173+
const low = lowA + lowB
174+
const high = highA + highB + (low > 0xffffffff ? 1 : 0)
175+
out.data[0] = high >>> 0
176+
out.data[1] = low >>> 0
177+
return out
178+
}
179+
// signA === -1 with signB === 1 is impossible given: arrayIntA - arrayIntB >= 0
180+
181+
// Operation is a substraction
182+
let lowFirst = lowA
183+
let highFirst = highA
184+
let lowSecond = lowB
185+
let highSecond = highB
186+
if (signA === -1) {
187+
lowFirst = lowB
188+
highFirst = highB
189+
lowSecond = lowA
190+
highSecond = highA
191+
}
192+
let reminderLow = 0
193+
let low = lowFirst - lowSecond
194+
if (low < 0) {
195+
reminderLow = 1
196+
low = low >>> 0
197+
}
198+
out.data[0] = highFirst - highSecond - reminderLow
199+
out.data[1] = low
200+
return out
201+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt"
2+
import { uniformIntDistributionInternal } from "@tsplus/stdlib/io/Random/Distribution/_internal/uniformIntDistributionInternal"
3+
import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator"
4+
5+
/**
6+
* Uniformly generate an `ArrayInt` in the range [0, rangeSize].
7+
*
8+
* @remarks
9+
* In the worst case scenario it may discard half of the randomly generated
10+
* values. Worst case being: most significant number is `1` and remaining part
11+
* evaluates to `0`.
12+
*
13+
* @internal
14+
*/
15+
export function uniformArrayIntDistributionInternal(
16+
out: ArrayInt["data"],
17+
rangeSize: ArrayInt["data"],
18+
rng: RandomGenerator
19+
): ArrayInt["data"] {
20+
const rangeLength = rangeSize.length
21+
22+
// We iterate until we find a valid value for arrayInt
23+
// eslint-disable-next-line no-constant-condition
24+
while (true) {
25+
// We compute a new value for arrayInt
26+
for (let index = 0; index !== rangeLength; ++index) {
27+
const indexRangeSize = index === 0 ? rangeSize[0]! + 1 : 0x100000000
28+
out[index] = uniformIntDistributionInternal(indexRangeSize, rng)
29+
}
30+
31+
// If in the correct range we can return it
32+
for (let index = 0; index !== rangeLength; ++index) {
33+
const current = out[index]!
34+
const currentInRange = rangeSize[index]!
35+
if (current < currentInRange) {
36+
return out // arrayInt < rangeSize
37+
} else if (current > currentInRange) {
38+
break // arrayInt > rangeSize
39+
}
40+
}
41+
// Otherwise we need to try another one
42+
}
43+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator"
2+
3+
/**
4+
* Uniformly generate an integer in the range [0, rangeSize].
5+
*
6+
* @internal
7+
*/
8+
export function uniformIntDistributionInternal(rangeSize: number, rng: RandomGenerator): number {
9+
const MinRng = rng.min()
10+
const NumValues = rng.max() - rng.min() + 1
11+
12+
// Range provided by the RandomGenerator is large enough
13+
if (rangeSize <= NumValues) {
14+
const MaxAllowed = NumValues - (NumValues % rangeSize)
15+
// eslint-disable-next-line no-constant-condition
16+
while (true) {
17+
const out = rng.next()
18+
const deltaV = out - MinRng
19+
if (deltaV < MaxAllowed) {
20+
return deltaV % rangeSize // Warning: we expect NumValues <= 2**32, so diff too
21+
}
22+
}
23+
}
24+
25+
// Compute number of iterations required to have enough random
26+
// to build uniform entries in the asked range
27+
let FinalNumValues = NumValues * NumValues
28+
let NumIterations = 2 // At least 2 (at this point in the code)
29+
while (FinalNumValues < rangeSize) {
30+
FinalNumValues *= NumValues
31+
;++NumIterations
32+
}
33+
const MaxAcceptedRandom = rangeSize * Math.floor((1 * FinalNumValues) / rangeSize)
34+
35+
// eslint-disable-next-line no-constant-condition
36+
while (true) {
37+
// Aggregate mutiple calls to next() into a single random value
38+
let value = 0
39+
for (let num = 0; num !== NumIterations; ++num) {
40+
const out = rng.next()
41+
value = NumValues * value + (out - MinRng) // Warning: we overflow may when diff > max_safe (eg: =max_safe-min_safe+1)
42+
}
43+
if (value < MaxAcceptedRandom) {
44+
const inDiff = value - rangeSize * Math.floor((1 * value) / rangeSize)
45+
return inDiff
46+
}
47+
}
48+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator"
2+
3+
/**
4+
* Generate random value based on a given RandomGenerator. Return the generated
5+
* value and an offsetted version of the RandomGenerator.
6+
*
7+
* @tsplus type Random.Distribution
8+
*/
9+
export type Distribution<T> = (rng: RandomGenerator) => T
10+
11+
/**
12+
* @tsplus type Random.Distribution.Ops
13+
*/
14+
export interface DistributionOps {}
15+
export const Distribution: DistributionOps = {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@tsplus/stdlib/io/Random/Distribution"}
2+
export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformArrayIntDistribution"
3+
export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformBigIntDistribution"
4+
export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformIntDistribution"
5+
// codegen:end

0 commit comments

Comments
 (0)