1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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);
});
|