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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
- 文法エラーの表示を改善。理由を詳細に表示するように。
- 複数行のコメントがある時に文法エラーの表示行数がずれる問題を解消しました。
- 実行時エラーの発生位置が表示されるように。
- 関数`Math:gen_rng`に第二引数`algorithm`をオプション引数として追加。
- アルゴリズムを`chacha20`、`rc4`、`rc4_legacy`から選べるようになりました。
- **Breaking Change** `algorithm`を指定しない場合、`chacha20`が選択されます。
- **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。
- **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。
- **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。
- Fix: `Math:rnd`が範囲外の値を返す可能性があるのを修正。

# 0.17.0
- `package.json`を修正
Expand All @@ -20,6 +24,7 @@
- 関数`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`がサロゲートペアに対応していないのを修正
- 配列の範囲外および非整数のインデックスへの代入でエラーを出すように

## 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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"typescript": "5.3.3"
},
"dependencies": {
"@types/libsodium-wrappers-sumo": "0.7.8",
"libsodium-wrappers-sumo": "0.7.13",
"seedrandom": "3.0.5",
"stringz": "2.1.0",
"uuid": "9.0.1"
Expand Down
30 changes: 16 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 } 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 @@ -409,23 +410,24 @@ 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, algorithm]) => {
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());
});
const algo = !algorithm || algorithm.type !== 'str' ? STR('chacha20') : algorithm;
if (seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') return NULL;
switch (algo.value) {
case 'rc4_legacy':
return GenerateLegacyRandom(seed);
case 'rc4':
return GenerateRC4Random(seed);
default:
return GenerateChaCha20Random(seed);
}
}),
//#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);
}
}
107 changes: 107 additions & 0 deletions src/utils/random/chacha20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import sodium from 'libsodium-wrappers-sumo';
import { RandomBase, bigMaxSafeIntegerExclusive, bigSafeIntegerBits, readBigUintLittleEndian, safeIntegerBits } from './randomBase.js';

const chacha20BlockSize = 64;
const bigChacha20BlockSize = BigInt(chacha20BlockSize);

export class ChaCha20 extends RandomBase {
public static ready = sodium.ready;
private key: Uint8Array;
private nonce: Uint8Array;
private buffer: Uint8Array;
private filledBuffer: Uint8Array;
private counter: bigint;
constructor(seed?: string | number | Uint8Array | undefined) {
const keyNonceBytes = sodium.crypto_stream_chacha20_NONCEBYTES + sodium.crypto_stream_chacha20_KEYBYTES;
super();
let keynonce: Uint8Array;
if (typeof seed === 'undefined') {
keynonce = crypto.getRandomValues(new Uint8Array(keyNonceBytes));
} else if (typeof seed === 'number') {
const array = new Float64Array([seed]);
keynonce = sodium.crypto_generichash(keyNonceBytes, new Uint8Array(array.buffer));
} else {
keynonce = sodium.crypto_generichash(keyNonceBytes, seed);
}
this.key = keynonce.subarray(0, sodium.crypto_stream_chacha20_KEYBYTES);
this.nonce = keynonce.subarray(sodium.crypto_stream_chacha20_KEYBYTES);
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 blocks = BigInt(buffer.length / chacha20BlockSize);
let counter = this.counter % bigMaxSafeIntegerExclusive;
const newCount = counter + blocks;
const overflow = newCount >> bigSafeIntegerBits;
if (overflow === 0n) {
buffer.set(sodium.crypto_stream_chacha20_xor_ic(buffer, this.nonce, Number(counter), this.key));
this.counter = newCount;
return buffer;
}
let dst = buffer;
while (dst.length > 0) {
const remainingBlocks = bigMaxSafeIntegerExclusive - counter;
const genBlocks = remainingBlocks > blocks ? blocks : remainingBlocks;
blocks -= genBlocks;
const dbuf = dst.subarray(0, Number(genBlocks * bigChacha20BlockSize)); // safe integers wouldn't lose any precision with multiplying by a power-of-two anyway.
dbuf.set(sodium.crypto_stream_chacha20_xor_ic(dbuf, this.nonce, Number(counter), this.key));
dst = dst.subarray(dbuf.length);
counter = BigInt.asUintN(safeIntegerBits, counter + genBlocks);
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;
}
}
}
42 changes: 42 additions & 0 deletions src/utils/random/genrng.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import seedrandom from 'seedrandom';
import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js';
import { assertNumber } from '../../interpreter/util.js';
import { SeedRandomWrapper } from './seedrandom.js';
import type { VNativeFn, VNull, Value } from '../../interpreter/value.js';
import { ChaCha20 } from './chacha20.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<VNull | VNativeFn> {
if (!seed || seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') return NULL;
await ChaCha20.ready;
const rng = new ChaCha20(seed.type === 'null' ? undefined : 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());
});
}
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;
}
Loading