harb/tools/push3-transpiler/test/transpiler.test.ts
johba 0cb2e7ba07 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>
2026-03-20 08:42:34 +01:00

539 lines
17 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { parse } from '../src/parser.js';
import { transpile } from '../src/transpiler.js';
/**
* Helper: transpile a Push3 source string, returning the result.
* The transpiler primes 8 input slots on the DYADIC stack, so the program
* operates on top of those.
*/
function run(src: string) {
const ast = parse(src);
return transpile(ast);
}
// ---------------------------------------------------------------------------
// Stack underflow detection
// ---------------------------------------------------------------------------
describe('stack underflow', () => {
it('DYADIC.+ underflows when fewer than 2 extra values beyond inputs', () => {
// After 8 input slots are primed, DYADIC.+ pops two. Two pops from 8 is
// fine (no underflow). But after popping all 8 inputs + 1 more, underflow.
// Program: pop all 8 inputs, then try DYADIC.+ on an empty stack.
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.+
)`;
assert.throws(() => run(src), /DYADIC stack underflow/);
});
it('DYADIC.SWAP underflows on empty stack', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.SWAP
)`;
assert.throws(() => run(src), /DYADIC stack underflow/);
});
it('DYADIC.DUP underflows on empty stack', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.DUP
)`;
assert.throws(() => run(src), /DYADIC stack underflow/);
});
it('DYADIC.POP underflows on empty stack', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP
)`;
assert.throws(() => run(src), /DYADIC stack underflow/);
});
it('BOOLEAN.NOT underflows on empty boolean stack', () => {
const src = '(BOOLEAN.NOT)';
assert.throws(() => run(src), /BOOLEAN stack underflow/);
});
it('BOOLEAN.AND underflows on empty boolean stack', () => {
const src = '(BOOLEAN.AND)';
assert.throws(() => run(src), /BOOLEAN stack underflow/);
});
it('BOOLEAN.OR underflows with only one boolean', () => {
const src = '(TRUE BOOLEAN.OR)';
assert.throws(() => run(src), /BOOLEAN stack underflow/);
});
it('DYADIC.DEFINE underflows NAME stack', () => {
// Push a value on DYADIC but no name on NAME stack
const src = '(42 DYADIC.DEFINE)';
assert.throws(() => run(src), /NAME stack underflow/);
});
it('DYADIC.> underflows when only one value on stack after clearing', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.>
)`;
assert.throws(() => run(src), /DYADIC stack underflow/);
});
it('EXEC.IF underflows when boolean stack is empty', () => {
// EXEC.IF needs a boolean condition — pops from bStack
const src = '(EXEC.IF (1) (2))';
assert.throws(() => run(src), /BOOLEAN stack underflow/);
});
});
// ---------------------------------------------------------------------------
// Stack overflow / excess values — transpiler handles gracefully
// ---------------------------------------------------------------------------
describe('stack overflow / excess values', () => {
it('extra values on DYADIC stack are silently discarded (top 4 taken)', () => {
// Push 6 values on top of the 8 inputs = 14 total. Top 4 become outputs.
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
100 200 300 400 500 600
)`;
const result = run(src);
// Top of stack = 600 → ci, 500 → anchorShare, 400 → anchorWidth, 300 → discoveryDepth
assert.equal(result.ciVar, '600');
assert.equal(result.anchorShareVar, '500');
assert.equal(result.anchorWidthVar, '400');
assert.equal(result.discoveryDepthVar, '300');
});
it('fewer than 4 values uses bear defaults for missing positions', () => {
// Clear all 8, push only 1 value
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
42
)`;
const result = run(src);
assert.equal(result.ciVar, '42');
// Missing positions get bear defaults
assert.equal(result.anchorShareVar, '300000000000000000');
assert.equal(result.anchorWidthVar, '100');
assert.equal(result.discoveryDepthVar, '300000000000000000');
});
it('empty program (no instructions) uses inputs as outputs', () => {
// No instructions at all — the 8 primed input slots remain.
// Top 4 = inputs[0] through inputs[3].
const src = '()';
const result = run(src);
assert.equal(result.ciVar, 'uint256(inputs[0].mantissa)');
assert.equal(result.anchorShareVar, 'uint256(inputs[1].mantissa)');
assert.equal(result.anchorWidthVar, 'uint256(inputs[2].mantissa)');
assert.equal(result.discoveryDepthVar, 'uint256(inputs[3].mantissa)');
});
});
// ---------------------------------------------------------------------------
// EXEC.IF branching — balanced branches
// ---------------------------------------------------------------------------
describe('EXEC.IF balanced branches', () => {
it('emits if/else for simple balanced branches', () => {
// Clear stack, push condition value, compare, then branch
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 40)
(1 2 3 4)
)`;
const result = run(src);
// Both branches push 4 values; the result depends on the condition
// The output should contain if/else structure
const body = result.functionBody.join('\n');
assert.ok(body.includes('if ('), 'should contain if statement');
// Result vars should be used for differing positions
assert.ok(result.ciVar !== undefined);
assert.ok(result.anchorShareVar !== undefined);
});
it('both branches produce same value → no result variable needed', () => {
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 99)
(1 2 3 99)
)`;
const result = run(src);
// Both branches have 99 at bottom (same position from before) — doesn't need result var for that
// But top 3 differ → need result vars for those
const body = result.functionBody.join('\n');
assert.ok(body.includes('if ('), 'should emit if/else');
});
});
// ---------------------------------------------------------------------------
// EXEC.IF branching — unbalanced branches
// ---------------------------------------------------------------------------
describe('EXEC.IF unbalanced branches', () => {
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
100 50 DYADIC.>
EXEC.IF
(10 20)
(10)
)`;
assert.throws(() => run(src), /DYADIC stack depth mismatch/);
});
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
100 50 DYADIC.>
EXEC.IF
(10)
(10 20)
)`;
assert.throws(() => run(src), /DYADIC stack depth mismatch/);
});
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
(99 88)
()
)`;
assert.throws(() => run(src), /DYADIC stack depth mismatch/);
});
it('EXEC.IF with missing branches throws', () => {
// EXEC.IF needs exactly 2 subsequent items
const src = `(
TRUE EXEC.IF (1)
)`;
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/);
});
});
// ---------------------------------------------------------------------------
// EXEC.IF — nested branching
// ---------------------------------------------------------------------------
describe('EXEC.IF nested branches', () => {
it('nested EXEC.IF produces valid output', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
100 50 DYADIC.>
EXEC.IF
(
100 80 DYADIC.>
EXEC.IF
(1 2 3 4)
(5 6 7 8)
)
(10 20 30 40)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
// Should contain nested if blocks
const ifCount = (body.match(/if \(/g) || []).length;
assert.ok(ifCount >= 2, `expected at least 2 if statements, got ${ifCount}`);
});
});
// ---------------------------------------------------------------------------
// Arithmetic and comparisons
// ---------------------------------------------------------------------------
describe('arithmetic operations', () => {
it('DYADIC.+ produces addition expression', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
10 20 DYADIC.+
)`;
const result = run(src);
assert.equal(result.ciVar, '(10 + 20)');
});
it('DYADIC.- produces subtraction expression', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
10 3 DYADIC.-
)`;
const result = run(src);
assert.equal(result.ciVar, '(10 - 3)');
});
it('DYADIC.* produces multiplication expression', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
5 6 DYADIC.*
)`;
const result = run(src);
assert.equal(result.ciVar, '(5 * 6)');
});
it('DYADIC./ produces safe division expression', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
10 3 DYADIC./
)`;
const result = run(src);
assert.equal(result.ciVar, '(3 == 0 ? 0 : 10 / 3)');
});
it('DYADIC.> produces comparison on boolean stack', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
10 5 DYADIC.>
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('(10 > 5)'), 'condition should contain (10 > 5)');
});
it('DYADIC.< produces less-than comparison', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
3 9 DYADIC.<
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('(3 < 9)'));
});
it('DYADIC.>= produces >= comparison', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
3 3 DYADIC.>=
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('(3 >= 3)'));
});
it('DYADIC.<= produces <= comparison', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
5 10 DYADIC.<=
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('(5 <= 10)'));
});
});
// ---------------------------------------------------------------------------
// Boolean operations
// ---------------------------------------------------------------------------
describe('boolean operations', () => {
it('BOOLEAN.NOT negates a boolean', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
TRUE BOOLEAN.NOT
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('!(true)'));
});
it('BOOLEAN.AND combines two booleans', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
TRUE FALSE BOOLEAN.AND
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('(true && false)'));
});
it('BOOLEAN.OR combines two booleans', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
TRUE FALSE BOOLEAN.OR
EXEC.IF (1 2 3 4) (5 6 7 8)
)`;
const result = run(src);
const body = result.functionBody.join('\n');
assert.ok(body.includes('(true || false)'));
});
});
// ---------------------------------------------------------------------------
// Name binding and DEFINE
// ---------------------------------------------------------------------------
describe('DYADIC.DEFINE and name binding', () => {
it('binds a name and retrieves it', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
42 FOO DYADIC.DEFINE
FOO
)`;
const result = run(src);
// FOO should be bound to 'foo' variable, and ci should reference it
assert.equal(result.ciVar, 'foo');
const body = result.functionBody.join('\n');
assert.ok(body.includes('uint256 foo = uint256(42);'));
});
it('bound names push to dStack, not nameStack', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
99 X DYADIC.DEFINE
X X DYADIC.+
)`;
const result = run(src);
assert.equal(result.ciVar, '(x + x)');
});
});
// ---------------------------------------------------------------------------
// DYADIC.DUP and DYADIC.SWAP
// ---------------------------------------------------------------------------
describe('DYADIC.DUP and DYADIC.SWAP', () => {
it('DYADIC.DUP duplicates top of stack', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
42 DYADIC.DUP DYADIC.+
)`;
const result = run(src);
// DUP materialises into dup0, then + is (dup0 + dup0)
assert.ok(result.ciVar.includes('dup0'));
const body = result.functionBody.join('\n');
assert.ok(body.includes('uint256 dup0 = uint256(42);'));
});
it('DYADIC.SWAP exchanges top two values', () => {
const src = `(
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
10 20 DYADIC.SWAP
)`;
const result = run(src);
// After SWAP: bottom=20, top=10
assert.equal(result.ciVar, '10');
assert.equal(result.anchorShareVar, '20');
});
});
// ---------------------------------------------------------------------------
// Unsupported instruction
// ---------------------------------------------------------------------------
describe('unsupported instructions', () => {
it('throws on unknown instruction', () => {
const src = '(CODE.NOOP)';
assert.throws(() => run(src), /Unsupported instruction: CODE.NOOP/);
});
});
// ---------------------------------------------------------------------------
// Integration: optimizer_seed.push3-style programs
// ---------------------------------------------------------------------------
describe('integration: bear/bull program', () => {
it('simple bear/bull program transpiles without error', () => {
// Simplified version of the optimizer_v3.push3 structure
const src = `(
PERCENTAGESTAKED DYADIC.DEFINE
TAXRATE DYADIC.DEFINE
DYADIC.POP DYADIC.POP DYADIC.POP DYADIC.POP
DYADIC.POP DYADIC.POP
PERCENTAGESTAKED 100 DYADIC.* 1000000000000000000 DYADIC./
STAKED DYADIC.DEFINE
STAKED 91 DYADIC.>
EXEC.IF
(
1000000000000000000
20
1000000000000000000
0
)
(
300000000000000000
100
300000000000000000
0
)
)`;
const result = run(src);
// Should produce valid output with if/else
const body = result.functionBody.join('\n');
assert.ok(body.includes('if ('));
// Output vars should be result variables (rN) since branches differ
assert.ok(result.ciVar !== undefined);
assert.ok(result.functionBody.length > 0);
});
});