tricu

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

codecs.ts (9010B)


      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' | 'record';
      8 export type ValueView = { type: TreeResultFormat; value: string; file: TreeFile } | { type: 'tree'; value: GraphPreview }
      9   | { type: Exclude<OutputType, 'tree' | TreeResultFormat>; value: string | boolean | number[] | GraphPreview[] | Record<string, string | number | boolean> };
     10 export const TEXT_LIMIT = 4096;
     11 export function natural(graph: Graph, value: bigint): NodeId {
     12   if (value < 0n || value.toString(2).length > 4096) throw new Error('Natural number limit exceeded');
     13   const bits: boolean[] = [];
     14   while (value) { bits.push((value & 1n) !== 0n); value >>= 1n; }
     15   let result = graph.leaf;
     16   for (let i = bits.length - 1; i >= 0; i--) result = graph.fork(bits[i] ? graph.stem(graph.leaf) : graph.leaf, result);
     17   return result;
     18 }
     19 export function readNatural(graph: Graph, handle: NodeId): bigint {
     20   let current = handle, result = 0n, place = 1n;
     21   for (let bits = 0; bits <= 4096; bits++) {
     22     const { node } = graph.observe(current);
     23     if (node.kind === 'leaf') return result;
     24     if (node.kind !== 'fork') throw new Error('Invalid canonical number');
     25     const bit = graph.observe(node.left).node;
     26     if (bit.kind === 'stem' && graph.observe(bit.child).node.kind === 'leaf') result += place;
     27     else if (bit.kind !== 'leaf') throw new Error('Invalid number bit');
     28     place <<= 1n; current = node.right;
     29   }
     30   throw new Error('Natural number decode limit exceeded');
     31 }
     32 export function list(graph: Graph, values: readonly NodeId[]): NodeId {
     33   let result = graph.leaf;
     34   for (let i = values.length - 1; i >= 0; i--) result = graph.fork(values[i]!, result);
     35   return result;
     36 }
     37 export function readList(graph: Graph, handle: NodeId, limit = TEXT_LIMIT): NodeId[] {
     38   const values: NodeId[] = []; let current = handle;
     39   for (;;) {
     40     const { node } = graph.observe(current);
     41     if (node.kind === 'leaf') return values;
     42     if (node.kind !== 'fork') throw new Error('Invalid canonical list');
     43     if (values.length >= limit) throw new Error('List decode limit exceeded');
     44     values.push(node.left); current = node.right;
     45   }
     46 }
     47 export function text(graph: Graph, value: string, limit = TEXT_LIMIT): NodeId {
     48   if (value.length > limit * 2) throw new Error('Text limit exceeded');
     49   const chars = Array.from(value);
     50   if (chars.length > limit) throw new Error('Text limit exceeded');
     51   return list(graph, chars.map(char => {
     52     const scalar = char.codePointAt(0)!;
     53     if (scalar >= 0xd800 && scalar <= 0xdfff) throw new Error('Unpaired Unicode surrogate');
     54     return natural(graph, BigInt(scalar));
     55   }));
     56 }
     57 export function readText(graph: Graph, handle: NodeId, limit = TEXT_LIMIT): string {
     58   return readList(graph, handle, limit).map(char => {
     59     const n = readNatural(graph, char);
     60     if (n > 0x10ffffn || (n >= 0xd800n && n <= 0xdfffn)) throw new Error('Invalid Unicode scalar');
     61     return String.fromCodePoint(Number(n));
     62   }).join('');
     63 }
     64 export function line(graph: Graph, value: string): NodeId {
     65   if (/[\r\n]/u.test(value)) throw new Error('Input must be one line');
     66   return text(graph, value, 256);
     67 }
     68 // Runtime validation complements TS: every boundary accepts unknown data.
     69 export function literals(input: unknown): Literal[] {
     70   if (!Array.isArray(input) || input.length > 16) throw new Error('Arguments must be an array of at most 16 literals');
     71   let count = 0;
     72   function validate(value: unknown, depth: number): Literal {
     73     if (++count > 4096 || depth > 16) throw new Error('Literal nesting/item limit exceeded');
     74     if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected typed literal');
     75     const v = value as Record<string, unknown>;
     76     switch (v['type']) {
     77       case 'string': case 'number': case 'tree': {
     78         if (typeof v['value'] !== 'string' || v['value'].length > 8192) throw new Error('Literal text limit exceeded');
     79         if (v['type'] === 'number' && (!/^(0|[1-9][0-9]*)$/u.test(v['value']) || v['value'].length > 1024))
     80           throw new Error('Natural input must be bounded nonnegative decimal digits');
     81         return { type: v['type'], value: v['value'] };
     82       }
     83       case 'bool':
     84         if (typeof v['value'] !== 'boolean') throw new Error('Boolean literal requires true or false');
     85         return { type: 'bool', value: v['value'] };
     86       case 'bytes': {
     87         const bytes = v['value'];
     88         if (!Array.isArray(bytes) || bytes.length > TEXT_LIMIT || !bytes.every(n => Number.isInteger(n) && n >= 0 && n <= 255))
     89           throw new Error('Bytes must be a bounded array of integers 0–255');
     90         return { type: 'bytes', value: bytes as number[] };
     91       }
     92       case 'list':
     93         if (!Array.isArray(v['value']) || v['value'].length > TEXT_LIMIT) throw new Error('List literal requires a bounded array of typed literals');
     94         return { type: 'list', value: v['value'].map(item => validate(item, depth + 1)) };
     95       default: throw new Error('Unsupported literal type');
     96     }
     97   }
     98   return input.map(value => validate(value, 0));
     99 }
    100 export function makeLiteral(graph: Graph, value: Literal): NodeId {
    101   switch (value.type) {
    102     case 'string': return text(graph, value.value);
    103     case 'number': return natural(graph, BigInt(value.value));
    104     case 'bool': return value.value ? graph.stem(graph.leaf) : graph.leaf;
    105     case 'tree': return graph.fromTernary(value.value);
    106     case 'bytes': return list(graph, value.value.map(n => natural(graph, BigInt(n))));
    107     case 'list': return list(graph, value.value.map(item => makeLiteral(graph, item)));
    108   }
    109 }
    110 export function outputType(input: unknown): OutputType {
    111   if (!['tree', 'string', 'number', 'bool', 'bytes', 'list', 'record', 'tricu', 'triangle', 'dag', 'ternary'].includes(String(input))) throw new Error('Unsupported output codec');
    112   return input as OutputType;
    113 }
    114 export function view(graph: Graph, handle: NodeId, type: OutputType): ValueView {
    115   switch (type) {
    116     case 'tricu': case 'triangle': case 'dag': case 'ternary': return treeResult(graph.snapshot(handle), type);
    117     case 'record': return { type, value: readRecord(graph, handle) };
    118     case 'tree': return { type, value: graph.preview(handle) };
    119     case 'string': return { type, value: readText(graph, handle) };
    120     case 'number': return { type, value: readNatural(graph, handle).toString() };
    121     case 'bool': {
    122       const { node } = graph.observe(handle);
    123       if (node.kind === 'leaf') return { type, value: false };
    124       if (node.kind === 'stem' && graph.observe(node.child).node.kind === 'leaf') return { type, value: true };
    125       throw new Error('Invalid canonical Boolean');
    126     }
    127     case 'bytes': return { type, value: readList(graph, handle).map(id => {
    128       const n = readNatural(graph, id); if (n > 255n) throw new Error('Invalid byte'); return Number(n);
    129     }) };
    130     case 'list': return { type, value: readList(graph, handle, 32).map(id => graph.preview(id, 8)) };
    131   }
    132 }
    133 
    134 // Portable scalar-record convention, independent of executable semantics.
    135 // List of [text key, natural tag, value]; 0 text, 1 safe natural, 2 Boolean.
    136 export function readRecord(graph: Graph, handle: NodeId): Record<string, string | number | boolean> {
    137   const result: Record<string, string | number | boolean> = Object.create(null) as Record<string, string | number | boolean>;
    138   let scalars = 0;
    139   for (const row of readList(graph, handle, 32)) {
    140     const fields = readList(graph, row, 3);
    141     if (fields.length !== 3) throw new Error('Record field must be [name tag value]');
    142     const name = readText(graph, fields[0]!, 128), tag = readNatural(graph, fields[1]!);
    143     if (!name || Object.hasOwn(result, name)) throw new Error('Record field names must be nonempty and unique');
    144     let value: string | number | boolean;
    145     if (tag === 0n) value = readText(graph, fields[2]!);
    146     else if (tag === 1n) {
    147       const n = readNatural(graph, fields[2]!);
    148       if (n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('Record number exceeds safe integer range');
    149       value = Number(n);
    150     } else if (tag === 2n) {
    151       const node = graph.observe(fields[2]!).node;
    152       if (node.kind === 'leaf') value = false;
    153       else if (node.kind === 'stem' && graph.observe(node.child).node.kind === 'leaf') value = true;
    154       else throw new Error('Invalid record Boolean');
    155     } else throw new Error('Unsupported record field tag');
    156     scalars += Array.from(name).length + (typeof value === 'string' ? Array.from(value).length : String(value).length);
    157     if (scalars > 16_384) throw new Error('Record output limit exceeded');
    158     result[name] = value;
    159   }
    160   return result;
    161 }