tricu

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

codecs.ts (8783B)


      1 import { treeResult, type TreeResultFormat } from './tree-result.js';
      2 import type { TreeFile } from './tree-export.js';
      3 import { Graph, type NodeId, type GraphPreview } from './graph.js';
      4 export type Literal = { type: 'string' | 'number' | 'tree'; value: string }
      5   | { type: 'bool'; value: boolean } | { type: 'bytes'; value: number[] }
      6   | { type: 'list'; value: Literal[] };
      7 export type OutputType = TreeResultFormat | 'tree' | 'string' | 'number' | 'bool' | 'bytes' | 'list' | 'values';
      8 export type DecodedScalar = { type: 'string' | 'number'; value: string };
      9 export type ValueView = { type: TreeResultFormat; value: string; file: TreeFile } | { type: 'tree'; value: GraphPreview }
     10   | { type: 'values'; value: DecodedScalar[] }
     11   | { type: Exclude<OutputType, 'tree' | 'values' | TreeResultFormat>; value: string | boolean | number[] | GraphPreview[] };
     12 // Decoded program data is not a tiny constructor-preview list.
     13 export const DECODED_LIST_LIMIT = 4096;
     14 export const TEXT_LIMIT = 4096;
     15 export function natural(graph: Graph, value: bigint): NodeId {
     16   if (value < 0n || value.toString(2).length > 4096) throw new Error('Natural number limit exceeded');
     17   const bits: boolean[] = [];
     18   while (value) { bits.push((value & 1n) !== 0n); value >>= 1n; }
     19   let result = graph.leaf;
     20   for (let i = bits.length - 1; i >= 0; i--) result = graph.fork(bits[i] ? graph.stem(graph.leaf) : graph.leaf, result);
     21   return result;
     22 }
     23 export function readNatural(graph: Graph, handle: NodeId): bigint {
     24   let current = handle, result = 0n, place = 1n;
     25   for (let bits = 0; bits <= 4096; bits++) {
     26     const { node } = graph.observe(current);
     27     if (node.kind === 'leaf') return result;
     28     if (node.kind !== 'fork') throw new Error('Invalid canonical number');
     29     const bit = graph.observe(node.left).node;
     30     if (bit.kind === 'stem' && graph.observe(bit.child).node.kind === 'leaf') result += place;
     31     else if (bit.kind !== 'leaf') throw new Error('Invalid number bit');
     32     place <<= 1n; current = node.right;
     33   }
     34   throw new Error('Natural number decode limit exceeded');
     35 }
     36 export function list(graph: Graph, values: readonly NodeId[]): NodeId {
     37   let result = graph.leaf;
     38   for (let i = values.length - 1; i >= 0; i--) result = graph.fork(values[i]!, result);
     39   return result;
     40 }
     41 export function readList(graph: Graph, handle: NodeId, limit = TEXT_LIMIT): NodeId[] {
     42   const values: NodeId[] = []; let current = handle;
     43   for (;;) {
     44     const { node } = graph.observe(current);
     45     if (node.kind === 'leaf') return values;
     46     if (node.kind !== 'fork') throw new Error('Invalid canonical list');
     47     if (values.length >= limit) throw new Error('List decode limit exceeded');
     48     values.push(node.left); current = node.right;
     49   }
     50 }
     51 export function text(graph: Graph, value: string, limit = TEXT_LIMIT): NodeId {
     52   if (value.length > limit * 2) throw new Error('Text limit exceeded');
     53   const chars = Array.from(value);
     54   if (chars.length > limit) throw new Error('Text limit exceeded');
     55   return list(graph, chars.map(char => {
     56     const scalar = char.codePointAt(0)!;
     57     if (scalar >= 0xd800 && scalar <= 0xdfff) throw new Error('Unpaired Unicode surrogate');
     58     return natural(graph, BigInt(scalar));
     59   }));
     60 }
     61 export function readText(graph: Graph, handle: NodeId, limit = TEXT_LIMIT): string {
     62   return readList(graph, handle, limit).map(char => {
     63     const n = readNatural(graph, char);
     64     if (n > 0x10ffffn || (n >= 0xd800n && n <= 0xdfffn)) throw new Error('Invalid Unicode scalar');
     65     return String.fromCodePoint(Number(n));
     66   }).join('');
     67 }
     68 export function line(graph: Graph, value: string): NodeId {
     69   if (/[\r\n]/u.test(value)) throw new Error('Input must be one line');
     70   return text(graph, value, 256);
     71 }
     72 // Runtime validation complements TS: every boundary accepts unknown data.
     73 export function literals(input: unknown): Literal[] {
     74   if (!Array.isArray(input) || input.length > 16) throw new Error('Arguments must be an array of at most 16 literals');
     75   let count = 0;
     76   function validate(value: unknown, depth: number): Literal {
     77     if (++count > 4096 || depth > 16) throw new Error('Literal nesting/item limit exceeded');
     78     if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected typed literal');
     79     const v = value as Record<string, unknown>;
     80     switch (v['type']) {
     81       case 'string': case 'number': case 'tree': {
     82         if (typeof v['value'] !== 'string' || v['value'].length > 8192) throw new Error('Literal text limit exceeded');
     83         if (v['type'] === 'number' && (!/^(0|[1-9][0-9]*)$/u.test(v['value']) || v['value'].length > 1024))
     84           throw new Error('Natural input must be bounded nonnegative decimal digits');
     85         return { type: v['type'], value: v['value'] };
     86       }
     87       case 'bool':
     88         if (typeof v['value'] !== 'boolean') throw new Error('Boolean literal requires true or false');
     89         return { type: 'bool', value: v['value'] };
     90       case 'bytes': {
     91         const bytes = v['value'];
     92         if (!Array.isArray(bytes) || bytes.length > TEXT_LIMIT || !bytes.every(n => Number.isInteger(n) && n >= 0 && n <= 255))
     93           throw new Error('Bytes must be a bounded array of integers 0–255');
     94         return { type: 'bytes', value: bytes as number[] };
     95       }
     96       case 'list':
     97         if (!Array.isArray(v['value']) || v['value'].length > TEXT_LIMIT) throw new Error('List literal requires a bounded array of typed literals');
     98         return { type: 'list', value: v['value'].map(item => validate(item, depth + 1)) };
     99       default: throw new Error('Unsupported literal type');
    100     }
    101   }
    102   return input.map(value => validate(value, 0));
    103 }
    104 export function makeLiteral(graph: Graph, value: Literal): NodeId {
    105   switch (value.type) {
    106     case 'string': return text(graph, value.value);
    107     case 'number': return natural(graph, BigInt(value.value));
    108     case 'bool': return value.value ? graph.stem(graph.leaf) : graph.leaf;
    109     case 'tree': return graph.fromTernary(value.value);
    110     case 'bytes': return list(graph, value.value.map(n => natural(graph, BigInt(n))));
    111     case 'list': return list(graph, value.value.map(item => makeLiteral(graph, item)));
    112   }
    113 }
    114 export function outputType(input: unknown): OutputType {
    115   if (!['tree', 'string', 'number', 'bool', 'bytes', 'list', 'values', 'tricu', 'triangle', 'dag', 'ternary'].includes(String(input))) throw new Error('Unsupported output codec');
    116   return input as OutputType;
    117 }
    118 export function view(graph: Graph, handle: NodeId, type: OutputType): ValueView {
    119   switch (type) {
    120     case 'tricu': case 'triangle': case 'dag': case 'ternary': return treeResult(graph.snapshot(handle), type);
    121     case 'values': return { type, value: readList(graph, handle, DECODED_LIST_LIMIT).map(id => readScalar(graph, id)) };
    122     case 'tree': return { type, value: graph.preview(handle) };
    123     case 'string': return { type, value: readText(graph, handle) };
    124     case 'number': return { type, value: readNatural(graph, handle).toString() };
    125     case 'bool': {
    126       const { node } = graph.observe(handle);
    127       if (node.kind === 'leaf') return { type, value: false };
    128       if (node.kind === 'stem' && graph.observe(node.child).node.kind === 'leaf') return { type, value: true };
    129       throw new Error('Invalid canonical Boolean');
    130     }
    131     case 'bytes': return { type, value: readList(graph, handle).map(id => {
    132       const n = readNatural(graph, id); if (n > 255n) throw new Error('Invalid byte'); return Number(n);
    133     }) };
    134     case 'list': return { type, value: readList(graph, handle, 32).map(id => graph.preview(id, 8)) };
    135   }
    136 }
    137 
    138 // Deliberately conservative display policy shared by Auto and decoded lists.
    139 // Unicode letters/marks/numbers/punctuation/symbols/spaces, text whitespace and
    140 // joining characters are readable; controls/private-use/unassigned scalars aren't.
    141 export function readableText(value: string): boolean {
    142   return value.length > 0 && /^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}\t\n\r\u200c\u200d]+$/u.test(value);
    143 }
    144 export function readScalar(graph: Graph, handle: NodeId): DecodedScalar {
    145   try {
    146     const value = readText(graph, handle);
    147     if (readableText(value)) return { type: 'string', value };
    148   } catch { /* Try a natural interpretation of this same tree. */ }
    149   return { type: 'number', value: readNatural(graph, handle).toString() };
    150 }
    151 export function valueText(result: ValueView): string {
    152   if ('file' in result) return result.file.text;
    153   if (result.type === 'values') return '[' + result.value.map(item => item.type === 'string' ? JSON.stringify(item.value) : item.value).join(' ') + ']';
    154   if (result.type === 'tree' || result.type === 'list') throw new Error('Choose a source interpretation for trees');
    155   return typeof result.value === 'string' ? result.value : JSON.stringify(result.value, null, 2);
    156 }