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 }