Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bf2611a
Updated `vite.config.js`
MineCake147E Nov 27, 2023
8eb041c
Implemented unbiased variant of `Math:rnd` and `Math:gen_rng_unbiased`
MineCake147E Nov 28, 2023
19ddcb2
Merge remote-tracking branch 'upstream/master' into rnd-remove-bias
MineCake147E Nov 28, 2023
5ae8288
Updated `aiscript.api.md`
MineCake147E Dec 3, 2023
94c33c2
* Refactored `Math:rnd` to use crypto
MineCake147E Dec 3, 2023
c7574a0
Merge remote-tracking branch 'upstream/master' into rnd-remove-bias
MineCake147E Dec 3, 2023
eb7edd3
Updated `aiscript.api.md`
MineCake147E Dec 3, 2023
26f0c3a
* Removed `Math:rnd_unbiased`
MineCake147E Dec 3, 2023
7176293
* Updated `CHANGELOG.md`
MineCake147E Dec 3, 2023
27463e6
Implemented `Math:gen_rng_chacha20`
MineCake147E Dec 3, 2023
a0934f5
* Refactored `Math:gen_rng_unbiased` to use ChaCha20
MineCake147E Dec 3, 2023
a976795
+ Added tests for `Math:gen_rng_unbiased`
MineCake147E Dec 3, 2023
c77a1af
Merge remote-tracking branch 'upstream/master' into rnd-chacha20
MineCake147E Dec 5, 2023
72f7f24
Merge remote-tracking branch 'upstream/aiscript-next' into rnd-chacha20
MineCake147E Feb 9, 2024
3ef118a
* `Math:gen_rng` now has 2nd parameter `algorithm`
MineCake147E Feb 9, 2024
cd0f8d4
*Updated `CHANGELOG.md`
MineCake147E Feb 9, 2024
4029164
Merge commit '0fc741a7de4160bfc584a2e63d98412e99f01e46' into rnd-chac…
MineCake147E Jul 29, 2024
af24c71
Updated `std-math.md`
MineCake147E Jul 29, 2024
3feb937
Updated `package-lock.json`
MineCake147E Jul 29, 2024
d715671
* Reverted `CHANGELOG.md` to match upstream
MineCake147E Jul 29, 2024
1ec26b4
Added `random algorithms.md`
MineCake147E Jul 29, 2024
6904a1a
Updated std-math.md
MineCake147E Aug 2, 2024
12da3b3
Merge commit '8d984d21cae9d19b59c84fe23ef45a99f492d555' into rnd-chac…
MineCake147E Aug 2, 2024
22f2be5
Added initial support for option objects in `Math:gen_rng`
MineCake147E Aug 2, 2024
c87b630
* Changed implementation of ChaCha20
MineCake147E Aug 2, 2024
96375ca
Updated `std-math.md`
MineCake147E Aug 2, 2024
556839a
Updated `std-math.md`
MineCake147E Aug 2, 2024
fb29ab4
Fixed `Math:gen_rng` returned `Promise<VNativeFn | VNull>` instead of…
MineCake147E Aug 2, 2024
36e3fef
* Fixed ChaCha20 generating wrong values
MineCake147E Aug 3, 2024
8758830
Merge commit 'e1454f4db5ef68b1440ddc60ffca4dec12ae46d5' into rnd-chac…
MineCake147E Aug 3, 2024
452d353
* `Math:gen_rng`: `options` no longer accepts anything but `obj` or `…
MineCake147E Aug 3, 2024
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
14 changes: 13 additions & 1 deletion docs/std-math.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,20 @@ _x_ +1の自然対数を計算します。
_min_ および _max_ を渡した場合、_min_ <= x, x <= _max_ の整数、
渡していない場合は 0 <= x, x < 1 の 小数が返されます。

### @Math:gen_rng(_seed_: num | str): fn
### @Math:gen_rng(_seed_: num | str, _options_?: obj): @(_min_?: num, _max_?: num)
シードから乱数生成機を生成します。
生成された乱数生成器は、_min_ および _max_ を渡した場合、_min_ <= x, x <= _max_ の整数、
渡していない場合は 0 <= x, x < 1 の浮動小数点数を返します。
_options_ に渡したオブジェクトを通じて、内部の挙動を指定できます。
`options.algorithm`の指定による挙動の変化は下記の通りです。
| `options.algorithm` | 内部の乱数生成アルゴリズム | 範囲指定整数生成アルゴリズム |
|--|--|--|
| `rc4` | RC4 | Rejection Sampling |
| `rc4_legacy` | RC4 | 浮動小数点数演算による範囲制限​(0.19.0以前のアルゴリズム) |
| 無指定 または 'chacha20' | ChaCha20 | Rejection Sampling |

> [!CAUTION]
> `rc4_legacy`等、浮動小数点数演算を伴う範囲指定整数生成アルゴリズムでは、演算時の丸め誤差により、指定した _max_ の値より大きな値が生成される可能性があります。

## その他
### @Math:clz32(_x_: num): num
Expand Down
40 changes: 26 additions & 14 deletions src/interpreter/lib/std.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable no-empty-pattern */
import { v4 as uuid } from 'uuid';
import seedrandom from 'seedrandom';
import { NUM, STR, FN_NATIVE, FALSE, TRUE, ARR, NULL, BOOL, OBJ, ERROR } from '../value.js';
import { assertNumber, assertString, assertBoolean, valToJs, jsToVal, assertFunction, assertObject, eq, expectAny, assertArray, reprValue } from '../util.js';
import { AiScriptRuntimeError, AiScriptUserError } from '../../error.js';
import { AISCRIPT_VERSION } from '../../constants.js';
import { textDecoder } from '../../const.js';
import { CryptoGen } from '../../utils/random/CryptoGen.js';
import { GenerateChaCha20Random, GenerateLegacyRandom, GenerateRC4Random } from '../../utils/random/genrng.js';
import type { Value } from '../value.js';

export const std: Record<string, Value> = {
Expand Down Expand Up @@ -451,23 +452,34 @@ 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]) => {
'Math:gen_rng': FN_NATIVE(async ([seed, options]) => {
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());
});
let algo = 'chacha20';
if (options?.type === 'obj') {
const v = options.value.get('algorithm');
if (v?.type !== 'str') throw new AiScriptRuntimeError('`options.algorithm` must be string.');
algo = v.value;
}
else if (options?.type !== undefined) {
throw new AiScriptRuntimeError('`options` must be an object if specified.');
}
if (seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') throw new AiScriptRuntimeError('`seed` must be either number or string if specified.');
switch (algo) {
case 'rc4_legacy':
return GenerateLegacyRandom(seed);
case 'rc4':
return GenerateRC4Random(seed);
case 'chacha20':
return await GenerateChaCha20Random(seed);
default:
throw new AiScriptRuntimeError('`options.algorithm` must be one of these: `chacha20`, `rc4`, or `rc4_legacy`.');
}
}),
//#endregion

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);
}
}
153 changes: 153 additions & 0 deletions src/utils/random/chacha20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { RandomBase, readBigUintLittleEndian } from './randomBase.js';

// translated from https://github.com/skeeto/chacha-js/blob/master/chacha.js
const chacha20BlockSize = 64;
const CHACHA_ROUNDS = 20;
const CHACHA_KEYSIZE = 32;
const CHACHA_IVSIZE = 8;
function rotate(v: number, n: number): number { return (v << n) | (v >>> (32 - n)); }
function quarterRound(x: Uint32Array, a: number, b: number, c: number, d: number): void {
if (x.length < 16) return;
let va = x[a];
let vb = x[b];
let vc = x[c];
let vd = x[d];
if (va === undefined || vb === undefined || vc === undefined || vd === undefined) return;
va = (va + vb) | 0;
vd = rotate(vd ^ va, 16);
vc = (vc + vd) | 0;
vb = rotate(vb ^ vc, 12);
va = (va + vb) | 0;
vd = rotate(vd ^ va, 8);
vc = (vc + vd) | 0;
vb = rotate(vb ^ vc, 7);
x[a] = va;
x[b] = vb;
x[c] = vc;
x[d] = vd;
}
function generateChaCha20(dst: Uint32Array, state: Uint32Array) : void {
if (dst.length < 16 || state.length < 16) return;
dst.set(state);
for (let i = 0; i < CHACHA_ROUNDS; i += 2) {
quarterRound(dst, 0, 4, 8, 12);
quarterRound(dst, 1, 5, 9, 13);
quarterRound(dst, 2, 6, 10, 14);
quarterRound(dst, 3, 7, 11, 15);
quarterRound(dst, 0, 5, 10, 15);
quarterRound(dst, 1, 6, 11, 12);
quarterRound(dst, 2, 7, 8, 13);
quarterRound(dst, 3, 4, 9, 14);
}
for (let i = 0; i < 16; i++) {
let d = dst[i];
const s = state[i];
if (d === undefined || s === undefined) throw new Error('generateChaCha20: Something went wrong!');
d = (d + s) | 0;
dst[i] = d;
}
}
export class ChaCha20 extends RandomBase {
private keynonce: Uint32Array;
private state: Uint32Array;
private buffer: Uint8Array;
private filledBuffer: Uint8Array;
private counter: bigint;
constructor(seed?: Uint8Array | undefined) {
const keyNonceBytes = CHACHA_IVSIZE + CHACHA_KEYSIZE;
super();
let keynonce: Uint8Array;
if (typeof seed === 'undefined') {
keynonce = crypto.getRandomValues(new Uint8Array(keyNonceBytes));
} else {
keynonce = seed;
if (keynonce.byteLength > keyNonceBytes) keynonce = seed.subarray(0, keyNonceBytes);
if (keynonce.byteLength < keyNonceBytes) {
const y = new Uint8Array(keyNonceBytes);
y.set(keynonce);
keynonce = y;
}
}
const key = keynonce.subarray(0, CHACHA_KEYSIZE);
const nonce = keynonce.subarray(CHACHA_KEYSIZE, CHACHA_KEYSIZE + CHACHA_IVSIZE);
const kn = new Uint8Array(16 * 4);
kn.set([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]);
kn.set(key, 4 * 4);
kn.set(nonce, 14 * 4);
this.keynonce = new Uint32Array(kn.buffer);
this.state = new Uint32Array(16);
this.buffer = new Uint8Array(chacha20BlockSize);
this.counter = 0n;
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 % chacha20BlockSize) !== 0) throw new Error('ChaCha20.fillBufferDirect should always be called with the buffer with the length a multiple-of-64!');
buffer.fill(0);
let counter = this.counter;
const state = this.state;
const counterState = new BigUint64Array(state.buffer);
let dst = buffer;
while (dst.length > 0) {
const dbuf = dst.subarray(0, state.byteLength);
const dst32 = new Uint32Array(dbuf.buffer);
state.set(this.keynonce);
counterState[6] = BigInt.asUintN(64, counter);
generateChaCha20(dst32, state);
dst = dst.subarray(dbuf.length);
counter = BigInt.asUintN(64, counter + 1n);
}
this.counter = counter;
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 >= chacha20BlockSize) {
const df64 = dst.subarray(0, dst.length - (dst.length % chacha20BlockSize));
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;
}
}
}
48 changes: 48 additions & 0 deletions src/utils/random/genrng.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import seedrandom from 'seedrandom';
import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js';
import { SeedRandomWrapper } from './seedrandom.js';
import { ChaCha20 } from './chacha20.js';
import type { VNativeFn, VNull, Value } from '../../interpreter/value.js';
import { textEncoder } from '../../const.js';

export function GenerateLegacyRandom(seed: Value | undefined) : VNativeFn | VNull {
if (!seed || 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());
});
}

export function GenerateRC4Random(seed: Value | undefined) : VNativeFn | VNull {
if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL;
const rng = new SeedRandomWrapper(seed.value);
return FN_NATIVE(([min, max]) => {
if (min && min.type === 'num' && max && max.type === 'num') {
const result = rng.generateRandomIntegerInRange(min.value, max.value);
return typeof result === 'number' ? NUM(result) : NULL;
}
return NUM(rng.generateNumber0To1());
});
}

export async function GenerateChaCha20Random(seed: Value | undefined) : Promise<VNativeFn | VNull> {
if (!seed || seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') return NULL;
let actualSeed : Uint8Array | undefined = undefined;
if (seed.type === 'num')
{
actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(new Float64Array([seed.value]))));
} else if (seed.type === 'str') {
actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(textEncoder.encode(seed.value))));
}
const rng = new ChaCha20(actualSeed);
return FN_NATIVE(([min, max]) => {
if (min && min.type === 'num' && max && max.type === 'num') {
const result = rng.generateRandomIntegerInRange(min.value, max.value);
return typeof result === 'number' ? NUM(result) : NULL;
}
return NUM(rng.generateNumber0To1());
});
}
Loading