diff --git a/src/interpreter/lib/math.ts b/src/interpreter/lib/math.ts index 691753b0..a4c75d2d 100644 --- a/src/interpreter/lib/math.ts +++ b/src/interpreter/lib/math.ts @@ -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`.'); diff --git a/src/utils/random/genrng.ts b/src/utils/random/genrng.ts index fff2ee9e..bb988d02 100644 --- a/src/utils/random/genrng.ts +++ b/src/utils/random/genrng.ts @@ -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()); @@ -26,11 +26,17 @@ export function GenerateRC4Random(seed: VNum | VStr): VNativeFn { }); } -export async function GenerateChaCha20Random(seed: VNum | VStr): Promise { +export async function GenerateChaCha20Random(seed: VNum | VStr, options: Map | undefined): Promise { 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)))); } diff --git a/test/std.ts b/test/std.ts index 22598d13..105475dd 100644 --- a/test/std.ts +++ b/test/std.ts @@ -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'; @@ -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; @@ -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); }); @@ -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 () => { diff --git a/unreleased/chacha20-seed-unexpected-rounding-fix.md b/unreleased/chacha20-seed-unexpected-rounding-fix.md new file mode 100644 index 00000000..8387aa18 --- /dev/null +++ b/unreleased/chacha20-seed-unexpected-rounding-fix.md @@ -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`でも修正前と修正後で生成される値が異なるようになります。