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 }