Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
- 関数`Str#charcode_at` `Str#to_arr` `Str#to_char_arr` `Str#to_charcode_arr` `Str#to_utf8_byte_arr` `Str#to_unicode_codepoint_arr` `Str:from_unicode_codepoints` `Str:from_utf8_bytes`を追加
- Fix: `Str#codepoint_at`がサロゲートペアに対応していないのを修正
- 配列の範囲外および非整数のインデックスへの代入でエラーを出すように
- Fix: `Math:rnd`が範囲外の値を返す可能性があるのを修正
- 関数`Math:gen_rng_unbiased`を追加
## Note
バージョン0.16.0に記録漏れがありました。
>- 関数`Str:from_codepoint` `Str#codepoint_at`を追加
Expand Down
5 changes: 5 additions & 0 deletions docs/std-math.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ _min_ および _max_ を渡した場合、_min_ <= x, x <= _max_ の整数、

### @Math:gen_rng(_seed_: num | str): fn
シードから乱数生成機を生成します。
生成された乱数生成器は 0 <= x, x < 1 の 小数を返します。

### @Math:gen_rng_unbiased(_seed_: num | str): @(_min_: num, _max_: num)
シードから _min_ <= x, x <= _max_ の整数を一様分布で生成する乱数生成機を生成します。
_min_ および _max_ が渡されていない場合はエラーとなります。

## その他
### @Math:clz32(_x_: num): num
Expand Down
21 changes: 18 additions & 3 deletions src/interpreter/lib/std.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { assertNumber, assertString, assertBoolean, valToJs, jsToVal, assertFunc
import { AiScriptRuntimeError } from '../../error.js';
import { AISCRIPT_VERSION } from '../../constants.js';
import { textDecoder } from '../../const.js';
import { CryptoGen } from '../../utils/random/CryptoGen.js';
import { SeedRandomWrapper } from '../../utils/random/seedrandom.js';
import type { Value } from '../value.js';

export const std: Record<string, Value> = {
Expand Down Expand Up @@ -409,24 +411,37 @@ export const std: Record<string, Value> = {

'Math:rnd': FN_NATIVE(([min, max]) => {
if (min && min.type === 'num' && max && max.type === 'num') {
return NUM(Math.floor(Math.random() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value)));
const res = CryptoGen.instance.generateRandomIntegerInRange(min.value, max.value);
return res === null ? NULL : NUM(res);
}
return NUM(Math.random());
return NUM(CryptoGen.instance.generateNumber0To1());
}),

'Math:gen_rng': FN_NATIVE(([seed]) => {
expectAny(seed);
if (seed.type !== 'num' && seed.type !== 'str') return NULL;

const rng = seedrandom(seed.value.toString());

return FN_NATIVE(([min, max]) => {
if (min && min.type === 'num' && max && max.type === 'num') {
return NUM(Math.floor(rng() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value)));
}
return NUM(rng());
});
}),

'Math:gen_rng_unbiased': FN_NATIVE(([seed]) => {
expectAny(seed);
if (seed.type !== 'num' && seed.type !== 'str') return NULL;

const rng = new SeedRandomWrapper(seed.value);
return FN_NATIVE(([min, max]) => {
assertNumber(min);
assertNumber(max);
const result = rng.generateRandomIntegerInRange(min.value, max.value);
return typeof result === 'number' ? NUM(result) : NULL;
});
}),
//#endregion

//#region Num
Expand Down
29 changes: 29 additions & 0 deletions src/utils/random/CryptoGen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RandomBase, readBigUintLittleEndian } from './randomBase.js';

export class CryptoGen extends RandomBase {
private static _instance: CryptoGen = new CryptoGen();
public static get instance() : CryptoGen {
return CryptoGen._instance;
}

private constructor() {
super();
}

protected generateBigUintByBytes(bytes: number): bigint {
let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8);
if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n;
u8a = this.generateBytes(u8a.subarray(0, bytes));
return readBigUintLittleEndian(u8a.buffer) ?? 0n;
}

public generateBigUintByBits(bits: number): bigint {
if (bits < 1 || !Number.isSafeInteger(bits)) return 0n;
const bytes = Math.ceil(bits / 8);
const wastedBits = BigInt(bytes * 8 - bits);
return this.generateBigUintByBytes(bytes) >> wastedBits;
}
public generateBytes(array: Uint8Array): Uint8Array {
return crypto.getRandomValues(array);
}
}
92 changes: 92 additions & 0 deletions src/utils/random/randomBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
export const safeIntegerBits = Math.ceil(Math.log2(Number.MAX_SAFE_INTEGER));
export const bigSafeIntegerBits = BigInt(safeIntegerBits);
export const bigMaxSafeIntegerExclusive = 1n << bigSafeIntegerBits;
export const fractionBits = safeIntegerBits - 1;
export const bigFractionBits = BigInt(fractionBits);

