fix: No tests for transpiler stack-depth validation (#619)
Add TypeScript unit test suite for the Push3 transpiler using Node's built-in test runner (node:test) with tsx. 47 tests across 12 suites covering parser, stack underflow/overflow, EXEC.IF balanced/unbalanced/ nested branching, arithmetic, boolean ops, name binding, and integration. Update CI to run `npm test` (which now includes unit tests + existing bash tests) and scope transpiler-tests step to only trigger on changes to tools/push3-transpiler/**. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
df3389bdeb
commit
793d875b4a
4 changed files with 597 additions and 3 deletions
|
|
@ -65,6 +65,11 @@ steps:
|
|||
|
||||
- name: transpiler-tests
|
||||
image: registry.niovi.voyage/harb/node-ci:latest
|
||||
when:
|
||||
- event: pull_request
|
||||
path:
|
||||
include:
|
||||
- tools/push3-transpiler/**
|
||||
commands:
|
||||
- |
|
||||
bash -c '
|
||||
|
|
@ -73,8 +78,7 @@ steps:
|
|||
cd tools/push3-transpiler
|
||||
npm install --silent
|
||||
npm run build
|
||||
bash test_inject_extraction.sh
|
||||
bash test_transpiler_clamping.sh
|
||||
npm test
|
||||
'
|
||||
|
||||
- name: single-package-manager
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"scripts": {
|
||||
"build": "tsc",
|
||||
"transpile": "tsx src/index.ts",
|
||||
"test": "bash test_inject_extraction.sh && bash test_transpiler_clamping.sh"
|
||||
"test": "tsx --test test/*.test.ts && bash test_inject_extraction.sh && bash test_transpiler_clamping.sh"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
70
tools/push3-transpiler/test/parser.test.ts
Normal file
70
tools/push3-transpiler/test/parser.test.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parse, Node } from '../src/parser.js';
|
||||
|
||||
describe('parser', () => {
|
||||
it('parses integer literals', () => {
|
||||
const node = parse('(42)');
|
||||
assert.equal(node.kind, 'list');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.equal(node.items.length, 1);
|
||||
assert.deepStrictEqual(node.items[0], { kind: 'int', value: 42n });
|
||||
});
|
||||
|
||||
it('parses negative integer literals', () => {
|
||||
const node = parse('(-5)');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.deepStrictEqual(node.items[0], { kind: 'int', value: -5n });
|
||||
});
|
||||
|
||||
it('parses boolean literals', () => {
|
||||
const node = parse('(TRUE FALSE)');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.deepStrictEqual(node.items[0], { kind: 'bool', value: true });
|
||||
assert.deepStrictEqual(node.items[1], { kind: 'bool', value: false });
|
||||
});
|
||||
|
||||
it('parses instructions', () => {
|
||||
const node = parse('(DYADIC.+ BOOLEAN.NOT EXEC.IF)');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.deepStrictEqual(node.items[0], { kind: 'instr', name: 'DYADIC.+' });
|
||||
assert.deepStrictEqual(node.items[1], { kind: 'instr', name: 'BOOLEAN.NOT' });
|
||||
assert.deepStrictEqual(node.items[2], { kind: 'instr', name: 'EXEC.IF' });
|
||||
});
|
||||
|
||||
it('parses unbound names', () => {
|
||||
const node = parse('(TAXRATE STAKED)');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.deepStrictEqual(node.items[0], { kind: 'name', text: 'TAXRATE' });
|
||||
assert.deepStrictEqual(node.items[1], { kind: 'name', text: 'STAKED' });
|
||||
});
|
||||
|
||||
it('parses nested lists', () => {
|
||||
const node = parse('(1 (2 3))');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.equal(node.items.length, 2);
|
||||
const inner = node.items[1];
|
||||
assert.equal(inner.kind, 'list');
|
||||
if (inner.kind !== 'list') throw new Error('unreachable');
|
||||
assert.equal(inner.items.length, 2);
|
||||
});
|
||||
|
||||
it('strips comments', () => {
|
||||
const node = parse('(;; this is a comment\n42)');
|
||||
if (node.kind !== 'list') throw new Error('unreachable');
|
||||
assert.equal(node.items.length, 1);
|
||||
assert.deepStrictEqual(node.items[0], { kind: 'int', value: 42n });
|
||||
});
|
||||
|
||||
it('throws on empty program', () => {
|
||||
assert.throws(() => parse(''), /Empty program/);
|
||||
});
|
||||
|
||||
it('throws on unmatched open paren', () => {
|
||||
assert.throws(() => parse('(1 2'), /Unmatched/);
|
||||
});
|
||||
|
||||
it('throws on trailing tokens', () => {
|
||||
assert.throws(() => parse('(1) 2'), /Unexpected tokens/);
|
||||
});
|
||||
});
|
||||
520
tools/push3-transpiler/test/transpiler.test.ts
Normal file
520
tools/push3-transpiler/test/transpiler.test.ts
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
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 values than false branch', () => {
|
||||
// True branch pushes 2 values, false branch pushes 1
|
||||
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)
|
||||
)`;
|
||||
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'
|
||||
});
|
||||
|
||||
it('false branch pushes more values than true 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)
|
||||
(10 20)
|
||||
)`;
|
||||
const result = run(src);
|
||||
const body = result.functionBody.join('\n');
|
||||
assert.ok(body.includes('if ('), 'should emit if/else');
|
||||
});
|
||||
|
||||
it('one branch is empty (no-op)', () => {
|
||||
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)
|
||||
()
|
||||
)`;
|
||||
const result = run(src);
|
||||
const body = result.functionBody.join('\n');
|
||||
assert.ok(body.includes('if ('), 'should emit if/else');
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue