fix: feat: Push3 default outputs — crash/no-output falls back to bear strategy (#634)

Three defensive layers so every Push3 program runs without reverting:

Layer A (transpiler/index.ts): assign bear defaults (CI=0, AS=0.3e18,
AW=100, DD=0.3e18) to all four outputs at the top of calculateParams.
Any output the evolved program does not overwrite keeps the safe default.

Layer B (transpiler/transpiler.ts): graceful stack underflow — dpop/bpop
return '0'/'false' instead of throwing, and the final output-pop falls
back to bear-default literals when fewer than 4 values remain on the
stack. Wrong output count no longer aborts transpilation.

Layer C (transpiler/transpiler.ts + index.ts): wrap the entire function
body in `unchecked {}` so integer overflow wraps (matching Push3), and
emit `(b == 0 ? 0 : a / b)` for every DYADIC./ (div-by-zero → 0,
matching Push3 no-op semantics).

Layer 2 (Optimizer.sol getLiquidityParams): clamp the three fraction
outputs (capitalInefficiency, anchorShare, discoveryDepth) to [0, 1e18]
after abi.decode so a buggy evolved program cannot produce out-of-range
values even if it runs without reverting.

Regenerated OptimizerV3Push3.sol with the updated transpiler; all 193
tests pass (34 Optimizer/OptimizerV3Push3 tests explicitly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-13 03:47:49 +00:00
parent 8e4bd905ac
commit c87064dc6c
4 changed files with 48 additions and 25 deletions

View file

@ -65,11 +65,23 @@ function main(): void {
' require(inputs[k].shift == 0, "shift not yet supported");',
' }',
'',
' // Layer A: bear defaults — any output not overwritten by the program keeps these.',
' // Matches Push3 no-op semantics: a program that crashes or produces no output',
' // returns safe bear-mode parameters rather than reverting.',
' ci = 0;',
' anchorShare = 300000000000000000;',
' anchorWidth = 100;',
' discoveryDepth = 300000000000000000;',
'',
' // Layer C: unchecked arithmetic — overflow wraps (matches Push3 semantics).',
' // Division by zero is guarded at the expression level (b == 0 ? 0 : a / b).',
' unchecked {',
...functionBody,
` ci = uint256(${ciVar});`,
` anchorShare = uint256(${anchorShareVar});`,
` anchorWidth = uint24(${anchorWidthVar});`,
` discoveryDepth = uint256(${discoveryDepthVar});`,
' }',
' }',
'}',
'',

View file

@ -31,13 +31,15 @@ function emit(state: TranspilerState, line: string): void {
function dpop(state: TranspilerState, ctx: string): string {
const v = state.dStack.pop();
if (v === undefined) throw new Error(`DYADIC stack underflow at ${ctx}`);
// Stack underflow → Push3 no-op semantics: treat missing value as 0
if (v === undefined) return '0';
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}`);
// Stack underflow → Push3 no-op semantics: treat missing bool as false
if (v === undefined) return 'false';
return v;
}
@ -102,7 +104,8 @@ function processInstruction(name: string, state: TranspilerState): void {
case 'DYADIC./': {
const b = dpop(state, 'DYADIC./');
const a = dpop(state, 'DYADIC./');
state.dStack.push(`(${a} / ${b})`);
// Safe division: div-by-zero → 0, matching Push3 no-op semantics.
state.dStack.push(`(${b} == 0 ? 0 : ${a} / ${b})`);
break;
}
case 'DYADIC.+': {
@ -391,15 +394,13 @@ export function transpile(program: Node): TranspileResult {
processItems(program.items, state);
// Pop 4 outputs: top → ci, then anchorShare, anchorWidth, discoveryDepth.
if (state.dStack.length !== 4) {
throw new Error(
`Program must leave exactly 4 values on the DYADIC stack; found ${state.dStack.length}`,
);
}
const ciVar = state.dStack.pop()!;
const anchorShareVar = state.dStack.pop()!;
const anchorWidthVar = state.dStack.pop()!;
const discoveryDepthVar = state.dStack.pop()!;
// 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 };
}