tree-export.test.mjs (5762B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import { readFile } from 'node:fs/promises'; 4 import { COMPILER_BUNDLE_LIMIT, decodeBundle } from '../dist/bundle.js'; 5 import { exportTree, treeFormat, FLAT_EXPORT_LIMIT, DAG_EXPORT_LIMIT } from '../dist/tree-export.js'; 6 import { makeBundle } from './bundle-fixture.mjs'; 7 // Independent structural readers. Application is constructor assembly only: 8 // a third argument to a fork would constitute evaluation and fails these tests. 9 const leaf = []; 10 function apply(fn, arg) { assert.ok(fn.length < 2); return [...fn, arg]; } 11 function dag(text) { 12 const env = new Map([['△', leaf]]); 13 for (const line of text.split('\n')) { 14 if (!line) continue; 15 const words = line.split(' '); assert.ok(words.every(Boolean)); 16 const get = name => { assert.ok(env.has(name), name); return env.get(name); }; 17 if (words.length === 1) return get(words[0]); 18 assert.ok(words.length === 2 || words.length === 3); 19 env.set(words[0], words.length === 2 ? get(words[1]) : apply(get(words[1]), get(words[2]))); 20 } 21 assert.fail('Missing final value'); 22 } 23 function expression(text, symbol) { 24 const tokens = text.match(/△|t|\(|\)/gu); let at = 0; 25 function read() { 26 const token = tokens[at++]; 27 if (token === symbol) return leaf; 28 assert.equal(token, '('); let result = read(); 29 while (tokens[at] !== ')') result = apply(result, read()); 30 at++; return result; 31 } 32 const result = read(); assert.equal(at, tokens.length); return result; 33 } 34 function ternary(text) { 35 let at = 0; 36 function read() { const n = Number(text[at++]); assert.ok(n >= 0 && n <= 2); return Array.from({length:n}, read); } 37 const result = read(); assert.equal(at,text.length); return result; 38 } 39 const bundle = records => decodeBundle(makeBundle(records,[['main',records.length-1],['alias',records.length-1]])); 40 test('leaf, stem, fork and identity export exact stored trees in every format', () => { 41 for (const [records, expected] of [[ [[]], '0'], [[[],[0]],'10'], [[[],[0,0]],'200'], [[[],[0],[1],[2,0]],'21100']]) { 42 const b = bundle(records), want = ternary(expected), original = JSON.stringify(b); 43 for (const format of ['ternary','tricu','triangle','dag']) { 44 const file = exportTree(b,'main',format); 45 assert.equal(file.bytes, Buffer.byteLength(file.text)); 46 assert.deepEqual(format === 'ternary' ? ternary(file.text) : format === 'dag' ? dag(file.text) : expression(file.text,format === 'tricu' ? 't' : '△'),want); 47 assert.deepEqual(exportTree(b,'alias',format),file); 48 } 49 assert.equal(exportTree(b,'main','ternary').text,expected); 50 assert.equal(JSON.stringify(b),original); 51 } 52 const b=bundle([[],[0],[1],[2,0]]); 53 assert.equal(exportTree(b,'main','tricu').text,'(t (t (t t)) t)'); 54 }); 55 test('DAG exports only reachable records, handles arbitrary names without embedding them, preserves sharing', () => { 56 const b=decodeBundle(makeBundle([[],[0],[1],[0,0],[3,3]],[['x\n△ hacked',4],['unused',2]])); 57 const file=exportTree(b,'x\n△ hacked','dag'), tree=dag(file.text); 58 assert.equal(file.storedNodes,3); assert.equal(tree[0],tree[1]); 59 assert.ok(!file.text.includes('hacked'));assert.ok(!file.text.includes(':n1')); 60 assert.match(file.text,/\ntree :n4\ntree\n$/u); 61 }); 62 test('expanded size preflight rejects exponential trees before unfolding, DAG remains linear', () => { 63 const records=[[]];for(let i=1;i<64;i++)records.push([i-1,i-1]); 64 const b=bundle(records), file=exportTree(b,'main','dag'); 65 assert.ok(file.bytes<4000);assert.equal(file.storedNodes,64); 66 const tree=dag(file.text);assert.equal(tree[0],tree[1]); 67 for(const format of ['ternary','tricu','triangle']) assert.throws(()=>exportTree(b,'main',format),/32 MiB.*DAG/); 68 assert.throws(()=>exportTree(b,'missing','dag'),/Unknown export/); 69 for(const format of ['json','',null]) assert.throws(()=>treeFormat(format),/format/); 70 }); 71 test('deep exports use iterative traversal, UTF-8 byte budgets and complete nontruncated boundaries', () => { 72 assert.equal(FLAT_EXPORT_LIMIT,32*1024*1024);assert.equal(DAG_EXPORT_LIMIT,128*1024*1024); 73 const records=[[]];for(let i=1;i<20000;i++)records.push([i-1]); 74 const b=bundle(records); 75 for(const format of ['ternary','tricu','triangle','dag']) { 76 const file=exportTree(b,'main',format);assert.equal(file.bytes,Buffer.byteLength(file.text)); 77 assert.equal(file.storedNodes,20000); 78 } 79 const doubled=[[]];for(let i=1;i<=24;i++)doubled.push([i-1,i-1]); 80 const exact=exportTree(bundle(doubled),'main','ternary');assert.equal(exact.bytes,FLAT_EXPORT_LIMIT-1); 81 assert.equal(exact.text.length,exact.bytes);assert.match(exact.text,/^[012]+$/u); 82 assert.throws(()=>exportTree(bundle([...doubled,[24]]),'main','triangle'),/32 MiB/); 83 assert.throws(()=>exportTree(bundle([...doubled,[24,24]]),'main','ternary'),/32 MiB.*DAG/); 84 }); 85 test('every curated definition and compiler definition has bounded DAG output without evaluating', async () => { 86 const entries=JSON.parse(await readFile(new URL('../catalogue.json',import.meta.url))); 87 entries.push(JSON.parse(await readFile(new URL('../compiler.json',import.meta.url)))); 88 for(const hash of new Set(entries.map(e=>e.hash))) { 89 const b=decodeBundle(await readFile(new URL('../bundles/'+hash+'.arboricx',import.meta.url)),COMPILER_BUNDLE_LIMIT); 90 // Largest compiler exports plus all demo definitions, not repeated all compiler traversals. 91 const names=b.exports.length>100 ? ['source_unit.compileStableModuleDefinitions','base.id','base.y'] : b.exports.map(e=>e.name); 92 for(const name of names) { 93 const file=exportTree(b,name,'dag');assert.equal(file.bytes,Buffer.byteLength(file.text)); 94 assert.ok(file.storedNodes<=b.nodes.length);dag(file.text); 95 } 96 } 97 });