fix: EXEC.IF branch reconciliation still injects synthetic zeros (#618) (#1033)

Fixes #618

## Changes
Add stack depth validation in processExecIf() so asymmetric EXEC.IF branches (where one branch pushes more values than the other) throw an explicit error instead of silently padding with '0'. Error messages identify both branch depths for DYADIC and BOOLEAN stacks. Removed dead-code '0'/'false' fallbacks in buildAssignments and reconstruction. Updated existing unbalanced-branch tests to expect errors; added regression tests for error message content and BOOLEAN mismatch. All existing seed files (optimizer_v3.push3, optimizer_seed.push3) continue to transpile.

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/1033
Reviewed-by: Disinto_bot <disinto_bot@noreply.codeberg.org>
This commit is contained in:
johba 2026-03-20 08:42:34 +01:00
parent 30abee68b8
commit 0cb2e7ba07
2 changed files with 56 additions and 21 deletions

View file

@ -255,8 +255,24 @@ function processExecIf(
processItems(toItems(falseBranch), falseState);
state.varCounter = falseState.varCounter;
// --- Validate branch stack depths match ---
if (trueState.dStack.length !== falseState.dStack.length) {
throw new Error(
`EXEC.IF: DYADIC stack depth mismatch — ` +
`true branch produces ${trueState.dStack.length} values, ` +
`false branch produces ${falseState.dStack.length}`
);
}
if (trueState.bStack.length !== falseState.bStack.length) {
throw new Error(
`EXEC.IF: BOOLEAN stack depth mismatch — ` +
`true branch produces ${trueState.bStack.length} values, ` +
`false branch produces ${falseState.bStack.length}`
);
}
// --- Compare dStacks elementwise ---
const maxDLen = Math.max(trueState.dStack.length, falseState.dStack.length);
const maxDLen = trueState.dStack.length; // both are equal after validation
// 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++) {
@ -272,7 +288,7 @@ function processExecIf(
}
// --- Compare bStacks elementwise ---
const maxBLen = Math.max(trueState.bStack.length, falseState.bStack.length);
const maxBLen = trueState.bStack.length; // both are equal after validation
const bResultMap = new Map<number, string>();
for (let k = 0; k < maxBLen; k++) {
const tv = trueState.bStack[k];
@ -296,11 +312,11 @@ function processExecIf(
const pad = ' '.repeat(indentLevel);
const assignments: string[] = [];
for (const [k, rv] of dMap) {
const val = branchState.dStack[k] ?? '0';
const val = branchState.dStack[k]!;
assignments.push(`${pad}${rv} = uint256(${val});`);
}
for (const [k, rv] of bMap) {
const val = branchState.bStack[k] ?? 'false';
const val = branchState.bStack[k]!;
assignments.push(`${pad}${rv} = ${val};`);
}
return assignments;
@ -326,12 +342,12 @@ function processExecIf(
const newDStack: string[] = [];
for (let k = 0; k < maxDLen; k++) {
const rv = dResultMap.get(k);
newDStack.push(rv ?? (dBefore[k] ?? trueState.dStack[k] ?? '0'));
newDStack.push(rv ?? (dBefore[k] ?? trueState.dStack[k]!));
}
const newBStack: string[] = [];
for (let k = 0; k < maxBLen; k++) {
const rv = bResultMap.get(k);
newBStack.push(rv ?? (bBefore[k] ?? trueState.bStack[k] ?? 'false'));
newBStack.push(rv ?? (bBefore[k] ?? trueState.bStack[k]!));
}
state.dStack = newDStack;

View file

@ -188,8 +188,7 @@ describe('EXEC.IF balanced branches', () => {
// ---------------------------------------------------------------------------
describe('EXEC.IF unbalanced branches', () => {
it('true branch pushes more values than false branch', () => {
// True branch pushes 2 values, false branch pushes 1
it('true branch pushes more DYADIC values than false branch → error', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
@ -198,13 +197,10 @@ describe('EXEC.IF unbalanced branches', () => {
(10 20)
(10)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('if ('), 'should emit if/else');
// The missing value in false branch defaults to '0'
assert.throws(() => run(src), /DYADIC stack depth mismatch/);
});
it('false branch pushes more values than true branch', () => {
it('false branch pushes more DYADIC values than true branch → error', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
@ -213,24 +209,33 @@ describe('EXEC.IF unbalanced branches', () => {
(10)
(10 20)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('if ('), 'should emit if/else');
assert.throws(() => run(src), /DYADIC stack depth mismatch/);
});
it('one branch is empty (no-op)', () => {
it('error message includes both branch depths', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
100 50 DYADIC.>
EXEC.IF
(10 20 30)
(10)
)`;
assert.throws(() => run(src), /true branch produces 3.*false branch produces 1/);
});
it('one branch pushes extra, the other is no-op → depth mismatch error', () => {
// True branch pushes 2 extra values, false branch leaves stack unchanged
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
50
100 50 DYADIC.>
EXEC.IF
(DYADIC.POP 99)
(99 88)
()
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('if ('), 'should emit if/else');
assert.throws(() => run(src), /DYADIC stack depth mismatch/);
});
it('EXEC.IF with missing branches throws', () => {
@ -240,6 +245,20 @@ describe('EXEC.IF unbalanced branches', () => {
)`;
assert.throws(() => run(src), /EXEC\.IF: missing branches/);
});
it('BOOLEAN stack depth mismatch between branches → error', () => {
// true branch pushes a boolean comparison, false branch does not
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
10 20 30 40
TRUE
EXEC.IF
(10 5 DYADIC.>)
()
)`;
assert.throws(() => run(src), /BOOLEAN stack depth mismatch/);
});
});
// ---------------------------------------------------------------------------