tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 });