diff options
| author | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
| commit | b1ce1927022d374198441afafeb27199fc8a052a (patch) | |
| tree | 6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/workbench/evaluation.test.mjs | |
| parent | d34f9426d5317a1948f12f229439946728bc342a (diff) | |
| download | pure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz pure-b1ce1927022d374198441afafeb27199fc8a052a.zip | |
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/workbench/evaluation.test.mjs')
| -rw-r--r-- | prototypes/workbench/evaluation.test.mjs | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/prototypes/workbench/evaluation.test.mjs b/prototypes/workbench/evaluation.test.mjs new file mode 100644 index 0000000..21a9590 --- /dev/null +++ b/prototypes/workbench/evaluation.test.mjs @@ -0,0 +1,133 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRuntime } from './web/runtime.mjs'; + +const wasm = await readFile(new URL('./web/core.wasm', import.meta.url)); +const make = (options = {}) => createRuntime(wasm, options); +const source = `state x = 3 +state spare = 0 +input next = KeyN +input other = KeyO +input fail = KeyF +text value(0, 0, (x + 1) * (x + 2) / 2) +on next.down { x = x + 0.25 } +on other.down { spare = spare + 1 } +on fail.down { x = 900; spare = 1 / 0 } +`; + +test('cache reuse, unrelated changes and discarded records preserve authored values', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + cached.apply(source); plain.apply(source); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(cached.evaluationStats().cacheHits > 0); + assert(cached.evaluationStats().instructions < plain.evaluationStats().instructions); + for (let i = 0; i < 100; i++) { + const name = i % 3 ? 'other' : 'next'; + for (const runtime of [cached, plain]) { runtime.down(name); runtime.up(name); } + if (i % 7 === 0) cached.forgetEvaluation('history'); + if (i % 11 === 0) cached.forgetEvaluation('values'); + if (i % 13 === 0) cached.forgetEvaluation('all'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + } + // Omit observations long enough to overwrite every change record. + for (let i = 0; i < 20; i++) for (const runtime of [cached, plain]) { + runtime.down('next'); runtime.up('next'); + } + assert.deepEqual(cached.snapshot(), plain.snapshot()); +}); + +test('committed changes only: failed events and failed edits preserve reusable output', async () => { + const cached = await make({ cacheEntries: 32 }); cached.apply(source); + const before = cached.snapshot(); + cached.down('fail'); cached.up('fail'); + const failed = cached.snapshot(); + assert.deepEqual(failed.states, before.states); + assert.deepEqual(failed.views, before.views); + assert.equal(failed.lastError, 4); + assert.equal(cached.evaluationStats().instructions, 0); + assert.throws(() => cached.apply('state x = missing')); + assert.deepEqual(cached.snapshot(), failed); + assert.equal(cached.evaluationStats().instructions, 0); +}); + +test('program edits, state reordering and reset cannot reuse old definitions', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + for (const r of [cached, plain]) { r.apply(source); r.down('next'); r.up('next'); r.snapshot(); } + const changed = source.replace('state x = 3\nstate spare = 0', 'state spare = 9\nstate x = 300') + .replace('(x + 1) * (x + 2) / 2', '(x + 10) * (x + 20) / 2'); + for (const r of [cached, plain]) r.apply(changed); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.reset(); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().states.x, 300); +}); + +test('finite float semantics include signed zero, high state bit and arithmetic errors', async () => { + const cached = await make({ cacheEntries: 32 }), plain = await make(); + const program = `${Array.from({ length: 32 }, (_, i) => `state s${i} = 0`).join('\n')} +input flip = KeyF +text sign(0, 0, s31) +text error(0, 0, 1 / s31) +on flip.down { s31 = -s31 } +on flip.up { s31 = 2 } +`; + for (const r of [cached, plain]) r.apply(program); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + for (const r of [cached, plain]) r.down('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert(Object.is(cached.snapshot().views[0].value, -0)); + assert.equal(cached.snapshot().displayError.code, 100); + for (const r of [cached, plain]) r.up('flip'); + assert.deepEqual(cached.snapshot(), plain.snapshot()); + assert.equal(cached.snapshot().displayError, null); +}); + +test('one budget covers the whole scene; failures publish no partial scene or state rollback', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + r.down('next'); r.up('next'); + const good = r.snapshot(); + r.configureEvaluation({ workLimit: 5 }); + const limited = r.snapshot(); + assert.equal(limited.lastError, 101); + assert.deepEqual(limited.views, []); + assert.deepEqual(limited.states, good.states); + assert.equal(limited.successfulEvents, good.successfulEvents); + assert(r.evaluationStats().work <= 5); + r.down('fail'); r.up('fail'); + assert.equal(r.snapshot().lastError, 4); + assert.equal(r.snapshot().displayError.code, 101); + r.configureEvaluation({ workLimit: 2048 }); + assert.deepEqual(r.snapshot().views, good.views); + assert.equal(r.snapshot().lastError, 4); +}); + +test('small caches, no history and every small work limit give exact results or explicit failure', async () => { + const plain = await make(); plain.apply(source); + for (const cacheEntries of [0, 1, 32]) for (const historyEntries of [0, 1, 8]) { + const r = await make({ cacheEntries, historyEntries }); r.apply(source); + for (let workLimit = 0; workLimit < 100; workLimit++) { + r.configureEvaluation({ workLimit }); + const actual = r.snapshot(); + assert(r.evaluationStats().work <= workLimit); + if (actual.lastError) { + assert.equal(actual.lastError, 101); + assert.deepEqual(actual.views, []); + } else assert.deepEqual(actual, plain.snapshot()); + } + } +}); + +test('invalid settings and forget names preserve policy and state', async () => { + const r = await make({ cacheEntries: 32 }); r.apply(source); + const before = r.snapshot(), policy = r.evaluationStats(); + for (const option of [{cacheEntries:33}, {historyEntries:9}, {workLimit:-1}, + {workLimit:0x100000000}, {workLimit:NaN}, {cacheEntries:0.5}]) { + assert.throws(() => r.configureEvaluation(option)); + assert.deepEqual(r.evaluationStats(), policy); + } + for (const name of ['unknown', '__proto__', 'constructor']) assert.throws(() => r.forgetEvaluation(name)); + assert.deepEqual(r.snapshot(), before); + assert.equal(r.snapshot().memory.linearBytes, 65536); +}); |
