Skip to content
Open
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: 1 addition & 1 deletion src/interpreter/lib/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export const stdMath: Record<`Math:${string}`, Value> = {
}
case 'chacha20': {
if (!isSecureContext) throw new AiScriptRuntimeError(`The random algorithm ${algo} cannot be used because \`crypto.subtle\` is not available. Maybe in non-secure context?`);
return await GenerateChaCha20Random(seed);
return await GenerateChaCha20Random(seed, options?.value);
}
default:
throw new AiScriptRuntimeError('`options.algorithm` must be one of these: `chacha20`, `rc4`, or `rc4_legacy`.');
Expand Down
16 changes: 11 additions & 5 deletions src/utils/random/genrng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js';
import { textEncoder } from '../../const.js';
import { SeedRandomWrapper } from './seedrandom.js';
import { ChaCha20 } from './chacha20.js';
import type { VNativeFn, VNum, VStr } from '../../interpreter/value.js';
import type { Value, VNativeFn, VNum, VStr } from '../../interpreter/value.js';

export function GenerateLegacyRandom(seed: VNum | VStr): VNativeFn {
const rng = seedrandom(seed.value.toString());
Expand All @@ -26,11 +26,17 @@ export function GenerateRC4Random(seed: VNum | VStr): VNativeFn {
});
}

export async function GenerateChaCha20Random(seed: VNum | VStr): Promise<VNativeFn> {
export async function GenerateChaCha20Random(seed: VNum | VStr, options: Map<string, Value> | undefined): Promise<VNativeFn> {
let actualSeed: Uint8Array;
if (seed.type === 'num')
{
actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(new Float64Array([seed.value]))));
if (seed.type === 'num') {
const float64Array = new Float64Array([seed.value]);
const numberAsIntegerOptionValue = options?.get('chacha20NumberSeedLegacyBehaviour');
let numberAsInteger = false;
if (numberAsIntegerOptionValue?.type === 'bool') {
numberAsInteger = numberAsIntegerOptionValue.value;
}
const seedToDigest = numberAsInteger ? new Uint8Array(float64Array) : new Uint8Array(float64Array.buffer);
actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', seedToDigest));
} else {
actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(textEncoder.encode(seed.value))));
}
Expand Down
35 changes: 32 additions & 3 deletions test/std.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as assert from 'assert';
import { describe, expect, test } from 'vitest';
import { utils } from '../src';
import { AiScriptRuntimeError } from '../src/error';
import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value';
import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR, FN_NATIVE } from '../src/interpreter/value';
import { exe, eq } from './testutils';


Expand Down Expand Up @@ -88,7 +88,7 @@ describe('Math', () => {
test.concurrent('max', async () => {
eq(await exe("<: Math:max(-2, -3)"), NUM(-2));
});

/* flaky
test.concurrent('rnd', async () => {
const steps = 512;
Expand Down Expand Up @@ -158,6 +158,35 @@ describe('Math', () => {
eq(res, ARR([BOOL(true), BOOL(true)]));
});

test.concurrent('gen_rng number seed', async () => {
// 2つのシード値から1~maxの乱数をn回生成して一致率を見る(numがシード値として指定された場合)
const res = await exe(`
@test(seed1, seed2) {
let n = 100
let max = 100000
let threshold = 0.05
let random1 = Math:gen_rng(seed1)
let random2 = Math:gen_rng(seed2)
var same = 0
for n {
if random1(1, max) == random2(1, max) {
same += 1
}
}
let rate = same / n
if seed1 == seed2 { rate == 1 }
else { rate < threshold }
}
let seed1 = 3.0
let seed2 = 3.0000000000000004
<: [
test(seed1, seed1)
test(seed1, seed2)
]
`)
eq(res, ARR([BOOL(true), BOOL(true)]));
});

test.concurrent('gen_rng should reject when null is provided as a seed', async () => {
await expect(() => exe('Math:gen_rng(null)')).rejects.toThrow(AiScriptRuntimeError);
});
Expand Down Expand Up @@ -202,7 +231,7 @@ describe('Obj', () => {

<: Obj:merge(o1, o2)
`);
eq(res, utils.jsToVal({ a: 1, b: 3, c: 4}));
eq(res, utils.jsToVal({ a: 1, b: 3, c: 4 }));
});

test.concurrent('pick', async () => {
Expand Down
3 changes: 3 additions & 0 deletions unreleased/chacha20-seed-unexpected-rounding-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Fix: **Breaking Change** `Math:gen_rng`の`seed`に`num`を与え、`options.algorithm`に`chacha20`を指定した或いは何も指定しなかった場合、`seed & 255`を内部的に`seed`としてしまう問題を修正。
- 関数`Math:gen_rng`の`options.chacha20NumberSeedLegacyBehaviour`に`true`を指定した場合、修正前の動作をする機能を追加(デフォルト:`false`)。
- これらの修正により、同じ`seed`でも修正前と修正後で生成される値が異なるようになります。
Loading