Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-carpets-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tsplus/stdlib": patch
---

update Random implementation
16 changes: 12 additions & 4 deletions packages/stdlib/_src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ import { Exception } from "@tsplus/stdlib/exceptions/Exception"
* @tsplus global
*/
import { Eval } from "@tsplus/stdlib/io/Eval/definition"
/**
* @tsplus global
*/
import { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt"
/**
* @tsplus global
*/
import { MutableRandom } from "@tsplus/stdlib/io/Random/MutableRandom"
/**
* @tsplus global
*/
import { PCGRandom } from "@tsplus/stdlib/io/Random/PCGRandom"
/**
* @tsplus global
*/
Expand Down Expand Up @@ -435,10 +447,6 @@ import {
* @tsplus global
*/
import { lazy } from "@tsplus/stdlib/utilities/Lazy"
/**
* @tsplus global
*/
import { RandomPCG } from "@tsplus/stdlib/utilities/RandomPCG"
/**
* @tsplus global
*/
Expand Down
1 change: 1 addition & 0 deletions packages/stdlib/_src/io.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * as eval from "@tsplus/stdlib/io/Eval"
export * as random from "@tsplus/stdlib/io/Random"
6 changes: 6 additions & 0 deletions packages/stdlib/_src/io/Random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// codegen:start {preset: barrel, include: ./Random/*.ts, prefix: "@tsplus/stdlib/io"}
export * from "@tsplus/stdlib/io/Random/Distribution"
export * from "@tsplus/stdlib/io/Random/Generator"
export * from "@tsplus/stdlib/io/Random/MutableRandom"
export * from "@tsplus/stdlib/io/Random/PCGRandom"
// codegen:end
4 changes: 4 additions & 0 deletions packages/stdlib/_src/io/Random/Distribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// codegen:start {preset: barrel, include: ./Distribution/*.ts, prefix: "@tsplus/stdlib/io/Random"}
export * from "@tsplus/stdlib/io/Random/Distribution/definition"
export * from "@tsplus/stdlib/io/Random/Distribution/operations"
// codegen:end
201 changes: 201 additions & 0 deletions packages/stdlib/_src/io/Random/Distribution/_internal/ArrayInt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* An ArrayInt represents an integer larger than what can be represented in
* classical JavaScript.
*
* The values stored in data must be in the range `[0, 0xffffffff]`.
*
* ```ts
* { sign: 1, data: [ 42 ] } // = 42
* { sign: -1, data: [ 42 ] } // = -42
* { sign: -1, data: [ 5, 42 ] } // = -1 * (5 * 2**32 + 42)
* { sign: -1, data: [ 1, 5, 42 ] } // = -1 * (1 * 2**64 + 5 * 2**32 + 42)
* ```
*/
export interface ArrayInt {
/**
* Sign of the represented number
*/
sign: -1 | 1
/**
* Value of the number, must only contain numbers in the range [0, 0xffffffff]
*/
data: number[]
}

/**
* Add two ArrayInt
* @internal
*/
export function addArrayIntToNew(arrayIntA: ArrayInt, arrayIntB: ArrayInt): ArrayInt {
if (arrayIntA.sign !== arrayIntB.sign) {
return substractArrayIntToNew(arrayIntA, {
sign: -arrayIntB.sign as -1 | 1,
data: arrayIntB.data
})
}
const data = []
let reminder = 0
const dataA = arrayIntA.data
const dataB = arrayIntB.data
for (let indexA = dataA.length - 1, indexB = dataB.length - 1; indexA >= 0 || indexB >= 0; --indexA, --indexB) {
const vA = indexA >= 0 ? dataA[indexA]! : 0
const vB = indexB >= 0 ? dataB[indexB]! : 0
const current = vA + vB + reminder
data.push(current >>> 0)
reminder = ~~(current / 0x100000000)
}
if (reminder !== 0) {
data.push(reminder)
}
return { sign: arrayIntA.sign, data: data.reverse() }
}

/**
* Add one to a given positive ArrayInt
* @internal
*/
export function addOneToPositiveArrayInt(arrayInt: ArrayInt): ArrayInt {
arrayInt.sign = 1 // handling case { sign: -1, data: [0,...,0] }
const data = arrayInt.data
for (let index = data.length - 1; index >= 0; --index) {
if (data[index] === 0xffffffff) {
data[index] = 0
} else {
data[index] += 1
return arrayInt
}
}
data.unshift(1)
return arrayInt
}

/** @internal */
function isStrictlySmaller(dataA: number[], dataB: number[]): boolean {
const maxLength = Math.max(dataA.length, dataB.length)
for (let index = 0; index < maxLength; ++index) {
const indexA = index + dataA.length - maxLength
const indexB = index + dataB.length - maxLength
const vA = indexA >= 0 ? dataA[indexA]! : 0
const vB = indexB >= 0 ? dataB[indexB]! : 0
if (vA < vB) return true
if (vA > vB) return false
}
return false
}

/**
* Substract two ArrayInt
* @internal
*/
export function substractArrayIntToNew(arrayIntA: ArrayInt, arrayIntB: ArrayInt): ArrayInt {
if (arrayIntA.sign !== arrayIntB.sign) {
return addArrayIntToNew(arrayIntA, { sign: -arrayIntB.sign as -1 | 1, data: arrayIntB.data })
}
const dataA = arrayIntA.data
const dataB = arrayIntB.data
if (isStrictlySmaller(dataA, dataB)) {
const out = substractArrayIntToNew(arrayIntB, arrayIntA)
out.sign = -out.sign as -1 | 1
return out
}
const data = []
let reminder = 0
for (let indexA = dataA.length - 1, indexB = dataB.length - 1; indexA >= 0 || indexB >= 0; --indexA, --indexB) {
const vA = indexA >= 0 ? dataA[indexA]! : 0
const vB = indexB >= 0 ? dataB[indexB]! : 0
const current = vA - vB - reminder
data.push(current >>> 0)
reminder = current < 0 ? 1 : 0
}
return { sign: arrayIntA.sign, data: data.reverse() }
}

/**
* Trim uneeded zeros in ArrayInt
* and uniform notation for zero: {sign: 1, data: [0]}
*/
export function trimArrayIntInplace(arrayInt: ArrayInt) {
const data = arrayInt.data
let firstNonZero = 0
// eslint-disable-next-line no-empty
for (; firstNonZero !== data.length && data[firstNonZero] === 0; ++firstNonZero) {}
if (firstNonZero === data.length) {
// only zeros
arrayInt.sign = 1
arrayInt.data = [0]
return arrayInt
}
data.splice(0, firstNonZero)
return arrayInt
}

// Helpers specific to 64 bits versions

/** @internal */
export type ArrayInt64 = Required<ArrayInt> & { data: [number, number] }

/**
* We only accept safe integers here
* @internal
*/
export function fromNumberToArrayInt64(out: ArrayInt64, n: number): ArrayInt64 {
if (n < 0) {
const posN = -n
out.sign = -1
out.data[0] = ~~(posN / 0x100000000)
out.data[1] = posN >>> 0
} else {
out.sign = 1
out.data[0] = ~~(n / 0x100000000)
out.data[1] = n >>> 0
}
return out
}

/**
* Substract two ArrayInt of 64 bits on 64 bits.
* With arrayIntA - arrayIntB >= 0
* @internal
*/
export function substractArrayInt64(out: ArrayInt64, arrayIntA: ArrayInt64, arrayIntB: ArrayInt64): ArrayInt64 {
const lowA = arrayIntA.data[1]
const highA = arrayIntA.data[0]
const signA = arrayIntA.sign
const lowB = arrayIntB.data[1]
const highB = arrayIntB.data[0]
const signB = arrayIntB.sign

// Requirement: arrayIntA - arrayIntB >= 0
out.sign = 1

if (signA === 1 && signB === -1) {
// Operation is a simple sum of arrayIntA + abs(arrayIntB)
const low = lowA + lowB
const high = highA + highB + (low > 0xffffffff ? 1 : 0)
out.data[0] = high >>> 0
out.data[1] = low >>> 0
return out
}
// signA === -1 with signB === 1 is impossible given: arrayIntA - arrayIntB >= 0

// Operation is a substraction
let lowFirst = lowA
let highFirst = highA
let lowSecond = lowB
let highSecond = highB
if (signA === -1) {
lowFirst = lowB
highFirst = highB
lowSecond = lowA
highSecond = highA
}
let reminderLow = 0
let low = lowFirst - lowSecond
if (low < 0) {
reminderLow = 1
low = low >>> 0
}
out.data[0] = highFirst - highSecond - reminderLow
out.data[1] = low
return out
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt"
import { uniformIntDistributionInternal } from "@tsplus/stdlib/io/Random/Distribution/_internal/uniformIntDistributionInternal"
import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator"

/**
* Uniformly generate an `ArrayInt` in the range [0, rangeSize].
*
* @remarks
* In the worst case scenario it may discard half of the randomly generated
* values. Worst case being: most significant number is `1` and remaining part
* evaluates to `0`.
*
* @internal
*/
export function uniformArrayIntDistributionInternal(
out: ArrayInt["data"],
rangeSize: ArrayInt["data"],
rng: RandomGenerator
): ArrayInt["data"] {
const rangeLength = rangeSize.length

// We iterate until we find a valid value for arrayInt
// eslint-disable-next-line no-constant-condition
while (true) {
// We compute a new value for arrayInt
for (let index = 0; index !== rangeLength; ++index) {
const indexRangeSize = index === 0 ? rangeSize[0]! + 1 : 0x100000000
out[index] = uniformIntDistributionInternal(indexRangeSize, rng)
}

// If in the correct range we can return it
for (let index = 0; index !== rangeLength; ++index) {
const current = out[index]!
const currentInRange = rangeSize[index]!
if (current < currentInRange) {
return out // arrayInt < rangeSize
} else if (current > currentInRange) {
break // arrayInt > rangeSize
}
}
// Otherwise we need to try another one
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator"

/**
* Uniformly generate an integer in the range [0, rangeSize].
*
* @internal
*/
export function uniformIntDistributionInternal(rangeSize: number, rng: RandomGenerator): number {
const MinRng = rng.min()
const NumValues = rng.max() - rng.min() + 1

// Range provided by the RandomGenerator is large enough
if (rangeSize <= NumValues) {
const MaxAllowed = NumValues - (NumValues % rangeSize)
// eslint-disable-next-line no-constant-condition
while (true) {
const out = rng.next()
const deltaV = out - MinRng
if (deltaV < MaxAllowed) {
return deltaV % rangeSize // Warning: we expect NumValues <= 2**32, so diff too
}
}
}

// Compute number of iterations required to have enough random
// to build uniform entries in the asked range
let FinalNumValues = NumValues * NumValues
let NumIterations = 2 // At least 2 (at this point in the code)
while (FinalNumValues < rangeSize) {
FinalNumValues *= NumValues
;++NumIterations
}
const MaxAcceptedRandom = rangeSize * Math.floor((1 * FinalNumValues) / rangeSize)

// eslint-disable-next-line no-constant-condition
while (true) {
// Aggregate mutiple calls to next() into a single random value
let value = 0
for (let num = 0; num !== NumIterations; ++num) {
const out = rng.next()
value = NumValues * value + (out - MinRng) // Warning: we overflow may when diff > max_safe (eg: =max_safe-min_safe+1)
}
if (value < MaxAcceptedRandom) {
const inDiff = value - rangeSize * Math.floor((1 * value) / rangeSize)
return inDiff
}
}
}
15 changes: 15 additions & 0 deletions packages/stdlib/_src/io/Random/Distribution/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator"

/**
* Generate random value based on a given RandomGenerator. Return the generated
* value and an offsetted version of the RandomGenerator.
*
* @tsplus type Random.Distribution
*/
export type Distribution<T> = (rng: RandomGenerator) => T

/**
* @tsplus type Random.Distribution.Ops
*/
export interface DistributionOps {}
export const Distribution: DistributionOps = {}
5 changes: 5 additions & 0 deletions packages/stdlib/_src/io/Random/Distribution/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@tsplus/stdlib/io/Random/Distribution"}
export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformArrayIntDistribution"
export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformBigIntDistribution"
export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformIntDistribution"
// codegen:end
Loading