tricu

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

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 }