literal-input.ts (4752B)
1 import { literals, type Literal } from './codecs.js'; 2 3 // Deliberately a data-only subset, not a second source compiler. No names, 4 // arbitrary applications, comments, or effects are evaluated in argument fields. 5 export function parseLiteral(source: string): Literal { 6 if (source.length > 16_000) throw new Error('Argument text limit exceeded'); 7 let at = 0, items = 0; 8 const space = () => { while (source[at] === ' ' || source[at] === '\n') at++; }; 9 const fail = (): never => { throw new Error(`Expected a literal at character ${at + 1}`); }; 10 function atom(depth: number): Literal { 11 if (++items > 4096 || depth > 16) throw new Error('Literal nesting/item limit exceeded'); 12 space(); const start = at, token = source[at++]; 13 if (token === '"') { 14 let value = ''; 15 while (at < source.length) { 16 const char = source[at++]; 17 if (char === '"') { 18 const scalars = Array.from(value); 19 if (scalars.length > 4096 || scalars.some(c => { const n = c.codePointAt(0)!; return n >= 0xd800 && n <= 0xdfff; })) 20 throw new Error('Expected bounded Unicode scalar text'); 21 return { type: 'string', value }; 22 } 23 if (char === '\\') { 24 const escapes: Record<string, string> = { n: '\n', t: '\t', r: '\r', f: '\f', b: '\b', '\\': '\\', '"': '"', "'": "'" }; 25 const escaped = escapes[source[at++]!]; if (escaped === undefined) fail(); value += escaped; 26 } else value += char; 27 } 28 fail(); 29 } 30 if (token === '[') { 31 const value: Literal[] = []; space(); 32 while (source[at] !== ']') { if (at >= source.length) fail(); value.push(atom(depth + 1)); space(); } 33 at++; return { type: 'list', value }; 34 } 35 if (token === '(') { const value = expression(depth + 1); space(); if (source[at++] !== ')') fail(); return value; } 36 if (token === 't') return { type: 'tree', value: '0' }; 37 if (token && /[0-9]/u.test(token)) { 38 while (source[at] && /[0-9]/u.test(source[at]!)) at++; 39 return literals([{ type: 'number', value: source.slice(start, at).replace(/^0+(?=\d)/u, '') }])[0]!; 40 } 41 return fail(); 42 } 43 function expression(depth: number): Literal { 44 space(); const constructor = source[at] === 't'; const first = atom(depth); 45 if (!constructor) return first; 46 const children: Literal[] = []; space(); 47 while (at < source.length && source[at] !== ')' && source[at] !== ']') { 48 if (children.length === 2) fail(); children.push(atom(depth + 1)); space(); 49 } 50 // Constructor children can contain encoded scalar/list data without evaluation. 51 const ternary = (literal: Literal): string => { 52 switch (literal.type) { 53 case 'tree': return literal.value; 54 case 'bool': return literal.value ? '10' : '0'; 55 case 'number': { 56 let n = BigInt(literal.value), result = ''; 57 while (n) { result += n & 1n ? '210' : '20'; n >>= 1n; } 58 return result + '0'; 59 } 60 case 'string': return ternary({ type: 'list', value: Array.from(literal.value).map(c => ({ type: 'number', value: String(c.codePointAt(0)) })) }); 61 case 'bytes': return ternary({ type: 'list', value: literal.value.map(n => ({ type: 'number', value: String(n) })) }); 62 case 'list': return literal.value.map(v => '2' + ternary(v)).join('') + '0'; 63 } 64 }; 65 const value = String(children.length) + children.map(ternary).join(''); 66 if (value.length > 8192) throw new Error('Tree literal limit exceeded'); 67 return { type: 'tree', value }; 68 } 69 const value = expression(0); space(); if (at !== source.length) fail(); 70 return literals([value])[0]!; 71 } 72 73 export function printLiteral(literal: Literal): string { 74 switch (literal.type) { 75 case 'number': return literal.value; 76 case 'string': return '"' + literal.value.replace(/[\\"\n\t\r\f\b]/gu, c => ({ '\\': '\\\\', '"': '\\"', '\n': '\\n', '\t': '\\t', '\r': '\\r', '\f': '\\f', '\b': '\\b' })[c]!) + '"'; 77 case 'bool': return literal.value ? '(t t)' : 't'; 78 case 'bytes': return '[' + literal.value.join(' ') + ']'; 79 case 'list': return '[' + literal.value.map(printLiteral).join(' ') + ']'; 80 case 'tree': { 81 let at = 0; const value = literal.value; 82 function read(depth: number): string { 83 if (depth > 16) throw new Error('Use raw JSON for deeply nested trees'); 84 const tag = value[at++]; 85 if (tag === '0') return 't'; 86 if (tag === '1') return `(t ${read(depth + 1)})`; 87 if (tag === '2') return `(t ${read(depth + 1)} ${read(depth + 1)})`; 88 throw new Error('Invalid ternary tree'); 89 } 90 const result = read(0); if (at !== literal.value.length) throw new Error('Trailing ternary data'); return result; 91 } 92 } 93 }