Skip to content
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('chacha20_number_seed_legacy_behavior');
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.chacha20_number_seed_legacy_behavior`に`true`を指定した場合、修正前の動作をする機能を追加(デフォルト:`false`)。
- これらの修正により、同じ`seed`でも修正前と修正後で生成される値が異なるようになります。
Comment on lines +2 to +3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

インデントがスペースに戻っちゃいましたね……

Suggested change
- 関数`Math:gen_rng``options.chacha20_number_seed_legacy_behavior``true`を指定した場合、修正前の動作をする機能を追加(デフォルト:`false`)。
- これらの修正により、同じ`seed`でも修正前と修正後で生成される値が異なるようになります。
- 関数`Math:gen_rng`の`options.chacha20_number_seed_legacy_behavior`に`true`を指定した場合、修正前の動作をする機能を追加(デフォルト:`false`)。
- これらの修正により、同じ`seed`でも修正前と修正後で生成される値が異なるようになります。

Loading