harb/tools/push3-transpiler/src/transpiler.ts
openhands 3f24faba18 fix: package.json missing 'type': 'module' inconsistent with AGENTS.md (#850)
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".
2026-03-16 06:35:05 +00:00

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 };
}