graph.test.mjs (9118B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import { readFile } from 'node:fs/promises'; 4 import {BROWSER_BUDGET,collectionPolicy,PACKAGE_COLLECTION} from '../dist/execution.js'; 5 import { Graph } from '../dist/graph.js'; 6 import { natural, readNatural, text, readText, line, literals, makeLiteral, view } from '../dist/codecs.js'; 7 import { runIO } from '../dist/io.js'; 8 import { decodeBundle, loadBundle } from '../dist/bundle.js'; 9 const data = JSON.parse(await readFile(new URL('../demos.json', import.meta.url))); 10 for(const id of ['parser','lexer'])data.demos[id]=JSON.parse(await readFile(new URL('../'+id+'-demo.json',import.meta.url))); 11 function host(input = '') { 12 const state = { terminal: '', trace: [] }; 13 return { state, before: async event => state.trace.push(event), after() {}, running() {}, 14 read: async () => input, write: s => { state.terminal += s; } }; 15 } 16 test('Haskell-authored codecs and all primitive application rules', () => { 17 for (const vector of data.codecVectors) { 18 const g = new Graph(); 19 const id = vector.kind === 'number' ? natural(g, BigInt(vector.value)) : text(g, vector.value); 20 assert.equal(g.toTernary(id), vector.tree); 21 assert.equal(vector.kind === 'number' ? Number(readNatural(g, id)) : readText(g, id), vector.value); 22 } 23 for (const vector of data.applyVectors) { 24 const g = new Graph(); 25 assert.equal(g.toTernary(g.app(g.fromTernary(vector.program), g.fromTernary(vector.argument))), vector.result); 26 } 27 }); 28 test('300 independently captured Haskell differential cases', async () => { 29 const vectors=JSON.parse(await readFile(new URL('./conformance.json',import.meta.url))); 30 assert.equal(vectors.cases.length,300); 31 for(const c of vectors.cases){ 32 const g=new Graph(); 33 assert.equal(g.toTernary(g.apply(g.fromTernary(c.program),c.arguments.map(x=>g.fromTernary(x)))),c.result); 34 } 35 }); 36 test('normal order: Haskell discarded-divergence control now returns Leaf', () => { 37 const g = new Graph({ fuel: 1000 }); 38 const f = g.fromTernary('2120200212110021100'), q = g.fromTernary('212110021100'); 39 assert.equal(g.toTernary(g.app(f, q)), '0'); 40 assert.ok(g.stats.work < 150); 41 }); 42 test('partial applications retain lazy children; demanding divergence is bounded', () => { 43 const g = new Graph({ fuel: 2000 }); 44 const q = g.fromTernary('212110021100'), loop = g.app(q, q); 45 const partial = g.app(g.leaf, loop); 46 const {node} = g.observe(partial); 47 assert.equal(node.kind, 'stem'); assert.equal(node.child, loop); 48 assert.throws(() => g.force(loop), /fuel/); 49 }); 50 test('32-node /four-billion-occurrence graph survives identity and constant without expansion', () => { 51 const g = new Graph(); let root = g.leaf; 52 for (let i = 0; i < 31; i++) root = g.fork(root, root); 53 assert.equal(g.stats.nodes, 32); 54 const work = g.stats.work; 55 const shape = g.observe(root).node; 56 assert.equal(shape.left, shape.right); assert.equal(g.stats.work - work, 1); 57 const I = g.fromTernary('21100'), KLeaf = g.fromTernary('200'); 58 assert.equal(g.force(g.app(I, root)), root); 59 assert.equal(g.force(g.app(KLeaf, root)), g.leaf); 60 assert.ok(g.stats.nodes < 64); assert.ok(g.stats.work < 200); 61 const preview = g.preview(root); assert.equal(preview.nodes.length, 32); 62 assert.throws(() => g.toTernary(root, 100), /expansion/); 63 }); 64 test('memoization preserves constructor data and avoids repeated application work', () => { 65 const g = new Graph(); const saved = g.fromTernary('200'); 66 const id = g.app(g.fromTernary('21100'), saved); 67 assert.equal(g.force(id), saved); 68 const before = g.stats; assert.equal(g.force(id), saved); 69 assert.equal(g.stats.nodes, before.nodes); assert.ok(g.stats.work - before.work <= 2); 70 assert.equal(g.toTernary(saved), '200'); 71 }); 72 for (const [name, demo] of Object.entries(data.demos)) { 73 test(`${name}: packaged stable exports match ${demo.haskellCases.length} Haskell goldens`, async () => { 74 const bundle=decodeBundle(await readFile(new URL(`../bundles/${demo.hash}.arboricx`,import.meta.url))); 75 for (const golden of demo.haskellCases) { 76 const budget=BROWSER_BUDGET; 77 const g = new Graph(budget,collectionPolicy(budget,PACKAGE_COLLECTION.threshold)), h = host(golden.input); 78 const exports=loadBundle(g,bundle); 79 const applied=g.apply(exports.get(golden.export),golden.arguments.map(x=>g.fromTernary(x))); 80 const result=demo.mode==='io'?await runIO(g,applied,h):applied; 81 assert.equal(g.toTernary(result), golden.result); 82 assert.equal(h.state.terminal, golden.terminal); 83 if(golden.record)assert.deepEqual({...view(g,result,'record').value},golden.record); 84 } 85 }); 86 } 87 test('preview records forwarding aliases without broken child references', () => { 88 const g=new Graph(), delayed=g.app(g.fromTernary('21100'),g.leaf); 89 const root=g.stem(delayed), preview=g.preview(root); 90 assert.deepEqual(preview.aliases,[{from:delayed,to:g.leaf}]); 91 assert.equal(preview.nodes.find(n=>n.id===root).children[0],delayed); 92 }); 93 test('malformed codecs/trees, typed literals, allocations, stack and preview budgets', () => { 94 const g = new Graph(); 95 for (const value of ['', '1', '20', '00', 'x']) assert.throws(() => g.fromTernary(value)); 96 assert.throws(() => text(g, '\ud800'), /surrogate/); 97 assert.throws(() => readText(g, g.fork(natural(g, 0xd800n), g.leaf)), /Unicode/); 98 assert.throws(() => line(g, 'x'.repeat(257)), /limit/); 99 assert.throws(() => literals([{ type: 'number', value: '-1' }])); 100 assert.throws(() => literals([{ type: 'bytes', value: [256] }])); 101 assert.throws(() => literals([{ type: 'lambda', value: 'x:x' }])); 102 let deep = { type: 'bool', value: false }; 103 for(let i=0;i<18;i++) deep={type:'list',value:[deep]}; 104 assert.throws(() => literals([deep]), /nesting/); 105 const values=literals([{type:'list',value:[{type:'number',value:'42'},{type:'bool',value:true}]}]); 106 assert.equal(view(g,makeLiteral(g,values[0]),'list').value.length,2); 107 assert.equal(view(g,natural(g,2n**100n),'number').value,(2n**100n).toString()); 108 assert.throws(() => g.preview(g.leaf, 257), /budget/); 109 const tiny = new Graph({ nodes: 2 }); tiny.stem(tiny.leaf); 110 assert.throws(() => tiny.stem(tiny.leaf), /allocation/); 111 const shallow = new Graph({ stack: 1 }); 112 assert.throws(() => shallow.force(shallow.app(shallow.leaf,shallow.leaf)), /stack/); 113 }); 114 test('deep transport and graph observation do not use recursive JS evaluation', () => { 115 const g = new Graph(); const term='1'.repeat(10000)+'0'; 116 const root=g.fromTernary(term,term.length); 117 assert.equal(g.toTernary(root,term.length),term); 118 }); 119 test('IO rejects unknown tags/envelopes; pausing happens before effects', async () => { 120 const g=new Graph(), h=host(); 121 const action=(tag,payload=g.leaf)=>g.fork(natural(g,BigInt(tag)),payload); 122 const io=action=>g.fork(text(g,'tricuIO'),g.fork(natural(g,1n),action)); 123 await assert.rejects(runIO(g,io(action(20)),h),/Unsupported/); 124 await assert.rejects(runIO(g,g.leaf,h),/envelope/); 125 let resume, wrote=false; 126 h.before=()=>new Promise(resolve=>{resume=resolve;});h.write=()=>{wrote=true;}; 127 const pending=runIO(g,io(action(10,text(g,'hi'))),h); 128 await Promise.resolve();assert.equal(wrote,false);resume();await pending;assert.equal(wrote,true); 129 }); 130 test('IO action/output budgets and abort do not force arbitrary payloads',async()=>{ 131 for(const output of [false,true]){ 132 const g=new Graph(),h=host(); 133 const action=(tag,payload=g.leaf)=>g.fork(natural(g,BigInt(tag)),payload); 134 const primitive=output?action(10,text(g,'x'.repeat(4096))):action(0); 135 let current=action(0); 136 for(let i=0;i<(output?6:70);i++)current=action(1,g.fork(primitive,g.fork(g.leaf,current))); 137 const io=g.fork(text(g,'tricuIO'),g.fork(natural(g,1n),current)); 138 await assert.rejects(runIO(g,io,h),output?/output limit/:/action limit/); 139 assert.ok(h.state.terminal.length<=16384); 140 } 141 const g=new Graph({fuel:1000}),q=g.fromTernary('212110021100'),loop=g.app(q,q); 142 const abort=g.fork(natural(g,2n),loop),io=g.fork(text(g,'tricuIO'),g.fork(natural(g,1n),abort)); 143 await assert.rejects(runIO(g,io,host()),/Execution aborted/); 144 assert.ok(g.stats.work<500); 145 }); 146 147 test('draft checkpoints preserve sharing and lazy poison across failed disposable runs',()=>{ 148 const g=new Graph({fuel:10000}); 149 const q=g.fromTernary('212110021100'), loop=g.app(q,q); 150 const partial=g.stem(loop), shared=g.fork(partial,partial); 151 g.collect([shared]);const stats={...g.stats}; 152 const checkpoint=structuredClone(g.checkpoint()); 153 assert.deepEqual(g.stats,stats,'capturing does not evaluate or spend fuel'); 154 const attempt=()=>Graph.restore(checkpoint,{fuel:500,nodes:10000,stack:100}); 155 const first=attempt();assert.throws(()=>first.force(loop),/limit/); 156 const next=attempt(); 157 assert.deepEqual(next.observe(shared).node,{kind:'fork',left:partial,right:partial}); 158 assert.deepEqual(next.observe(partial).node,{kind:'stem',child:loop}); 159 assert.deepEqual(next.checkpoint(),checkpoint,'failed run never mutates checkpoint'); 160 assert.throws(()=>Graph.restore(checkpoint,{nodes:1}),/allocation/); 161 assert.throws(()=>next.force(loop),/limit/); 162 });