tree-export.ts (4675B)
1 import type { Bundle } from './bundle.js'; 2 export type TreeFormat = 'ternary' | 'tricu' | 'triangle' | 'dag'; 3 export interface TreeFile { text: string; bytes: number; extension: string; storedNodes: number; } 4 export const FLAT_EXPORT_LIMIT = 33_554_432; 5 export const DAG_EXPORT_LIMIT = 134_217_728; 6 export function treeFormat(value: unknown): TreeFormat { 7 if (value !== 'ternary' && value !== 'tricu' && value !== 'triangle' && value !== 'dag') throw new Error('Unknown tree export format'); 8 return value; 9 } 10 // Only immutable, authenticated bundle records are read. No graph observation, 11 // application or reduction. Reachability and capped size calculation count DAG 12 // records, not unfolded occurrences; expanded writers run only after preflight. 13 export function exportTree(bundle: Bundle, name: string, format: TreeFormat): TreeFile { 14 format = treeFormat(format); 15 const selected = bundle.exports.find(item => item.name === name); 16 if (!selected) throw new Error('Unknown export'); 17 const reachable = new Set<number>(), todo = [selected.root]; 18 while (todo.length) { 19 const id = todo.pop()!; 20 if (reachable.has(id)) continue; 21 reachable.add(id); 22 const node = bundle.nodes[id]!; 23 if (node.kind === 'stem') todo.push(node.child); 24 else if (node.kind === 'fork') todo.push(node.left, node.right); 25 } 26 const ids = [...reachable].sort((a, b) => a - b); 27 // Bound the number of fragments, not just output bytes. One array entry per 28 // unfolded node costs far more memory than the resulting text for large trees. 29 const parts: string[] = [], chunk: string[] = []; let chunkLength = 0; 30 const flush = (): void => { if (chunk.length) parts.push(chunk.join('')); chunk.length = 0; chunkLength = 0; }; 31 const write = (text: string): void => { 32 chunk.push(text); chunkLength += text.length; 33 if (chunkLength >= 65_536) flush(); 34 }; 35 const complete = (): string => { flush(); return parts.join(''); }; 36 let bytes = 0; 37 const append = (text: string): void => { 38 const size = text.length + 2 * (text.split('△').length - 1); 39 bytes += size; 40 if (bytes > DAG_EXPORT_LIMIT) throw new Error('DAG export exceeds 128 MiB'); 41 write(text); 42 }; 43 if (format === 'dag') { 44 const keys = new Map<number, string>(); 45 for (const id of ids) { 46 const node = bundle.nodes[id]!, key = `:n${id}`; 47 if (node.kind === 'leaf') keys.set(id, '△'); 48 else { 49 const child = keys.get(node.kind === 'stem' ? node.child : node.left)!; 50 if (node.kind === 'stem') append(`${key} △ ${child}\n`); 51 else { 52 const intermediate = `:f${id}`, right = keys.get(node.right)!; 53 append(`${intermediate} △ ${child}\n`); 54 append(`${key} ${intermediate} ${right}\n`); 55 } 56 keys.set(id, key); 57 } 58 } 59 const root = keys.get(selected.root)!; 60 // A safe fixed interface name, independent of arbitrary bundle export names. 61 const ending = `tree ${root}\ntree\n`; 62 append(ending); 63 return { text: complete(), bytes, extension: 'dag', storedNodes: ids.length }; 64 } 65 const sizes = new Map<number, number>(), triangle = format === 'triangle'; 66 const symbol = triangle ? '△' : 't', symbolBytes = triangle ? 3 : 1; 67 for (const id of ids) { 68 const node = bundle.nodes[id]!; 69 let size: number; 70 if (format === 'ternary') size = 1 + (node.kind === 'leaf' ? 0 : node.kind === 'stem' 71 ? sizes.get(node.child)! : sizes.get(node.left)! + sizes.get(node.right)!); 72 else size = node.kind === 'leaf' ? symbolBytes : node.kind === 'stem' 73 ? symbolBytes + 3 + sizes.get(node.child)! 74 : symbolBytes + 4 + sizes.get(node.left)! + sizes.get(node.right)!; 75 sizes.set(id, Math.min(FLAT_EXPORT_LIMIT + 1, size)); 76 } 77 bytes = sizes.get(selected.root)!; 78 if (bytes > FLAT_EXPORT_LIMIT) throw new Error('Expanded tree exceeds 32 MiB. Choose DAG to preserve sharing.'); 79 const stack: Array<number | string> = [selected.root]; 80 while (stack.length) { 81 const item = stack.pop()!; 82 if (typeof item === 'string') { write(item); continue; } 83 const node = bundle.nodes[item]!; 84 if (format === 'ternary') { 85 write(node.kind === 'leaf' ? '0' : node.kind === 'stem' ? '1' : '2'); 86 if (node.kind === 'stem') stack.push(node.child); 87 else if (node.kind === 'fork') stack.push(node.right, node.left); 88 } else if (node.kind === 'leaf') write(symbol); 89 else { 90 write(`(${symbol} `); stack.push(')'); 91 if (node.kind === 'stem') stack.push(node.child); 92 else stack.push(node.right, ' ', node.left); 93 } 94 } 95 return { text: complete(), bytes, extension: format === 'tricu' ? 'tri' : 'txt', storedNodes: ids.length }; 96 }