aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/evaluation.test.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes/workbench/evaluation.test.mjs')
-rw-r--r--prototypes/workbench/evaluation.test.mjs133
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);
+});