tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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