gc.test.mjs (3286B)
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 test('tracing preserves sharing and stable handles; stale IDs are never reused',()=>{ 7 const g=new Graph({fuel:10000}),garbage=g.stem(g.leaf);let root=g.leaf; 8 for(let i=0;i<31;i++)root=g.fork(root,root); 9 const before=g.preview(root);g.collect([root]); 10 assert.equal(g.stats.nodes,32);assert.equal(g.stats.reclaimed,1); 11 assert.deepEqual(g.preview(root),before); 12 const fresh=g.stem(g.leaf);assert.notEqual(fresh,garbage); 13 assert.throws(()=>g.observe(garbage),/Invalid node handle/); 14 assert.equal(g.observe(root).node.left,g.observe(root).node.right); 15 }); 16 test('marking traces application/ref edges without forcing discarded divergence',()=>{ 17 const g=new Graph({fuel:10000}),q=g.fromTernary('212110021100'),loop=g.app(q,q); 18 const value=g.stem(loop);g.collect([value]);assert.equal(g.observe(value).node.child,loop); 19 const result=g.app(g.fromTernary('200'),value); 20 assert.equal(g.force(result),g.leaf);g.collect([result]); 21 assert.equal(g.stats.nodes,2);assert.equal(g.force(result),g.leaf); 22 }); 23 test('automatic reducer collections preserve every pre-entry caller handle',()=>{ 24 const g=new Graph({nodes:1000,fuel:100000},{threshold:8}); 25 const caller=g.fromTernary('2100'),I=g.fromTernary('21100'); 26 let app=g.leaf;for(let i=0;i<60;i++)app=g.app(I,app); 27 assert.equal(g.force(app),g.leaf);assert.equal(g.toTernary(caller),'2100'); 28 assert.ok(g.stats.collections>0);g.collect([app,caller]); 29 assert.ok(g.stats.reclaimed>50);assert.equal(g.force(app),g.leaf); 30 }); 31 test('GC-enabled evaluator matches all 300 Haskell cases including all frame variants',async()=>{ 32 const corpus=JSON.parse(await readFile(new URL('./conformance.json',import.meta.url))); 33 for(const c of corpus.cases){ 34 const g=new Graph({nodes:1000,fuel:100000},{threshold:8}); 35 const fn=g.fromTernary(c.program),args=c.arguments.map(x=>g.fromTernary(x)); 36 const result=g.apply(fn,args);assert.equal(g.toTernary(result),c.result); 37 g.collect([result]);assert.equal(g.toTernary(result),c.result); 38 } 39 }); 40 test('collection is budgeted and commits atomically; no high-survival trace storm',()=>{ 41 const g=new Graph({fuel:5}),value=g.stem(g.leaf); 42 assert.throws(()=>g.collect([value]),/fuel/);assert.equal(g.stats.nodes,2);assert.equal(g.stats.collections,0); 43 const high=new Graph({nodes:40,fuel:2000},{threshold:2});let root=high.leaf; 44 for(let i=0;i<20;i++)root=high.stem(root); 45 assert.equal(high.force(root),root);assert.ok(high.stats.collections<=2); 46 }); 47 test('explicit Infinity profile removes graph/fuel/stack ceilings without changing defaults',()=>{ 48 const bounded=new Graph({nodes:2,fuel:100,stack:1});bounded.stem(bounded.leaf); 49 assert.throws(()=>bounded.stem(bounded.leaf),/allocation/); 50 const g=new Graph({nodes:Infinity,fuel:Infinity,stack:Infinity},{threshold:8}); 51 const I=g.fromTernary('21100');let root=g.leaf; 52 for(let i=0;i<100;i++)root=g.app(I,root); 53 assert.equal(g.force(root),g.leaf);assert.equal(g.stats.fuel,Infinity); 54 assert.ok(g.stats.collections>0);assert.ok(g.stats.work>100); 55 for(const fuel of [NaN,-Infinity,0,-1,1.5])assert.throws(()=>new Graph({fuel}),/budget/); 56 });