tricu

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

codecs.js (8803B)


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