codecs.js (8424B)
1 import { treeResult } from './tree-result.js'; 2 // Decoded program data is not a tiny constructor-preview list. 3 export const DECODED_LIST_LIMIT = 4096; 4 export const TEXT_LIMIT = 4096; 5 export function natural(graph, value) { 6 if (value < 0n || value.toString(2).length > 4096) 7 throw new Error('Natural number limit exceeded'); 8 const bits = []; 9 while (value) { 10 bits.push((value & 1n) !== 0n); 11 value >>= 1n; 12 } 13 let result = graph.leaf; 14 for (let i = bits.length - 1; i >= 0; i--) 15 result = graph.fork(bits[i] ? graph.stem(graph.leaf) : graph.leaf, result); 16 return result; 17 } 18 export function readNatural(graph, handle) { 19 let current = handle, result = 0n, place = 1n; 20 for (let bits = 0; bits <= 4096; bits++) { 21 const { node } = graph.observe(current); 22 if (node.kind === 'leaf') 23 return result; 24 if (node.kind !== 'fork') 25 throw new Error('Invalid canonical number'); 26 const bit = graph.observe(node.left).node; 27 if (bit.kind === 'stem' && graph.observe(bit.child).node.kind === 'leaf') 28 result += place; 29 else if (bit.kind !== 'leaf') 30 throw new Error('Invalid number bit'); 31 place <<= 1n; 32 current = node.right; 33 } 34 throw new Error('Natural number decode limit exceeded'); 35 } 36 export function list(graph, values) { 37 let result = graph.leaf; 38 for (let i = values.length - 1; i >= 0; i--) 39 result = graph.fork(values[i], result); 40 return result; 41 } 42 export function readList(graph, handle, limit = TEXT_LIMIT) { 43 const values = []; 44 let current = handle; 45 for (;;) { 46 const { node } = graph.observe(current); 47 if (node.kind === 'leaf') 48 return values; 49 if (node.kind !== 'fork') 50 throw new Error('Invalid canonical list'); 51 if (values.length >= limit) 52 throw new Error('List decode limit exceeded'); 53 values.push(node.left); 54 current = node.right; 55 } 56 } 57 export function text(graph, value, limit = TEXT_LIMIT) { 58 if (value.length > limit * 2) 59 throw new Error('Text limit exceeded'); 60 const chars = Array.from(value); 61 if (chars.length > limit) 62 throw new Error('Text limit exceeded'); 63 return list(graph, chars.map(char => { 64 const scalar = char.codePointAt(0); 65 if (scalar >= 0xd800 && scalar <= 0xdfff) 66 throw new Error('Unpaired Unicode surrogate'); 67 return natural(graph, BigInt(scalar)); 68 })); 69 } 70 export function readText(graph, handle, limit = TEXT_LIMIT) { 71 return readList(graph, handle, limit).map(char => { 72 const n = readNatural(graph, char); 73 if (n > 0x10ffffn || (n >= 0xd800n && n <= 0xdfffn)) 74 throw new Error('Invalid Unicode scalar'); 75 return String.fromCodePoint(Number(n)); 76 }).join(''); 77 } 78 export function line(graph, value) { 79 if (/[\r\n]/u.test(value)) 80 throw new Error('Input must be one line'); 81 return text(graph, value, 256); 82 } 83 // Runtime validation complements TS: every boundary accepts unknown data. 84 export function literals(input) { 85 if (!Array.isArray(input) || input.length > 16) 86 throw new Error('Arguments must be an array of at most 16 literals'); 87 let count = 0; 88 function validate(value, depth) { 89 if (++count > 4096 || depth > 16) 90 throw new Error('Literal nesting/item limit exceeded'); 91 if (!value || typeof value !== 'object' || Array.isArray(value)) 92 throw new Error('Expected typed literal'); 93 const v = value; 94 switch (v['type']) { 95 case 'string': 96 case 'number': 97 case 'tree': { 98 if (typeof v['value'] !== 'string' || v['value'].length > 8192) 99 throw new Error('Literal text limit exceeded'); 100 if (v['type'] === 'number' && (!/^(0|[1-9][0-9]*)$/u.test(v['value']) || v['value'].length > 1024)) 101 throw new Error('Natural input must be bounded nonnegative decimal digits'); 102 return { type: v['type'], value: v['value'] }; 103 } 104 case 'bool': 105 if (typeof v['value'] !== 'boolean') 106 throw new Error('Boolean literal requires true or false'); 107 return { type: 'bool', value: v['value'] }; 108 case 'bytes': { 109 const bytes = v['value']; 110 if (!Array.isArray(bytes) || bytes.length > TEXT_LIMIT || !bytes.every(n => Number.isInteger(n) && n >= 0 && n <= 255)) 111 throw new Error('Bytes must be a bounded array of integers 0–255'); 112 return { type: 'bytes', value: bytes }; 113 } 114 case 'list': 115 if (!Array.isArray(v['value']) || v['value'].length > TEXT_LIMIT) 116 throw new Error('List literal requires a bounded array of typed literals'); 117 return { type: 'list', value: v['value'].map(item => validate(item, depth + 1)) }; 118 default: throw new Error('Unsupported literal type'); 119 } 120 } 121 return input.map(value => validate(value, 0)); 122 } 123 export function makeLiteral(graph, value) { 124 switch (value.type) { 125 case 'string': return text(graph, value.value); 126 case 'number': return natural(graph, BigInt(value.value)); 127 case 'bool': return value.value ? graph.stem(graph.leaf) : graph.leaf; 128 case 'tree': return graph.fromTernary(value.value); 129 case 'bytes': return list(graph, value.value.map(n => natural(graph, BigInt(n)))); 130 case 'list': return list(graph, value.value.map(item => makeLiteral(graph, item))); 131 } 132 } 133 export function outputType(input) { 134 if (!['tree', 'string', 'number', 'bool', 'bytes', 'list', 'values', 'tricu', 'triangle', 'dag', 'ternary'].includes(String(input))) 135 throw new Error('Unsupported output codec'); 136 return input; 137 } 138 export function view(graph, handle, type) { 139 switch (type) { 140 case 'tricu': 141 case 'triangle': 142 case 'dag': 143 case 'ternary': return treeResult(graph.snapshot(handle), type); 144 case 'values': return { type, value: readList(graph, handle, DECODED_LIST_LIMIT).map(id => readScalar(graph, id)) }; 145 case 'tree': return { type, value: graph.preview(handle) }; 146 case 'string': return { type, value: readText(graph, handle) }; 147 case 'number': return { type, value: readNatural(graph, handle).toString() }; 148 case 'bool': { 149 const { node } = graph.observe(handle); 150 if (node.kind === 'leaf') 151 return { type, value: false }; 152 if (node.kind === 'stem' && graph.observe(node.child).node.kind === 'leaf') 153 return { type, value: true }; 154 throw new Error('Invalid canonical Boolean'); 155 } 156 case 'bytes': return { type, value: readList(graph, handle).map(id => { 157 const n = readNatural(graph, id); 158 if (n > 255n) 159 throw new Error('Invalid byte'); 160 return Number(n); 161 }) }; 162 case 'list': return { type, value: readList(graph, handle, 32).map(id => graph.preview(id, 8)) }; 163 } 164 } 165 // Deliberately conservative display policy shared by Auto and decoded lists. 166 // Unicode letters/marks/numbers/punctuation/symbols/spaces, text whitespace and 167 // joining characters are readable; controls/private-use/unassigned scalars aren't. 168 export function readableText(value) { 169 return value.length > 0 && /^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}\t\n\r\u200c\u200d]+$/u.test(value); 170 } 171 export function readScalar(graph, handle) { 172 try { 173 const value = readText(graph, handle); 174 if (readableText(value)) 175 return { type: 'string', value }; 176 } 177 catch { /* Try a natural interpretation of this same tree. */ } 178 return { type: 'number', value: readNatural(graph, handle).toString() }; 179 } 180 export function valueText(result) { 181 if ('file' in result) 182 return result.file.text; 183 if (result.type === 'values') 184 return '[' + result.value.map(item => item.type === 'string' ? JSON.stringify(item.value) : item.value).join(' ') + ']'; 185 if (result.type === 'tree' || result.type === 'list') 186 throw new Error('Choose a source interpretation for trees'); 187 return typeof result.value === 'string' ? result.value : JSON.stringify(result.value, null, 2); 188 }