Update tsconfig.json to use NodeNext module system (fixes CJS/ESM conflict), enable ts-node ESM mode, and add .js extensions to relative imports so the built output and ts-node dev script both work correctly with "type":"module".
403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
/**
|
|
* 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<string, string>; // 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<number, string>(); // 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<number, string>();
|
|
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<number, string>,
|
|
bMap: Map<number, string>,
|
|
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 };
|
|
}
|