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 }