gc.test.mjs (5126B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {readFile} from 'node:fs/promises'; 4 import {Graph} from '../dist/graph.js'; 5 import {text,natural,readText} from '../dist/codecs.js'; 6 import {runIO} from '../dist/io.js'; 7 test('tracing preserves sharing and stable handles; stale IDs are never reused',()=>{ 8 const g=new Graph({fuel:10000}),garbage=g.stem(g.leaf);let root=g.leaf; 9 for(let i=0;i<31;i++)root=g.fork(root,root); 10 const before=g.preview(root);g.collect([root]); 11 assert.equal(g.stats.nodes,32);assert.equal(g.stats.reclaimed,1); 12 assert.deepEqual(g.preview(root),before); 13 const fresh=g.stem(g.leaf);assert.notEqual(fresh,garbage); 14 assert.throws(()=>g.observe(garbage),/Invalid node handle/); 15 assert.equal(g.observe(root).node.left,g.observe(root).node.right); 16 }); 17 test('marking traces application/ref edges without forcing discarded divergence',()=>{ 18 const g=new Graph({fuel:10000}),q=g.fromTernary('212110021100'),loop=g.app(q,q); 19 const value=g.stem(loop);g.collect([value]);assert.equal(g.observe(value).node.child,loop); 20 const result=g.app(g.fromTernary('200'),value); 21 assert.equal(g.force(result),g.leaf);g.collect([result]); 22 assert.equal(g.stats.nodes,2);assert.equal(g.force(result),g.leaf); 23 }); 24 test('automatic reducer collections preserve every pre-entry caller handle',()=>{ 25 const g=new Graph({nodes:1000,fuel:100000},{threshold:8}); 26 const caller=g.fromTernary('2100'),I=g.fromTernary('21100'); 27 let app=g.leaf;for(let i=0;i<60;i++)app=g.app(I,app); 28 assert.equal(g.force(app),g.leaf);assert.equal(g.toTernary(caller),'2100'); 29 assert.ok(g.stats.collections>0);g.collect([app,caller]); 30 assert.ok(g.stats.reclaimed>50);assert.equal(g.force(app),g.leaf); 31 }); 32 test('GC-enabled evaluator matches all 300 Haskell cases including all frame variants',async()=>{ 33 const corpus=JSON.parse(await readFile(new URL('./conformance.json',import.meta.url))); 34 for(const c of corpus.cases){ 35 const g=new Graph({nodes:1000,fuel:100000},{threshold:8}); 36 const fn=g.fromTernary(c.program),args=c.arguments.map(x=>g.fromTernary(x)); 37 const result=g.apply(fn,args);assert.equal(g.toTernary(result),c.result); 38 g.collect([result]);assert.equal(g.toTernary(result),c.result); 39 } 40 }); 41 test('collection is budgeted and commits atomically; no high-survival trace storm',()=>{ 42 const g=new Graph({fuel:5}),value=g.stem(g.leaf); 43 assert.throws(()=>g.collect([value]),/fuel/);assert.equal(g.stats.nodes,2);assert.equal(g.stats.collections,0); 44 const high=new Graph({nodes:40,fuel:2000},{threshold:2});let root=high.leaf; 45 for(let i=0;i<20;i++)root=high.stem(root); 46 assert.equal(high.force(root),root);assert.ok(high.stats.collections<=2); 47 }); 48 test('owned IO collection retains saved continuations and input across pauses',async()=>{ 49 const g=new Graph({fuel:100000},{threshold:32}); 50 const action=(tag,payload=g.leaf)=>g.fork(natural(g,BigInt(tag)),payload); 51 const pure=g.fork(g.leaf,natural(g,0n)); // K Leaf; the continuation ignores line data. 52 const bind=action(1,g.fork(action(11),g.fork(g.leaf,action(0,text(g,'done'))))); 53 const io=g.fork(text(g,'tricuIO'),g.fork(natural(g,1n),bind)); 54 let resolve;const read=new Promise(r=>{resolve=r;});let waiting=false; 55 const resultPromise=runIO(g,io,{before:async()=>{},after(){},write(){},running(){},read(){waiting=true;return read;}},true); 56 while(!waiting)await Promise.resolve(); 57 assert.ok(g.stats.collections>=2);resolve('λ雪🙂'); 58 const result=await resultPromise;assert.equal(readText(g,result),'done'); 59 // A previously unrelated caller handle is invalidated only by the explicit 60 // owned-IO contract, never by automatic reducer collections. 61 assert.throws(()=>g.observe(pure),/Invalid node handle/); 62 }); 63 test('explicit Infinity profile removes graph/fuel/stack ceilings without changing defaults',()=>{ 64 const bounded=new Graph({nodes:2,fuel:100,stack:1});bounded.stem(bounded.leaf); 65 assert.throws(()=>bounded.stem(bounded.leaf),/allocation/); 66 const g=new Graph({nodes:Infinity,fuel:Infinity,stack:Infinity},{threshold:8}); 67 const I=g.fromTernary('21100');let root=g.leaf; 68 for(let i=0;i<100;i++)root=g.app(I,root); 69 assert.equal(g.force(root),g.leaf);assert.equal(g.stats.fuel,Infinity); 70 assert.ok(g.stats.collections>0);assert.ok(g.stats.work>100); 71 for(const fuel of [NaN,-Infinity,0,-1,1.5])assert.throws(()=>new Graph({fuel}),/budget/); 72 }); 73 test('owned IO extra roots preserve compiled definitions for repeated input runs',async()=>{ 74 const g=new Graph({fuel:100000},{threshold:32}); 75 const definition=g.fork(text(g,'tricuIO'),g.fork(natural(g,1n),g.fork(natural(g,11n),g.leaf))); 76 const identity=g.fromTernary('21100');const roots=[definition,identity]; 77 let reads=0; 78 for(const input of ['first','λ雪🙂','third']){ 79 const result=await runIO(g,definition,{before:async()=>{},after(){},write(){},running(){},read:async()=>{reads++;return input;}},true,roots.values()); 80 assert.equal(readText(g,result),input);g.collect(roots); 81 assert.equal(g.toTernary(identity),'21100'); 82 } 83 assert.equal(reads,3);assert.ok(g.stats.collections>=6); 84 assert.ok(g.stats.fuel<100000); // Work stays cumulative across replays. 85 });