export abstract class RandomBase {
protected abstract generateBigUintByBytes(bytes: number): bigint;
public abstract generateBigUintByBits(bits: number): bigint;
public abstract generateBytes(array: Uint8Array): Uint8Array;

public generateNumber0To1(): number {
let res = this.generateBigUintByBits(safeIntegerBits);
let exponent = 1022;
let remainingFractionBits = safeIntegerBits - bitsToRepresent(res);
while (remainingFractionBits > 0 && exponent >= safeIntegerBits) {
exponent -= remainingFractionBits;
res <<= BigInt(remainingFractionBits);
res |= this.generateBigUintByBits(remainingFractionBits);
remainingFractionBits = safeIntegerBits - bitsToRepresent(res);
}
if (remainingFractionBits > 0) {
const shift = Math.min(exponent - 1, remainingFractionBits);
res <<= BigInt(shift);
res |= this.generateBigUintByBits(shift);
exponent = Math.max(exponent - shift, 0);
}
return (Number(res) * 0.5 ** safeIntegerBits) * (0.5 ** (1022 - exponent));
}

public generateUniform(maxInclusive: bigint): bigint {
if (maxInclusive < 1) return 0n;
const log2 = maxInclusive.toString(2).length;
const bytes = Math.ceil(log2 / 8);
const wastedBits = BigInt(bytes * 8 - log2);
let result: bigint;
do {
result = this.generateBigUintByBytes(bytes) >> wastedBits;
} while (result > maxInclusive);
return result;
}

public generateRandomIntegerInRange(min: number, max: number): number | null {
const ceilMin = Math.ceil(min);
const floorMax = Math.floor(max);
const signedScale = floorMax - ceilMin;
if (signedScale === 0) return ceilMin;
const scale = Math.abs(signedScale);
const scaleSign = Math.sign(signedScale);
if (!Number.isSafeInteger(scale) || !Number.isSafeInteger(ceilMin) || !Number.isSafeInteger(floorMax)) {
return null;
}
const bigScale = BigInt(scale);
return Number(this.generateUniform(bigScale)) * scaleSign + ceilMin;
}
}

export function bitsToRepresent(num: bigint): number {
if (num === 0n) return 0;
return num.toString(2).length;
}

function readSmallBigUintLittleEndian(buffer: ArrayBufferLike): bigint | null {
if (buffer.byteLength === 0) return null;
if (buffer.byteLength < 8) {
const array = new Uint8Array(8);
array.set(new Uint8Array(buffer));
return new DataView(array.buffer).getBigUint64(0, true);
}
return new DataView(buffer).getBigUint64(0, true);
}

export function readBigUintLittleEndian(buffer: ArrayBufferLike): bigint | null {
if (buffer.byteLength === 0) return null;
if (buffer.byteLength <= 8) {
return readSmallBigUintLittleEndian(buffer);
}
const dataView = new DataView(buffer);
let pos = 0n;
let res = 0n;
let index = 0;
for (; index < dataView.byteLength - 7; index += 8, pos += 64n) {
const element = dataView.getBigUint64(index, true);
res |= element << pos;
}
if (index < dataView.byteLength) {
const array = new Uint8Array(8);
array.set(new Uint8Array(buffer, index));
res |= new DataView(array.buffer).getBigUint64(0, true) << pos;
}
return res;
}
76 changes: 76 additions & 0 deletions src/utils/random/seedrandom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import seedrandom from 'seedrandom';
import { RandomBase, readBigUintLittleEndian } from './randomBase.js';

const seedRandomBlockSize = Int32Array.BYTES_PER_ELEMENT;

export class SeedRandomWrapper extends RandomBase {
private rng: seedrandom.PRNG;
private buffer: Uint8Array;
private filledBuffer: Uint8Array;
constructor(seed: string | number) {
super();
this.rng = seedrandom(seed.toString());
this.buffer = new Uint8Array(seedRandomBlockSize);
this.filledBuffer = new Uint8Array(0);
}
private fillBuffer(): void {
this.buffer.fill(0);
this.buffer = this.fillBufferDirect(this.buffer);
this.filledBuffer = this.buffer;
}
private fillBufferDirect(buffer: Uint8Array): Uint8Array {
if ((buffer.length % seedRandomBlockSize) !== 0) throw new Error(`SeedRandomWrapper.fillBufferDirect should always be called with the buffer with the length a multiple-of-${seedRandomBlockSize}!`);
const length = buffer.length / seedRandomBlockSize;
const dataView = new DataView(buffer.buffer);
let byteOffset = 0;
for (let index = 0; index < length; index++, byteOffset += seedRandomBlockSize) {
dataView.setInt32(byteOffset, this.rng.int32(), false);
}
return buffer;
}
protected generateBigUintByBytes(bytes: number): bigint {
let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8);
if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n;
u8a = this.generateBytes(u8a.subarray(0, bytes));
return readBigUintLittleEndian(u8a.buffer) ?? 0n;
}

public generateBigUintByBits(bits: number): bigint {
if (bits < 1 || !Number.isSafeInteger(bits)) return 0n;
const bytes = Math.ceil(bits / 8);
const wastedBits = BigInt(bytes * 8 - bits);
return this.generateBigUintByBytes(bytes) >> wastedBits;
}

public generateBytes(array: Uint8Array): Uint8Array {
if (array.length < 1) return array;
array.fill(0);
let dst = array;
if (dst.length <= this.filledBuffer.length) {
dst.set(this.filledBuffer.subarray(0, dst.length));
this.filledBuffer = this.filledBuffer.subarray(dst.length);
return array;
} else {
while (dst.length > 0) {
if (this.filledBuffer.length === 0) {
if (dst.length >= seedRandomBlockSize) {
const df64 = dst.subarray(0, dst.length - (dst.length % seedRandomBlockSize));
this.fillBufferDirect(df64);
dst = dst.subarray(df64.length);
continue;
}
this.fillBuffer();
}
if (dst.length <= this.filledBuffer.length) {
dst.set(this.filledBuffer.subarray(0, dst.length));
this.filledBuffer = this.filledBuffer.subarray(dst.length);
return array;
}
dst.set(this.filledBuffer);
dst = dst.subarray(this.filledBuffer.length);
this.fillBuffer();
}
return array;
}
}
}