/** * Push3 → Solidity transpiler. * * Uses symbolic stack simulation (SSA-style). Each stack holds Solidity expression strings. * EXEC.IF runs both branches speculatively, then emits an if/else with result variables * for any stack positions that differ between branches. * * Only the subset of instructions used in optimizer_v3.push3 is implemented. */ import { Node } from './parser.js'; interface TranspilerState { dStack: string[]; // DYADIC stack (Solidity expressions) bStack: string[]; // BOOLEAN stack nameStack: string[]; // NAME stack (unbound identifiers) lines: string[]; // emitted Solidity statements bindings: Map; // identifier → Solidity variable name varCounter: number; // fresh variable counter (shared across branches) indent: number; // current indentation level } function freshVar(state: TranspilerState, prefix = 'v'): string { return `${prefix}${state.varCounter++}`; } function emit(state: TranspilerState, line: string): void { const pad = ' '.repeat(state.indent); state.lines.push(pad + line); } function dpop(state: TranspilerState, ctx: string): string { const v = state.dStack.pop(); if (v === undefined) throw new Error(`DYADIC stack underflow at ${ctx}`); return v; } function bpop(state: TranspilerState, ctx: string): string { const v = state.bStack.pop(); if (v === undefined) throw new Error(`BOOLEAN stack underflow at ${ctx}`); return v; } function processNode(node: Node, state: TranspilerState): void { switch (node.kind) { case 'int': state.dStack.push(node.value.toString()); break; case 'bool': state.bStack.push(node.value ? 'true' : 'false'); break; case 'name': // Unbound name goes to the NAME stack (separate from DYADIC stack) state.nameStack.push(node.text); break; case 'instr': processInstruction(node.name, state); break; case 'list': for (const item of node.items) { processNode(item, state); } break; } } function processInstruction(name: string, state: TranspilerState): void { switch (name) { // ---- DYADIC stack ops ---- case 'DYADIC.SWAP': { const a = dpop(state, 'DYADIC.SWAP'); const b = dpop(state, 'DYADIC.SWAP'); state.dStack.push(a); state.dStack.push(b); break; } case 'DYADIC.DUP': { const a = dpop(state, 'DYADIC.DUP'); // Materialise complex expressions before duplicating const vname = freshVar(state, 'dup'); emit(state, `uint256 ${vname} = uint256(${a});`); state.dStack.push(vname); state.dStack.push(vname); break; } case 'DYADIC.POP': { dpop(state, 'DYADIC.POP'); break; } // ---- DYADIC arithmetic ---- case 'DYADIC.*': { const b = dpop(state, 'DYADIC.*'); const a = dpop(state, 'DYADIC.*'); state.dStack.push(`(${a} * ${b})`); break; } case 'DYADIC./': { const b = dpop(state, 'DYADIC./'); const a = dpop(state, 'DYADIC./'); // Safe division: div-by-zero → 0, matching Push3 no-op semantics. state.dStack.push(`(${b} == 0 ? 0 : ${a} / ${b})`); break; } case 'DYADIC.+': { const b = dpop(state, 'DYADIC.+'); const a = dpop(state, 'DYADIC.+'); state.dStack.push(`(${a} + ${b})`); break; } case 'DYADIC.-': { const b = dpop(state, 'DYADIC.-'); const a = dpop(state, 'DYADIC.-'); state.dStack.push(`(${a} - ${b})`); break; } // ---- DYADIC comparisons → BOOLEAN ---- case 'DYADIC.>': { const b = dpop(state, 'DYADIC.>'); const a = dpop(state, 'DYADIC.>'); state.bStack.push(`(${a} > ${b})`); break; } case 'DYADIC.<': { const b = dpop(state, 'DYADIC.<'); const a = dpop(state, 'DYADIC.<'); state.bStack.push(`(${a} < ${b})`); break; } case 'DYADIC.>=': { const b = dpop(state, 'DYADIC.>='); const a = dpop(state, 'DYADIC.>='); state.bStack.push(`(${a} >= ${b})`); break; } case 'DYADIC.<=': { const b = dpop(state, 'DYADIC.<='); const a = dpop(state, 'DYADIC.<='); state.bStack.push(`(${a} <= ${b})`); break; } // ---- Name binding ---- case 'DYADIC.DEFINE': { const val = dpop(state, 'DYADIC.DEFINE (value)'); const id = state.nameStack.pop(); if (id === undefined) throw new Error('DYADIC.DEFINE: NAME stack underflow'); const varName = id.toLowerCase(); emit(state, `uint256 ${varName} = uint256(${val});`); state.bindings.set(id, varName); break; } // ---- BOOLEAN ---- case 'BOOLEAN.NOT': { const a = bpop(state, 'BOOLEAN.NOT'); state.bStack.push(`!(${a})`); break; } case 'BOOLEAN.AND': { const b = bpop(state, 'BOOLEAN.AND'); const a = bpop(state, 'BOOLEAN.AND'); state.bStack.push(`(${a} && ${b})`); break; } case 'BOOLEAN.OR': { const b = bpop(state, 'BOOLEAN.OR'); const a = bpop(state, 'BOOLEAN.OR'); state.bStack.push(`(${a} || ${b})`); break; } case 'EXEC.IF': throw new Error('EXEC.IF must be handled by processItems'); default: throw new Error(`Unsupported instruction: ${name}`); } } /** * Process a list of nodes sequentially, specially handling EXEC.IF by consuming * the next two items as true/false branches. */ function processItems(items: Node[], state: TranspilerState): void { let i = 0; while (i < items.length) { const item = items[i]; // Bound identifier → push its Solidity variable to dStack if (item.kind === 'name' && state.bindings.has(item.text)) { state.dStack.push(state.bindings.get(item.text)!); i++; continue; } // EXEC.IF — consume it plus the next two items (branches) if (item.kind === 'instr' && item.name === 'EXEC.IF') { const trueBranch = items[i + 1]; const falseBranch = items[i + 2]; if (!trueBranch || !falseBranch) throw new Error('EXEC.IF: missing branches'); i += 3; processExecIf(trueBranch, falseBranch, state); continue; } processNode(item, state); i++; } } function makeSubState(parent: TranspilerState, indentOffset = 1): TranspilerState { return { dStack: [...parent.dStack], bStack: [...parent.bStack], nameStack: [...parent.nameStack], lines: [], bindings: new Map(parent.bindings), varCounter: parent.varCounter, indent: parent.indent + indentOffset, }; } /** * Emit an if/else block for EXEC.IF. * * Both branches are simulated speculatively. We compare final stacks elementwise: * - Positions unchanged in both branches → keep as-is in parent * - Positions that differ → emit a result variable, assign in each branch * * This correctly handles both "new value produced" and "existing value mutated" cases. */ function processExecIf( trueBranch: Node, falseBranch: Node, state: TranspilerState, ): void { const cond = bpop(state, 'EXEC.IF condition'); const dBefore = [...state.dStack]; const bBefore = [...state.bStack]; // --- Simulate TRUE branch --- const trueState = makeSubState(state); processItems(toItems(trueBranch), trueState); state.varCounter = trueState.varCounter; // --- Simulate FALSE branch --- const falseState = makeSubState(state); processItems(toItems(falseBranch), falseState); state.varCounter = falseState.varCounter; // --- Compare dStacks elementwise --- const maxDLen = Math.max(trueState.dStack.length, falseState.dStack.length); // result var for each position that changed or is new const dResultMap = new Map(); // position → result var name for (let k = 0; k < maxDLen; k++) { const tv = trueState.dStack[k]; const fv = falseState.dStack[k]; const bv = dBefore[k]; // undefined for positions beyond dBefore if (tv !== undefined && fv !== undefined && tv === fv && tv === bv) { continue; // identical in both branches and unchanged from before — skip } const rv = freshVar(state, 'r'); emit(state, `uint256 ${rv};`); dResultMap.set(k, rv); } // --- Compare bStacks elementwise --- const maxBLen = Math.max(trueState.bStack.length, falseState.bStack.length); const bResultMap = new Map(); for (let k = 0; k < maxBLen; k++) { const tv = trueState.bStack[k]; const fv = falseState.bStack[k]; const bv = bBefore[k]; if (tv !== undefined && fv !== undefined && tv === fv && tv === bv) { continue; } const rv = freshVar(state, 'b'); emit(state, `bool ${rv};`); bResultMap.set(k, rv); } // --- Build branch assignments --- const buildAssignments = ( branchState: TranspilerState, dMap: Map, bMap: Map, indentLevel: number, ): string[] => { const pad = ' '.repeat(indentLevel); const assignments: string[] = []; for (const [k, rv] of dMap) { const val = branchState.dStack[k] ?? '0'; assignments.push(`${pad}${rv} = uint256(${val});`); } for (const [k, rv] of bMap) { const val = branchState.bStack[k] ?? 'false'; assignments.push(`${pad}${rv} = ${val};`); } return assignments; }; const trueAssign = buildAssignments(trueState, dResultMap, bResultMap, state.indent + 1); const falseAssign = buildAssignments(falseState, dResultMap, bResultMap, state.indent + 1); // --- Emit if/else --- const pad = ' '.repeat(state.indent); state.lines.push(`${pad}if (${cond}) {`); state.lines.push(...trueState.lines); state.lines.push(...trueAssign); const falseBody = [...falseState.lines, ...falseAssign]; if (falseBody.length > 0) { state.lines.push(`${pad}} else {`); state.lines.push(...falseBody); } state.lines.push(`${pad}}`); // --- Reconstruct parent stacks --- const newDStack: string[] = []; for (let k = 0; k < maxDLen; k++) { const rv = dResultMap.get(k); newDStack.push(rv ?? (dBefore[k] ?? trueState.dStack[k] ?? '0')); } const newBStack: string[] = []; for (let k = 0; k < maxBLen; k++) { const rv = bResultMap.get(k); newBStack.push(rv ?? (bBefore[k] ?? trueState.bStack[k] ?? 'false')); } state.dStack = newDStack; state.bStack = newBStack; } function toItems(node: Node): Node[] { return node.kind === 'list' ? node.items : [node]; } // ---- Public API ---- export interface TranspileResult { functionBody: string[]; ciVar: string; anchorShareVar: string; anchorWidthVar: string; discoveryDepthVar: string; } /** * Transpile a Push3 program (top-level list) into Solidity function body lines. * * Inputs are primed on the DYADIC stack as 8 dyadic rationals (slot 0 on top, * slot 7 at bottom). Each slot is represented as uint256(inputs[i].mantissa); * shift support is reserved for future evolution — callers must pass shift=0. * * The program must leave exactly 4 values on the DYADIC stack at termination: * top (index 3): capitalInefficiency * index 2: anchorShare * index 1: anchorWidth * bottom (index 0): discoveryDepth */ export function transpile(program: Node): TranspileResult { if (program.kind !== 'list') throw new Error('Expected top-level list'); // Prime DYADIC stack: slot 7 at bottom (index 0), slot 0 at top (index 7). const state: TranspilerState = { dStack: [ 'uint256(inputs[7].mantissa)', 'uint256(inputs[6].mantissa)', 'uint256(inputs[5].mantissa)', 'uint256(inputs[4].mantissa)', 'uint256(inputs[3].mantissa)', 'uint256(inputs[2].mantissa)', 'uint256(inputs[1].mantissa)', 'uint256(inputs[0].mantissa)', ], bStack: [], nameStack: [], lines: [], bindings: new Map(), varCounter: 0, indent: 2, }; processItems(program.items, state); // Pop 4 outputs: top → ci, then anchorShare, anchorWidth, discoveryDepth. // If the stack has fewer than 4 values, fall back to bear defaults for the // missing positions — matches Push3 no-op semantics (empty stack → 0/default). // If the stack has more than 4 values, take the top 4 and discard the rest. const ciVar = state.dStack.pop() ?? '0'; const anchorShareVar = state.dStack.pop() ?? '300000000000000000'; const anchorWidthVar = state.dStack.pop() ?? '100'; const discoveryDepthVar = state.dStack.pop() ?? '300000000000000000'; return { functionBody: state.lines, ciVar, anchorShareVar, anchorWidthVar, discoveryDepthVar }; }