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