import type { AnySchema, ObjectSchema, TypedArrayKind, UnionSchema, } from './descriptors.ts'; export interface CodegenResult { /** Inner-function body for encoder: takes (w, o), uses lifted pos/buf/view locals. */ encodeBody: string; /** Inner-function body for decoder: takes (r), uses lifted pos/buf/view locals. */ decodeBody: string; /** Cross-codec dependencies for ref/codec fields only (nested objects are inlined). */ deps: Map; /** Closure-captured values (enum maps, codec functions). */ closure: Map; } function sanitize(name: string): string { return name.replace(/[^A-Za-z0-9_]/g, '_'); } const TA_ELEM_SIZE: Record = { f32Array: 4, f64Array: 8, u8Array: 1, u16Array: 2, u32Array: 4, i32Array: 4, }; const TA_CTOR: Record = { f32Array: 'Float32Array', f64Array: 'Float64Array', u8Array: 'Uint8Array', u16Array: 'Uint16Array', u32Array: 'Uint32Array', i32Array: 'Int32Array', }; function isTypedArrayKind(k: string): k is TypedArrayKind { return k in TA_ELEM_SIZE; } /** * Upper bound on bytes the schema may write, or null when truly variable * (str, bytes, array, typed-array, ref, codec, and any composite containing them). */ function maxBytes(schema: AnySchema): number | null { switch (schema.kind) { case 'u8': case 'i8': case 'bool': case 'enum': return 1; case 'u16': case 'i16': return 2; case 'u32': case 'i32': case 'f32': return 4; case 'f64': return 8; case 'u53': case 'i53': case 'u64': case 'i64': return 10; case 'bitset': { const n = schema.flags.length; return n <= 8 ? 1 : n <= 16 ? 2 : n <= 32 ? 4 : 10; } case 'tuple': { let sum = 0; for (const e of schema.elems) { const m = maxBytes(e); if (m === null) return null; sum += m; } return sum; } case 'object': { let sum = 0; for (const f of Object.values(schema.fields)) { const m = maxBytes(f); if (m === null) return null; sum += m; } return sum; } case 'optional': { const m = maxBytes(schema.elem); return m === null ? null : 1 + m; } case 'union': { let max = 0; for (const v of Object.values(schema.variants)) { const m = maxBytes(v); if (m === null) return null; if (m > max) max = m; } return 1 + max; } case 'array': case 'str': case 'bytes': case 'ref': case 'codec': return null; } return null; } class Ctx { private counter = 0; mode: 'enc' | 'dec'; deps = new Map(); closure = new Map(); constructor(mode: 'enc' | 'dec') { this.mode = mode; } fresh(prefix: string): string { return `_${prefix}${++this.counter}`; } closureVar(prefix: string, value: unknown): string { const name = `__cv_${prefix}_${++this.counter}`; this.closure.set(name, value); return name; } } /** * Builds an encoder body. Bounded ops are buffered with a running budget; * unbounded ops (str, array, ref, etc.) flush the buffer with a single * `ensure(budget)` then emit themselves. Inside arrays with bounded elements, * we pre-ensure the whole batch and run the element loop in "noEnsure" mode. */ class SegBuilder { private buffered: string[] = []; private budget = 0; private output: string[] = []; noEnsure = false; addBounded(stmt: string, maxBytesSize: number): void { if (this.noEnsure) { this.output.push(stmt); return; } this.buffered.push(stmt); this.budget += maxBytesSize; } addPrelude(stmt: string): void { if (this.noEnsure) { this.output.push(stmt); return; } this.buffered.push(stmt); } flush(): void { if (this.buffered.length === 0) return; if (this.budget > 0 && !this.noEnsure) { this.output.push( `if (pos + ${this.budget} > buf.byteLength) { w.pos = pos; w.grow(${this.budget}); buf = w.buf; view = w.view; }`, ); } this.output.push(...this.buffered); this.buffered = []; this.budget = 0; } addUnbounded(stmt: string): void { this.flush(); this.output.push(stmt); } build(): string { this.flush(); return this.output.join('\n'); } } function emitEnc(schema: AnySchema, ctx: Ctx, seg: SegBuilder, vx: string): void { switch (schema.kind) { case 'u8': seg.addBounded(`buf[pos++] = (${vx}) & 0xff;`, 1); return; case 'i8': seg.addBounded(`buf[pos++] = (${vx}) & 0xff;`, 1); return; case 'bool': seg.addBounded(`buf[pos++] = ${vx} ? 1 : 0;`, 1); return; case 'u16': seg.addBounded(`view.setUint16(pos, ${vx}, true); pos += 2;`, 2); return; case 'i16': seg.addBounded(`view.setInt16(pos, ${vx}, true); pos += 2;`, 2); return; case 'u32': seg.addBounded(`view.setUint32(pos, ${vx}, true); pos += 4;`, 4); return; case 'i32': seg.addBounded(`view.setInt32(pos, ${vx}, true); pos += 4;`, 4); return; case 'f32': seg.addBounded(`view.setFloat32(pos, ${vx}, true); pos += 4;`, 4); return; case 'f64': seg.addBounded(`view.setFloat64(pos, ${vx}, true); pos += 8;`, 8); return; case 'u53': { const v = ctx.fresh('v'); seg.addBounded( `{ let ${v} = ${vx}; while (${v} >= 0x80) { buf[pos++] = (${v} & 0x7f) | 0x80; ${v} = Math.floor(${v} / 128); } buf[pos++] = ${v}; }`, 10, ); return; } case 'i53': { const v = ctx.fresh('v'); const src = ctx.fresh('s'); seg.addBounded( `{ const ${src} = ${vx}; let ${v} = ${src} >= 0 ? BigInt(${src}) * 2n : -BigInt(${src}) * 2n - 1n; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`, 10, ); return; } case 'u64': { const v = ctx.fresh('v'); seg.addBounded( `{ let ${v} = ${vx}; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`, 10, ); return; } case 'i64': { const v = ctx.fresh('v'); const src = ctx.fresh('s'); seg.addBounded( `{ const ${src} = ${vx}; let ${v} = ${src} >= 0n ? ${src} << 1n : (-${src} << 1n) - 1n; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`, 10, ); return; } case 'enum': { const vals = schema.values; if (vals.length === 2) { seg.addBounded(`buf[pos++] = ${vx} === ${JSON.stringify(vals[0])} ? 0 : 1;`, 1); } else if (vals.length === 3) { seg.addBounded( `buf[pos++] = ${vx} === ${JSON.stringify(vals[0])} ? 0 : ${vx} === ${JSON.stringify(vals[1])} ? 1 : 2;`, 1, ); } else { const map: Record = Object.create(null); for (let i = 0; i < vals.length; i++) map[vals[i]!] = i; const cv = ctx.closureVar('enum', Object.freeze(map)); seg.addBounded(`buf[pos++] = ${cv}[${vx}];`, 1); } return; } case 'bitset': { const b = ctx.fresh('b'); const flags = schema.flags; if (flags.length <= 32) { const parts: string[] = ['0']; for (let i = 0; i < flags.length; i++) { parts.push(`((${b})[${JSON.stringify(flags[i])}] ? ${1 << i} : 0)`); } const expr = parts.join(' | '); if (flags.length <= 8) { seg.addBounded(`{ const ${b} = ${vx}; buf[pos++] = ${expr}; }`, 1); } else if (flags.length <= 16) { seg.addBounded(`{ const ${b} = ${vx}; view.setUint16(pos, ${expr}, true); pos += 2; }`, 2); } else { seg.addBounded(`{ const ${b} = ${vx}; view.setUint32(pos, ${expr}, true); pos += 4; }`, 4); } } else { let big = '0n'; for (let i = 0; i < flags.length; i++) { big = `(${big}) | ((${b})[${JSON.stringify(flags[i])}] ? ${1n << BigInt(i)}n : 0n)`; } const v = ctx.fresh('v'); seg.addBounded( `{ const ${b} = ${vx}; let ${v} = ${big}; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`, 10, ); } return; } case 'str': { seg.addUnbounded( `w.pos = pos; w.str(${vx}); pos = w.pos; buf = w.buf; view = w.view;`, ); return; } case 'bytes': { seg.addUnbounded( `w.pos = pos; w.bytesPrefixed(${vx}); pos = w.pos; buf = w.buf; view = w.view;`, ); return; } case 'tuple': { const t = ctx.fresh('t'); seg.addPrelude(`const ${t} = ${vx};`); for (let i = 0; i < schema.elems.length; i++) { emitEnc(schema.elems[i]!, ctx, seg, `${t}[${i}]`); } return; } case 'optional': { const o = ctx.fresh('o'); const innerMax = maxBytes(schema.elem); if (innerMax !== null) { const innerSeg = new SegBuilder(); innerSeg.noEnsure = true; emitEnc(schema.elem, ctx, innerSeg, o); const innerSrc = innerSeg.build(); seg.addBounded( `{ const ${o} = ${vx}; if (${o} === undefined || ${o} === null) { buf[pos++] = 0; } else { buf[pos++] = 1; ${innerSrc} } }`, 1 + innerMax, ); } else { const innerSeg = new SegBuilder(); emitEnc(schema.elem, ctx, innerSeg, o); const innerSrc = innerSeg.build(); seg.addUnbounded( `{ const ${o} = ${vx}; if (pos + 1 > buf.byteLength) { w.pos = pos; w.grow(1); buf = w.buf; view = w.view; } if (${o} === undefined || ${o} === null) { buf[pos++] = 0; } else { buf[pos++] = 1; ${innerSrc} } }`, ); } return; } case 'object': { const oo = ctx.fresh('oo'); seg.addPrelude(`const ${oo} = ${vx};`); for (const fname of Object.keys(schema.fields)) { emitEnc(schema.fields[fname]!, ctx, seg, `${oo}[${JSON.stringify(fname)}]`); } return; } case 'union': { const u = ctx.fresh('u'); const disc = JSON.stringify(schema.discriminator); const keys = Object.keys(schema.variants); const cases: string[] = []; for (let i = 0; i < keys.length; i++) { const variant = schema.variants[keys[i]!]!; const innerSeg = new SegBuilder(); innerSeg.addBounded(`buf[pos++] = ${i};`, 1); for (const fname of Object.keys(variant.fields)) { emitEnc(variant.fields[fname]!, ctx, innerSeg, `${u}[${JSON.stringify(fname)}]`); } cases.push(`case ${JSON.stringify(keys[i])}: { ${innerSeg.build()} break; }`); } seg.addUnbounded( `{ const ${u} = ${vx}; switch (${u}[${disc}]) { ${cases.join('\n')} default: throw new Error('Bad union variant: ' + ${u}[${disc}]); } }`, ); return; } case 'array': { const arr = ctx.fresh('arr'); const L = ctx.fresh('L'); const i = ctx.fresh('i'); const elemMax = maxBytes(schema.elem); seg.addPrelude(`const ${arr} = ${vx}; const ${L} = ${arr}.length;`); const vL = ctx.fresh('vL'); seg.addBounded( `{ let ${vL} = ${L}; while (${vL} >= 0x80) { buf[pos++] = (${vL} & 0x7f) | 0x80; ${vL} = Math.floor(${vL} / 128); } buf[pos++] = ${vL}; }`, 10, ); if (elemMax !== null) { seg.addUnbounded( `if (pos + ${L} * ${elemMax} > buf.byteLength) { w.pos = pos; w.grow(${L} * ${elemMax}); buf = w.buf; view = w.view; }`, ); const elemSeg = new SegBuilder(); elemSeg.noEnsure = true; emitEnc(schema.elem, ctx, elemSeg, `${arr}[${i}]`); const elemSrc = elemSeg.build(); seg.addUnbounded(`for (let ${i} = 0; ${i} < ${L}; ${i}++) { ${elemSrc} }`); } else { const elemSeg = new SegBuilder(); emitEnc(schema.elem, ctx, elemSeg, `${arr}[${i}]`); const elemSrc = elemSeg.build(); seg.addUnbounded(`for (let ${i} = 0; ${i} < ${L}; ${i}++) { ${elemSrc} }`); } return; } case 'ref': { const resolved = schema.thunk(); const dep = `__enc_${sanitize(resolved.name)}`; ctx.deps.set(dep, { mode: 'enc', targetName: resolved.name }); seg.addUnbounded( `w.pos = pos; ${dep}(w, ${vx}); pos = w.pos; buf = w.buf; view = w.view;`, ); return; } case 'codec': { const cv = ctx.closureVar('codec_enc', schema.encode); seg.addUnbounded( `w.pos = pos; ${cv}(w, ${vx}); pos = w.pos; buf = w.buf; view = w.view;`, ); return; } } if (isTypedArrayKind(schema.kind)) { const ta = ctx.fresh('ta'); seg.addUnbounded( `{ const ${ta} = ${vx}; w.pos = pos; w.varu53(${ta}.length); w.raw(new Uint8Array(${ta}.buffer, ${ta}.byteOffset, ${ta}.byteLength)); pos = w.pos; buf = w.buf; view = w.view; }`, ); return; } throw new Error(`emitEnc: unknown kind ${(schema as { kind: string }).kind}`); } function emitDec(schema: AnySchema, ctx: Ctx): { pre: string; expr: string } { switch (schema.kind) { case 'u8': return { pre: '', expr: 'buf[pos++]' }; case 'i8': return { pre: '', expr: '(buf[pos++] << 24 >> 24)' }; case 'bool': return { pre: '', expr: '(buf[pos++] !== 0)' }; case 'u16': { const v = ctx.fresh('v'); return { pre: `const ${v} = view.getUint16(pos, true); pos += 2;`, expr: v }; } case 'i16': { const v = ctx.fresh('v'); return { pre: `const ${v} = view.getInt16(pos, true); pos += 2;`, expr: v }; } case 'u32': { const v = ctx.fresh('v'); return { pre: `const ${v} = view.getUint32(pos, true); pos += 4;`, expr: v }; } case 'i32': { const v = ctx.fresh('v'); return { pre: `const ${v} = view.getInt32(pos, true); pos += 4;`, expr: v }; } case 'f32': { const v = ctx.fresh('v'); return { pre: `const ${v} = view.getFloat32(pos, true); pos += 4;`, expr: v }; } case 'f64': { const v = ctx.fresh('v'); return { pre: `const ${v} = view.getFloat64(pos, true); pos += 8;`, expr: v }; } case 'u53': { const v = ctx.fresh('v'); const vv = ctx.fresh('vv'); const m = ctx.fresh('m'); const b = ctx.fresh('b'); return { pre: `let ${v}; { let ${vv} = 0, ${m} = 1, ${b}; do { ${b} = buf[pos++]; ${vv} += (${b} & 0x7f) * ${m}; ${m} *= 128; } while (${b} & 0x80); ${v} = ${vv}; }`, expr: v, }; } case 'i53': { const v = ctx.fresh('v'); const vv = ctx.fresh('vv'); const sh = ctx.fresh('sh'); const b = ctx.fresh('b'); return { pre: `let ${v}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); const _z = (${vv} & 1n) === 0n ? ${vv} >> 1n : -((${vv} >> 1n) + 1n); ${v} = Number(_z); }`, expr: v, }; } case 'u64': { const v = ctx.fresh('v'); const vv = ctx.fresh('vv'); const sh = ctx.fresh('sh'); const b = ctx.fresh('b'); return { pre: `let ${v}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); ${v} = ${vv}; }`, expr: v, }; } case 'i64': { const v = ctx.fresh('v'); const vv = ctx.fresh('vv'); const sh = ctx.fresh('sh'); const b = ctx.fresh('b'); return { pre: `let ${v}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); ${v} = (${vv} & 1n) === 0n ? ${vv} >> 1n : -((${vv} >> 1n) + 1n); }`, expr: v, }; } case 'enum': { const vals = schema.values; if (vals.length <= 2) { return { pre: '', expr: `(buf[pos++] === 0 ? ${JSON.stringify(vals[0])} : ${JSON.stringify(vals[1])})`, }; } else if (vals.length === 3) { const v = ctx.fresh('v'); const t = ctx.fresh('t'); return { pre: `let ${v}; { const ${t} = buf[pos++]; ${v} = ${t} === 0 ? ${JSON.stringify(vals[0])} : ${t} === 1 ? ${JSON.stringify(vals[1])} : ${JSON.stringify(vals[2])}; }`, expr: v, }; } else { const cv = ctx.closureVar('enum_dec', Object.freeze(vals.slice())); return { pre: '', expr: `${cv}[buf[pos++]]` }; } } case 'bitset': { const v = ctx.fresh('v'); const raw = ctx.fresh('raw'); const flags = schema.flags; let rawDecl: string; let isBig = false; if (flags.length <= 8) { rawDecl = `const ${raw} = buf[pos++];`; } else if (flags.length <= 16) { rawDecl = `const ${raw} = view.getUint16(pos, true); pos += 2;`; } else if (flags.length <= 32) { rawDecl = `const ${raw} = view.getUint32(pos, true); pos += 4;`; } else { isBig = true; const vv = ctx.fresh('vv'); const sh = ctx.fresh('sh'); const b = ctx.fresh('b'); rawDecl = `let ${raw}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); ${raw} = ${vv}; }`; } const props: string[] = []; for (let i = 0; i < flags.length; i++) { if (isBig) { props.push(`${JSON.stringify(flags[i])}: (${raw} & ${1n << BigInt(i)}n) !== 0n`); } else { props.push(`${JSON.stringify(flags[i])}: (${raw} & ${1 << i}) !== 0`); } } return { pre: `${rawDecl} const ${v} = { ${props.join(', ')} };`, expr: v, }; } case 'str': { const v = ctx.fresh('v'); return { pre: `r.pos = pos; const ${v} = r.str(); pos = r.pos;`, expr: v, }; } case 'bytes': { const v = ctx.fresh('v'); return { pre: `r.pos = pos; const ${v} = r.bytesPrefixed(); pos = r.pos;`, expr: v, }; } case 'tuple': { let pre = ''; const exprs: string[] = []; for (const elem of schema.elems) { const inner = emitDec(elem, ctx); if (inner.pre === '') { exprs.push(inner.expr); } else { const tmp = ctx.fresh('tupv'); pre += `${inner.pre} const ${tmp} = ${inner.expr};`; exprs.push(tmp); } } const tup = ctx.fresh('tup'); return { pre: `${pre} const ${tup} = [${exprs.join(', ')}];`, expr: tup }; } case 'optional': { const v = ctx.fresh('opt'); const inner = emitDec(schema.elem, ctx); return { pre: `let ${v} = undefined; if (buf[pos++] !== 0) { ${inner.pre} ${v} = ${inner.expr}; }`, expr: v, }; } case 'object': { let pre = ''; const props: string[] = []; for (const fname of Object.keys(schema.fields)) { const inner = emitDec(schema.fields[fname]!, ctx); const tmp = ctx.fresh(`f_${sanitize(fname)}`); pre += `${inner.pre} const ${tmp} = ${inner.expr};`; props.push(`${JSON.stringify(fname)}: ${tmp}`); } const obj = ctx.fresh('obj'); return { pre: `${pre} const ${obj} = { ${props.join(', ')} };`, expr: obj }; } case 'union': { const v = ctx.fresh('un'); const disc = JSON.stringify(schema.discriminator); const keys = Object.keys(schema.variants); const cases: string[] = []; for (let i = 0; i < keys.length; i++) { const variant = schema.variants[keys[i]!]!; let varPre = ''; const props: string[] = [`${disc}: ${JSON.stringify(keys[i])}`]; for (const fname of Object.keys(variant.fields)) { const inner = emitDec(variant.fields[fname]!, ctx); if (inner.pre === '') { props.push(`${JSON.stringify(fname)}: ${inner.expr}`); } else { const tmp = ctx.fresh('uv'); varPre += `${inner.pre} const ${tmp} = ${inner.expr};`; props.push(`${JSON.stringify(fname)}: ${tmp}`); } } cases.push(`case ${i}: { ${varPre} ${v} = { ${props.join(', ')} }; break; }`); } return { pre: `let ${v}; switch (buf[pos++]) { ${cases.join(' ')} default: throw new Error('Bad union tag'); }`, expr: v, }; } case 'array': { const a = ctx.fresh('arr'); const L = ctx.fresh('len'); const i = ctx.fresh('i'); const vv = ctx.fresh('vv'); const m = ctx.fresh('m'); const b = ctx.fresh('b'); const elem = emitDec(schema.elem, ctx); const lenRead = `let ${L}; { let ${vv} = 0, ${m} = 1, ${b}; do { ${b} = buf[pos++]; ${vv} += (${b} & 0x7f) * ${m}; ${m} *= 128; } while (${b} & 0x80); ${L} = ${vv}; }`; return { pre: `${lenRead} const ${a} = new Array(${L}); for (let ${i} = 0; ${i} < ${L}; ${i}++) { ${elem.pre} ${a}[${i}] = ${elem.expr}; }`, expr: a, }; } case 'ref': { const resolved = schema.thunk(); const dep = `__dec_${sanitize(resolved.name)}`; ctx.deps.set(dep, { mode: 'dec', targetName: resolved.name }); const v = ctx.fresh('v'); return { pre: `r.pos = pos; const ${v} = ${dep}(r); pos = r.pos;`, expr: v, }; } case 'codec': { const cv = ctx.closureVar('codec_dec', schema.decode); const v = ctx.fresh('v'); return { pre: `r.pos = pos; const ${v} = ${cv}(r); pos = r.pos;`, expr: v, }; } } if (isTypedArrayKind(schema.kind)) { const len = ctx.fresh('talen'); const arr = ctx.fresh('ta'); const size = TA_ELEM_SIZE[schema.kind]; const ctor = TA_CTOR[schema.kind]; const vv = ctx.fresh('vv'); const m = ctx.fresh('m'); const b = ctx.fresh('b'); return { pre: `let ${len}; { let ${vv} = 0, ${m} = 1, ${b}; do { ${b} = buf[pos++]; ${vv} += (${b} & 0x7f) * ${m}; ${m} *= 128; } while (${b} & 0x80); ${len} = ${vv}; } const ${arr} = new ${ctor}(${len}); { const _bytes = buf.subarray(pos, pos + ${len} * ${size}); new Uint8Array(${arr}.buffer, ${arr}.byteOffset, ${len} * ${size}).set(_bytes); pos += ${len} * ${size}; }`, expr: arr, }; } throw new Error(`emitDec: unknown kind ${(schema as { kind: string }).kind}`); } const BARE_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; export interface CompileOptions { /** * If set, the generated decoder builds the result with * `Object.create(${boundProtoExpr})` and assigns fields one by one, so the * decoded value is an instance of the class whose prototype this expression * resolves to. When omitted, the decoder returns a plain object literal. */ boundProtoExpr?: string; } export function compileObject( schema: ObjectSchema, options: CompileOptions = {}, ): CodegenResult { const encCtx = new Ctx('enc'); const decCtx = new Ctx('dec'); // Encoder body const encSeg = new SegBuilder(); for (const fname of Object.keys(schema.fields)) { emitEnc(schema.fields[fname]!, encCtx, encSeg, `o[${JSON.stringify(fname)}]`); } const encodeBody = encSeg.build(); // Decoder body. For fields whose inner.expr is already a bare identifier // (declared via inner.pre), skip the wrapping `const tmp = expr;`. let pre = ''; const pairs: Array<{ key: string; expr: string; rawName: string }> = []; for (const fname of Object.keys(schema.fields)) { const inner = emitDec(schema.fields[fname]!, decCtx); let expr: string; if (inner.pre !== '' && BARE_IDENT.test(inner.expr)) { pre += inner.pre; expr = inner.expr; } else { const tmp = decCtx.fresh(`f_${sanitize(fname)}`); pre += `${inner.pre} const ${tmp} = ${inner.expr};`; expr = tmp; } pairs.push({ key: JSON.stringify(fname), expr, rawName: fname }); } let decodeBody: string; if (options.boundProtoExpr) { // Class-bound: build via Object.create(proto) + sequential assigns so the // result satisfies `instanceof` and inherits prototype methods. const out = decCtx.fresh('out'); const assigns = pairs.map((p) => `${out}[${p.key}] = ${p.expr};`).join(''); decodeBody = `${pre} const ${out} = Object.create(${options.boundProtoExpr}); ${assigns} r.pos = pos; return ${out};`; } else { const literal = pairs.map((p) => `${p.key}: ${p.expr}`).join(', '); decodeBody = `${pre} r.pos = pos; return { ${literal} };`; } const deps = new Map(); for (const [k, v] of encCtx.deps) deps.set(k, v); for (const [k, v] of decCtx.deps) deps.set(k, v); const closure = new Map(); for (const [k, v] of encCtx.closure) closure.set(k, v); for (const [k, v] of decCtx.closure) closure.set(k, v); return { encodeBody, decodeBody, deps, closure }; } export function compileUnion( schema: UnionSchema, _options: CompileOptions = {}, ): CodegenResult { // Class-bound unions are not yet supported (each variant would need its own // prototype binding). For now, ignore `boundProtoExpr` and emit a plain // literal-based decoder. const encCtx = new Ctx('enc'); const decCtx = new Ctx('dec'); const encSeg = new SegBuilder(); emitEnc(schema, encCtx, encSeg, 'o'); const encodeBody = encSeg.build(); const decRes = emitDec(schema, decCtx); const decodeBody = `${decRes.pre} r.pos = pos; return ${decRes.expr};`; const deps = new Map(); for (const [k, v] of encCtx.deps) deps.set(k, v); for (const [k, v] of decCtx.deps) deps.set(k, v); const closure = new Map(); for (const [k, v] of encCtx.closure) closure.set(k, v); for (const [k, v] of decCtx.closure) closure.set(k, v); return { encodeBody, decodeBody, deps, closure }; }