From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- prototypes/workbench/rules.test.mjs | 94 +++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 prototypes/workbench/rules.test.mjs (limited to 'prototypes/workbench/rules.test.mjs') diff --git a/prototypes/workbench/rules.test.mjs b/prototypes/workbench/rules.test.mjs new file mode 100644 index 0000000..b4c4969 --- /dev/null +++ b/prototypes/workbench/rules.test.mjs @@ -0,0 +1,94 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import {compile} from './web/compiler.mjs'; +import {createRuntime} from './web/runtime.mjs'; +import {parseProject} from './web/project.mjs'; +const wasm=await readFile(new URL('./web/core.wasm',import.meta.url)); +const shared=await readFile(new URL('./examples/shared-layout.pure',import.meta.url),'utf8'); +const explicit=await readFile(new URL('./examples/layout.pure',import.meta.url),'utf8'); +const fresh=()=>createRuntime(wasm,{cacheEntries:32}); +const named=['green','blue','amber']; + +function simple(n){return 'record Cell { value=1 }\ninput tap=Space\nrule inc(t: Cell) { t.value=t.value+1 }\n'+Array.from({length:n},(_,i)=>`item cell${i}=Cell()\non tap.down use inc(cell${i})`).join('\n');} + +test('binding more objects shares one VM body instead of expanding its instructions',()=>{ + for(const n of [1,2,4,8]){ + const p=compile(simple(n)); + assert.equal(p.instructions.length,4);assert.equal(p.handlers.length,n); + assert.deepEqual(p.handlers.map(h=>h.start),Array(n).fill(0)); + assert.deepEqual(p.handlers.map(h=>[h.parameterBase,h.parameterCount]),Array.from({length:n},(_,i)=>[i,1])); + } + assert.equal(compile('record R { value=0 }\nrule unused(t:R) { t.value=t.value+1 }').instructions.length,0); +}); + +test('records and rules resolve forward names and reject invalid types fields and bindings',()=>{ + const p=compile('on tap.down use inc(a)\nitem a=R(v=4)\ninput tap=Space\nrule inc(t:R) { t.v=t.v+1 }\nrecord R { v=0 }'); + assert.equal(p.states[0].name,'a.v');assert.deepEqual([p.handlers[0].parameterBase,p.handlers[0].parameterCount],[0,1]); + for(const code of [ + 'record R {}','record R { x=0; x=1 }','record R { event_x=0 }', + 'item a=Missing()','record R {x=0}\nitem a=R(y=1)', + 'record R {x=0}\nitem a=R(x=1,x=2)','record R {x=0}\nstate a=0\nitem a=R()', + 'record R {x=0}\nrule bad(t:R) {t.missing=0}', + 'record R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {t.y=1}', + 'state value=0\nrecord R {x=0}\nrecord S {y=0}\nitem t=S()\nrule bad(t:R) {value=t.y}', + 'rule bad(t:Missing) {}','record R {x=0}\nitem a=R()\ninput k=Space\non k.down use missing(a)', + 'record R {x=0}\nrule r(t:R) {}\ninput k=Space\non k.down use r(missing)', + 'record R {x=0}\nrecord S {x=0}\nitem a=S()\nrule r(t:R) {}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_x}\ninput k=Space\non k.down use r(a)', + 'record R {x=0}\nitem a=R()\nrule r(t:R) {t.x=event_y}\non frame use r(a)', + 'record R {x=0}\nitem a=R()\ntext label(0,0,t.x)', + `record R { ${Array.from({length:9},(_,i)=>`f${i}=0`).join(';')} }`, + ]) assert.throws(()=>compile(code),code); + assert.throws(()=>compile(simple(33)),/32 states/); +}); + +test('shared layout matches the existing authored layout across independent and simultaneous gestures',async()=>{ + const a=await fresh(),b=await fresh();a.apply(explicit);b.apply(shared); + const compare=()=>{ + const old=a.snapshot(),next=b.snapshot();assert.equal(next.lastError,old.lastError); + assert.equal(next.states.selected,old.states.selected); + for(let i=1;i<=3;i++)for(const field of ['x','y','ox','oy','sx','sy','held'])assert.equal(next.states[`${named[i-1]}.${field}`],old.states[`${field}${i}`]); + for(let i=0;i{a.point(`drag${i}`,phase,x,y,`p${i}`);b.point(`drag_${named[i-1]}`,phase,x,y,`p${i}`);compare();}; + compare();point(1,'down',90,130);point(2,'down',290,170);point(1,'move',350,200);point(2,'move',500,300);point(2,'up',290,170);point(1,'cancel',350,200); + for(const key of ['left','right','up','down']){a.down(key);b.down(key);a.up(key);b.up(key);compare();} + point(3,'down',460,230);point(3,'move',900,500);point(3,'up',460,230); + const saved=JSON.parse(JSON.stringify(b.exportDocument()));const c=await fresh();c.restoreDocument(saved); + assert.deepEqual(c.snapshot().states,b.snapshot().states);assert.deepEqual(c.snapshot().views,b.snapshot().views); + c.down('left');c.up('left');assert.equal(c.snapshot().states['amber.x'],b.snapshot().states['amber.x']-10); +}); + +test('field names retain identity across schema and instance reorder, removal and restoration',async()=>{ + const r=await fresh(); + const source='record R { x=1;y=2 }\nitem a=R(x=10)\nitem b=R(x=20)\ninput k=Space\nrule step(t:R) {t.x=t.x+1}\non k.down use step(b)'; + r.apply(source);r.down('k');r.up('k'); + r.apply(source.replace('x=1;y=2','y=2;x=1').replace('item a=R(x=10)\nitem b=R(x=20)','item b=R(x=20)\nitem a=R(x=10)')); + r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],22);assert.equal(r.snapshot().states['a.x'],10); + r.apply(source.replace('item a=R(x=10)\n',''));r.down('k');r.up('k');assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); + const saved=r.exportDocument();r.apply(source);assert.equal(r.snapshot().states['a.x'],10); + r.restoreDocument(saved);assert.equal(r.snapshot().states['b.x'],23);assert(!Object.hasOwn(r.snapshot().states,'a.x')); +}); + +test('bindings share one event transaction including global effects and multiple uses of one item',async()=>{ + const r=await fresh();r.apply('state total=0\nrecord R {x=8;divisor=2}\nitem a=R()\nitem b=R(divisor=0)\ninput k=Space\nrule divide(t:R) {total=total+1\nt.x=t.x/t.divisor}\non k.down use divide(a)\non k.down use divide(b)'); + r.down('k');assert.equal(r.snapshot().lastError,4);assert.equal(r.snapshot().states.total,0);assert.equal(r.snapshot().states['a.x'],8); + r.up('k');r.apply(simple(1)+'\non tap.down use inc(cell0)');r.down('tap');r.up('tap');assert.equal(r.snapshot().states['cell0.value'],3); +}); + +test('bound writes invalidate affected display caches and failed edits preserve active bindings',async()=>{ + const r=await fresh();const source=simple(2)+'\ntext first(0,0,cell0.value)\ntext second(0,0,cell1.value)';r.apply(source);r.snapshot();r.snapshot(); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[2,2]); + const before=r.snapshot();assert.throws(()=>r.apply(source.replace('inc(cell1)','inc(missing)')));assert.deepEqual(r.snapshot(),before); + r.down('tap');r.up('tap');assert.deepEqual(r.snapshot().views.map(v=>v.value),[3,3]); +}); + +test('generated qualified names cannot make a valid exported document unreadable by its own loader',async()=>{ + const field='f'.repeat(18000); + const source=`record R { ${field}=1 }\n`+Array.from({length:32},(_,i)=>`item i${i}=R()`).join('\n'); + const r=await fresh();r.apply(source); + const saved=JSON.stringify(r.exportDocument('\u0001'.repeat(32768))); + r.restoreDocument(parseProject(saved)); + assert.equal(r.snapshot().states[`i31.${field}`],1); +}); -- cgit v1.2.3