diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c46aa2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +onchain/cache/ +onchain/out/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e9dccc6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ + +[submodule "onchain/lib/forge-std"] + path = onchain/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/Interpreter.md b/Interpreter.md new file mode 100644 index 0000000..3e8fcc4 --- /dev/null +++ b/Interpreter.md @@ -0,0 +1,124 @@ +# Interpreter.md + +## Overview + +This repository provides a **Push3-like interpreter** implemented in Solidity. It uses a _token-based bytecode_ approach to parse programs from a `bytes` array (the `code`) into typed **descriptors**, which are then executed by the interpreter. + +**Push3** is a stack-based language originally designed for genetic programming and autoconstructive evolution. Each data type (e.g. `INT_LITERAL`, `FLOAT`, `BOOLEAN`, etc.) has its own stack. The **`EXEC`** stack manages the flow of execution. + +**In this implementation**: +- We store code in a single `bytes` array (`code`). +- We parse sublists and integer literals from that array. +- We push instructions and literals onto an execution stack. +- We pop them in LIFO order and execute them. + +### Key Features + +1. **Token-Based Parsing**: We define small tokens (`0x00` => NOOP, `0x01` => INTEGER_PLUS, `0x02` => INT_LITERAL, `0x03` => SUBLIST). If we see `INT_LITERAL`, we read 4 bytes for a 32-bit integer; if we see `SUBLIST`, we read 2 bytes for the sublist length, then parse it recursively. +2. **Descriptor System**: We represent instructions, literals, and sublists using 256-bit “descriptors.” Each descriptor encodes: + - A `tag` (top 8 bits): e.g. `INT_LITERAL`, `INSTRUCTION`, `SUBLIST`. + - `offset` and `length` for sublists. + - A leftover field (e.g. for storing integer data). +3. **Reversing Items**: When we pop a sublist descriptor, we parse it into descriptors and then push them onto the `EXEC` stack in reverse order. This ensures the list’s leftmost token executes first, consistent with the Push3 spec. +4. **Separation of Parsing and Execution**: + - **Parsing**: Transforms raw byte tokens into typed descriptors (instructions, literals, or nested sublists). + - **Execution**: The `runInterpreter` function repeatedly pops from `EXEC`, decides if it’s an instruction or literal or sublist, and acts accordingly. + +## Code Structure + +### 1. **Enums** + +- **`CodeTag`**: + - `NO_TAG` (unused), + - `INSTRUCTION`, + - `INT_LITERAL`, + - `SUBLIST`. + +- **`OpCode`**: + - `NOOP`, + - `INTEGER_PLUS`. + +### 2. **Descriptor Layout** + +A **256-bit** word has: +``` +[ top 8 bits | offset(32 bits) | length(32 bits) | leftover(184 bits) ] +``` +- `tag`: The top 8 bits (e.g. `INT_LITERAL`). +- `offset, length`: If `tag = SUBLIST`, these define which bytes in `code` represent the sub-sublists. +- `leftover`: If `tag = INT_LITERAL`, we store the 32-bit integer in the bottom 32 bits of leftover. If `tag = INSTRUCTION`, we store the opcode (`NOOP`, `INTEGER_PLUS`) in the lowest 8 bits of leftover. + +### 3. **Helper Reads** + +- **`readUint32(code, start)`**: Copies 4 bytes from calldata into a small buffer, returns the final `uint32`. By shifting bits if needed, we ensure we get the integer from the correct part of the loaded word. +- **`readUint16(code, start)`**: Similar logic for 2 bytes. + +### 4. **`parseSublist` Function** + +When we see a sublist descriptor (`tag=SUBLIST`), we parse the slice `[off..off+len]` in the `bytes code`. We iterate token by token: +- `0x00` => `NOOP` +- `0x01` => `INTEGER_PLUS` +- `0x02` => read 4 bytes => `INT_LITERAL` +- `0x03` => read 2 bytes => sub-sublist length => build a sub-descriptor + +We accumulate these descriptors in an array. Then, later, we push them _in reverse_ onto the `EXEC` stack. + +### 5. **`runInterpreter` Function** + +1. Loads initial CODE, EXEC, and INT stacks from the provided arrays. +2. While `EXEC` is non-empty: + - Pop the top descriptor. + - If `INSTRUCTION`, decode the opcode (e.g. `INTEGER_PLUS`) and do it (pop top 2 ints, push their sum). + - If `INT_LITERAL`, push that integer onto the int stack. + - If `SUBLIST`, parse that sublist into descriptors, then push those in reverse. + +## How to Contribute + +1. **Add New Instructions** + - Expand `OpCode` enum (e.g. `INTEGER_MINUS`, `INTEGER_MUL`, etc.). + - Write a `makeInstruction` or `makeIntLiteral` function to build the descriptor for it. + - In the main loop, implement the logic (pop the correct number of integers, do the operation, push the result). + +2. **Support More Data Types** + - For instance, a `FLOAT` or `BOOLEAN` stack. + - Add a new tag (`FLOAT_LITERAL`?), new instructions (`FLOAT.PLUS`?), etc. + +3. **Improve Parsing** + - If you want 256-bit literals, define a new token that reads 32 bytes, or do other expansions (like `CODE` type). + - Or define advanced sublist structures (like nested `( ( ) )` expansions). + +4. **Gas Optimization** + - Check how large arrays are allocated (like in `parseSublist`). + - Possibly implement step-by-step execution if large code exceeds block gas limits. + +5. **Add Logging & Debug Tools** + - Use `console.log(...)` to trace tokens, track final states, or measure performance. + +6. **Test Coverage** + - Provide more unit tests in Foundry for advanced instructions, nested sublists, edge cases with zero-length sublists, etc. + +## Next Steps + +### A. Extending the VM + +A “full” Push3 VM might include: +- **Multiple Data Stacks**: `BOOLEAN`, `FLOAT`, `NAME`, etc. +- **NAME Binding**: Let `NAME`s reference values or code. +- **Control Structures**: `EXEC.IF`, `EXEC.DO*COUNT`, or combinators like `EXEC.Y`. + +### B. Example Project + +You could use this interpreter in: +- A genetic programming environment, where random code is generated and mutated. +- A puzzle solver that runs tiny “scripts” in EVM-like constraints. + +## Conclusion + +This `Push3Interpreter` is a **minimal** scaffold for a token-based, list-expanding interpreter with typed descriptors. By following the patterns for instructions, sublists, and typed stacks, you can easily expand it into a more complete VM. We welcome contributions in the form of new instructions, data types, and performance optimizations. + +1. **Clone** this repo. +2. **Check** the `test/` folder for Foundry-based tests. +3. **Add** or modify instructions & sublists in `parseSublist` and `runInterpreter`. +4. **Run** `forge test --via-ir -vv` to see logs and confirm your changes. + +Enjoy exploring the **Push3** style VM in Solidity! \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..15b3d04 --- /dev/null +++ b/flake.lock @@ -0,0 +1,111 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "foundry": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1733217107, + "narHash": "sha256-T5rE+F85pRWkG7efabISw7ZNLgCBhnAg6egu5uGT4Bg=", + "owner": "shazow", + "repo": "foundry.nix", + "rev": "b0534fca03a058756b4a405aacd5f5059a879673", + "type": "github" + }, + "original": { + "owner": "shazow", + "ref": "monthly", + "repo": "foundry.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1666753130, + "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1734435836, + "narHash": "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4989a246d7a390a859852baddb1013f825435cee", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "foundry": "foundry", + "nixpkgs": "nixpkgs_2", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d4ce681 --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +# flake.nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + utils.url = "github:numtide/flake-utils"; + foundry.url = "github:shazow/foundry.nix/monthly"; # Use monthly branch for permanent releases + }; + + outputs = { self, nixpkgs, utils, foundry }: + utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ foundry.overlay ]; + }; + in { + + devShell = with pkgs; mkShell { + buildInputs = [ + # From the foundry overlay + # Note: Can also be referenced without overlaying as: foundry.defaultPackage.${system} + foundry-bin + + # ... any other dependencies we need + solc + ]; + + # Decorative prompt override so we know when we're in a dev shell + shellHook = '' + export PS1="[dev] $PS1" + ''; + }; + }); +} diff --git a/onchain/foundry.toml b/onchain/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/onchain/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/onchain/lib/forge-std b/onchain/lib/forge-std new file mode 160000 index 0000000..b93cf4b --- /dev/null +++ b/onchain/lib/forge-std @@ -0,0 +1 @@ +Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66 diff --git a/onchain/script/Counter.s.sol b/onchain/script/Counter.s.sol new file mode 100644 index 0000000..f12da74 --- /dev/null +++ b/onchain/script/Counter.s.sol @@ -0,0 +1,19 @@ +/* // SPDX-License-Identifier: UNLICENSED */ +/* pragma solidity ^0.8.13; */ + +/* import {Script, console} from "forge-std/Script.sol"; */ +/* import {Counter} from "../src/Counter.sol"; */ + +/* contract CounterScript is Script { */ +/* Counter public counter; */ + +/* function setUp() public {} */ + +/* function run() public { */ +/* vm.startBroadcast(); */ + +/* counter = new Counter(); */ + +/* vm.stopBroadcast(); */ +/* } */ +/* } */ diff --git a/onchain/src/Push3IntAdapter.sol b/onchain/src/Push3IntAdapter.sol new file mode 100644 index 0000000..40f4c39 --- /dev/null +++ b/onchain/src/Push3IntAdapter.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/console.sol"; +import {Push3Interpreter} from "../src/Push3Interpreter.sol"; + +/** + * @title Push3IntAdapter + * @dev An adapter contract for our Push3Interpreter that provides + * various integer-only I/O function overloads. We now create + * a SUBLIST descriptor for the entire code array, placing it + * on the EXEC stack. We ignore finalCodeStack/finalExecStack + * to avoid compiler warnings. + */ +contract Push3IntAdapter { + Push3Interpreter public interpreter; + + constructor(address _interpreter) { + interpreter = Push3Interpreter(_interpreter); + } + + /** + * @notice Utility function to build a single sublist descriptor + * covering the entire `code` array, offset=0, length=code.length. + */ + function _makeFullSublistDescriptor(bytes calldata code) + internal + view + returns (uint256) + { + // CodeTag.SUBLIST is typically enum = 3, but + // let's fetch it via + // (uint8)Push3Interpreter.CodeTag.SUBLIST + // or we can directly pass 3 if we know the numeric value. + + // We'll do: + return interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, // the tag + 0, // offset + uint32(code.length), // length of entire code array + 0 // leftover bits=0 + ); + } + + /** + * @notice Single int input, single int output. + * 1) Build a sublist descriptor for the entire code, + * 2) Place that descriptor in the EXEC stack, + * 3) Preload `in1` on the integer stack, + * 4) Run the interpreter, + * 5) Return top of final int stack. + */ + function run1In1Out(bytes calldata code, int256 in1) + external + view + returns (int256 out1) + { + // 1) Build the sublist descriptor + uint256 fullDesc = _makeFullSublistDescriptor(code); + + // 2) EXEC stack of size 1 + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = fullDesc; + + // No code stack + uint256[] memory initCodeStack = new uint256[](0); + + // 3) Preload int stack + int256[] memory initIntStack = new int256[](1); + initIntStack[0] = in1; + + // 4) Run + (, , int256[] memory finalIntStack) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // 5) Return top of final int stack + require(finalIntStack.length > 0, "No integer output produced"); + out1 = finalIntStack[finalIntStack.length - 1]; + } + + /** + * @notice Two ints input, single int output + */ + function run2In1Out(bytes calldata code, int256 in1, int256 in2) + external + view + returns (int256 out1) + { + uint256 fullDesc = _makeFullSublistDescriptor(code); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = fullDesc; + + uint256[] memory initCodeStack = new uint256[](0); + + int256[] memory initIntStack = new int256[](2); + initIntStack[0] = in1; + initIntStack[1] = in2; + + (, , int256[] memory finalIntStack) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + require(finalIntStack.length > 0, "No integer output"); + + out1 = finalIntStack[finalIntStack.length - 1]; + } + + /** + * @notice Single int input, two int outputs + */ + function run1In2Out(bytes calldata code, int256 in1) + external + view + returns (int256 out1, int256 out2) + { + uint256 fullDesc = _makeFullSublistDescriptor(code); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = fullDesc; + + uint256[] memory initCodeStack = new uint256[](0); + + int256[] memory initIntStack = new int256[](1); + initIntStack[0] = in1; + + (, , int256[] memory finalIntStack) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + require(finalIntStack.length >= 2, "Not enough outputs"); + out1 = finalIntStack[finalIntStack.length - 1]; + out2 = finalIntStack[finalIntStack.length - 2]; + } + + /** + * @notice Zero int input, returns entire final int stack + */ + function run0InNOut(bytes calldata code) + external + view + returns (int256[] memory) + { + uint256 fullDesc = _makeFullSublistDescriptor(code); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = fullDesc; + + uint256[] memory initCodeStack = new uint256[](0); + int256[] memory initIntStack = new int256[](0); + + (, , int256[] memory finalIntStack) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + return finalIntStack; + } +} diff --git a/onchain/src/Push3Interpreter.sol b/onchain/src/Push3Interpreter.sol new file mode 100644 index 0000000..3871fab --- /dev/null +++ b/onchain/src/Push3Interpreter.sol @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/console.sol"; + +/** + * @title Push3Interpreter + * @dev Demonstrates a minimal Push3-like interpreter that uses + * a token-based bytecode approach. + */ +contract Push3Interpreter { + // ----------------------------------------------------- + // 1. ENUMS + // ----------------------------------------------------- + enum CodeTag { + NO_TAG, // 0 + INSTRUCTION, // 1 + INT_LITERAL, // 2 + SUBLIST // 3 + } + + enum OpCode { + NOOP, // 0 + INTEGER_PLUS, // 1 + INTEGER_MINUS, // 2 + INTEGER_MULT, // 3 + INTEGER_DUP, // 4 + INTEGER_POP // 5 + } + + // ----------------------------------------------------- + // 2. DESCRIPTOR BIT LAYOUT + // ----------------------------------------------------- + // [255..248] : tag (8 bits) + // [247..216] : offset (32 bits) (for SUBLIST) + // [215..184] : length (32 bits) (for SUBLIST) + // [183..0] : leftover bits (opcode or literal data) + + function getTag(uint256 desc) internal pure returns (CodeTag) { + return CodeTag(uint8(desc >> 248)); + } + + function getOffset(uint256 desc) internal pure returns (uint32) { + return uint32(desc >> 216); + } + + function getLength(uint256 desc) internal pure returns (uint32) { + return uint32(desc >> 184); + } + + function getLow184(uint256 desc) internal pure returns (uint256) { + return desc & ((1 << 184) - 1); + } + + // Build a descriptor + function makeDescriptor( + CodeTag tag, + uint32 offset, + uint32 length, + uint256 low184 + ) + public + pure + returns (uint256) + { + require(low184 < (1 << 184), "low184 out of range"); + return + (uint256(uint8(tag)) << 248) | + (uint256(offset) << 216) | + (uint256(length) << 184) | + low184; + } + + function makeInstruction(OpCode op) internal pure returns (uint256) { + return makeDescriptor(CodeTag.INSTRUCTION, 0, 0, uint256(uint8(op))); + } + + function getOpCode(uint256 desc) internal pure returns (OpCode) { + return OpCode(uint8(desc & 0xFF)); + } + + function makeIntLiteral(int32 val) internal pure returns (uint256) { + // store 32-bit integer in leftover + uint256 masked = uint256(uint32(val)); + return makeDescriptor(CodeTag.INT_LITERAL, 0, 0, masked); + } + + function extractInt32(uint256 desc) internal pure returns (int32) { + // read the low 32 bits + return int32(uint32(getLow184(desc))); + } + + // ----------------------------------------------------- + // 3. HELPER READS + // ----------------------------------------------------- + + /** + * @dev Read 4 bytes from `code` at `start` as uint32. + */ + function readUint32(bytes calldata code, uint32 start) internal pure returns (uint32 val) { + require(start + 4 <= code.length, "readUint32 out of range"); + uint256 word; + assembly { + let buf := mload(0x40) // free memory + // copy exactly 4 bytes from code into buf + calldatacopy(buf, add(code.offset, start), 4) + word := mload(buf) + } + val = uint32(word >> 224); + } + + /** + * @dev Read 2 bytes from `code` at `start` as uint16. + */ + function readUint16(bytes calldata code, uint32 start) internal pure returns (uint16 val) { + require(start + 2 <= code.length, "readUint16 out of range"); + uint256 word; + assembly { + let buf := mload(0x40) + calldatacopy(buf, add(code.offset, start), 2) + word := mload(buf) + val := and(word, 0xffff) + } + // Not sure this is correct + val = uint16(word >> 240); + } + + // ----------------------------------------------------- + // 4. SUBLIST PARSING + // ----------------------------------------------------- + /** + * Token format: + * 0x00 => NOOP + * 0x01 => INTEGER_PLUS + * 0x02 => INT_LITERAL => read next 4 bytes => int32 + * 0x03 => SUBLIST => read next 2 bytes => subLen => parse that + * 0x04 => INTEGER_MINUS + */ + function parseSublist(bytes calldata code, uint32 off, uint32 len) + internal + pure + returns (uint256[] memory descs) + { + uint256[] memory temp = new uint256[](len); + uint256 count = 0; + uint32 endPos = off + len; + uint32 cur = off; + + while (cur < endPos) { + if (cur >= code.length) { + break; + } + + uint8 tokenType = uint8(code[cur]); + cur++; + + if (tokenType == 0x00) { + // NOOP + temp[count] = makeInstruction(OpCode.NOOP); + count++; + } + else if (tokenType == 0x01) { + // INTEGER_PLUS + temp[count] = makeInstruction(OpCode.INTEGER_PLUS); + count++; + } + else if (tokenType == 0x02) { + // INT_LITERAL => 4 bytes + if (cur + 4 <= endPos && (cur + 4) <= code.length) { + uint32 raw = readUint32(code, cur); + cur += 4; + int32 val = int32(raw); + temp[count] = makeIntLiteral(val); + count++; + } else { + break; + } + } + else if (tokenType == 0x03) { + // SUBLIST => read 2 bytes => length + if (cur + 2 <= endPos && (cur + 2) <= code.length) { + uint16 subLen = readUint16(code, cur); + cur += 2; + if (cur + subLen <= endPos && (cur + subLen) <= code.length) { + // create descriptor + temp[count] = makeDescriptor(CodeTag.SUBLIST, cur, subLen, 0); + count++; + // skip over that subLen chunk + cur += subLen; + } else { + break; + } + } else { + break; + } + } + else if (tokenType == 0x04) { + // INTEGER_PLUS + temp[count] = makeInstruction(OpCode.INTEGER_MINUS); + count++; + } + else if (tokenType == 0x05) { + // INTEGER_PLUS + temp[count] = makeInstruction(OpCode.INTEGER_MULT); + count++; + } + else if (tokenType == 0x06) { + temp[count] = makeInstruction(OpCode.INTEGER_DUP); + count++; + } + else if (tokenType == 0x07) { + temp[count] = makeInstruction(OpCode.INTEGER_POP); + count++; + } + else { + // unknown => NOOP + temp[count] = makeInstruction(OpCode.NOOP); + count++; + } + } + + descs = new uint256[](count); + for (uint256 i = 0; i < count; i++) { + descs[i] = temp[i]; + } + } + + // ----------------------------------------------------- + // 5. MAIN INTERPRETER + // ----------------------------------------------------- + function runInterpreter( + bytes calldata code, + uint256[] calldata initCodeStack, + uint256[] calldata initExecStack, + int256[] calldata initIntStack + ) + external + pure + returns ( + uint256[] memory finalCodeStack, + uint256[] memory finalExecStack, + int256[] memory finalIntStack + ) + { + // A) CODE STACK + uint256[] memory codeStack = new uint256[](initCodeStack.length + 256); + uint256 codeTop = initCodeStack.length; + for (uint256 i = 0; i < initCodeStack.length; i++) { + codeStack[i] = initCodeStack[i]; + } + + // B) EXEC STACK + uint256[] memory execStack = new uint256[](initExecStack.length + 256); + uint256 execTop = initExecStack.length; + for (uint256 e = 0; e < initExecStack.length; e++) { + execStack[e] = initExecStack[e]; + } + + // C) INT STACK + int256[] memory intStack = new int256[](initIntStack.length + 256); + uint256 intTop = initIntStack.length; + for (uint256 k = 0; k < initIntStack.length; k++) { + intStack[k] = initIntStack[k]; + } + + // D) MAIN LOOP + while (execTop > 0) { + uint256 topDesc = execStack[execTop - 1]; + execTop--; + + CodeTag tag = getTag(topDesc); + + if (tag == CodeTag.INSTRUCTION) { + OpCode op = getOpCode(topDesc); + if (op == OpCode.NOOP) { + // do nothing + } + else if (op == OpCode.INTEGER_PLUS) { + // pop top 2 => sum + if (intTop >= 2) { + int256 a = intStack[intTop - 1]; + int256 b = intStack[intTop - 2]; + intTop -= 2; + intStack[intTop] = b + a; + intTop++; + } + } + else if (op == OpCode.INTEGER_MINUS) { + // pop top 2 => minus + if (intTop >= 2) { + int256 a = intStack[intTop - 1]; + int256 b = intStack[intTop - 2]; + intTop -= 2; + intStack[intTop] = b - a; + intTop++; + } + } + else if (op == OpCode.INTEGER_MULT) { + // pop top 2 => minus + if (intTop >= 2) { + int256 a = intStack[intTop - 1]; + int256 b = intStack[intTop - 2]; + intTop -= 2; + intStack[intTop] = b * a; + intTop++; + } + } + else if (op == OpCode.INTEGER_DUP) { + if (intTop >= 1) { + int256 a = intStack[intTop - 1]; + intStack[intTop] = a; + intTop++; + } + } + else if (op == OpCode.INTEGER_POP) { + if (intTop >= 1) { + intTop -= 1; + } + } + } + else if (tag == CodeTag.INT_LITERAL) { + int32 val = extractInt32(topDesc); + intStack[intTop] = int256(val); + intTop++; + } + else if (tag == CodeTag.SUBLIST) { + // parse sublist => push in reverse + uint32 off = getOffset(topDesc); + uint32 len = getLength(topDesc); + + if (off + len <= code.length) { + uint256[] memory parsed = parseSublist(code, off, len); + for (uint256 p = 0; p < parsed.length; p++) { + execStack[execTop] = parsed[parsed.length - 1 - p]; + execTop++; + } + } + } + else { + // NO_TAG => do nothing + } + } + + // E) RETURN + finalCodeStack = new uint256[](codeTop); + for (uint256 i = 0; i < codeTop; i++) { + finalCodeStack[i] = codeStack[i]; + } + + finalExecStack = new uint256[](execTop); + for (uint256 i = 0; i < execTop; i++) { + finalExecStack[i] = execStack[i]; + } + + finalIntStack = new int256[](intTop); + for (uint256 i = 0; i < intTop; i++) { + finalIntStack[i] = intStack[i]; + } + } +} diff --git a/onchain/test/Push3IntAdapter.t.sol b/onchain/test/Push3IntAdapter.t.sol new file mode 100644 index 0000000..4ecd13c --- /dev/null +++ b/onchain/test/Push3IntAdapter.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Push3Interpreter} from "../src/Push3Interpreter.sol"; +import {Push3IntAdapter} from "../src/Push3IntAdapter.sol"; + +/** + * @title Push3IntAdapterTest + * @dev Foundry test for the Push3IntAdapter contract, + * which composes with the Push3Interpreter. + */ +contract Push3IntAdapterTest is Test { + Push3Interpreter public interpreter; + Push3IntAdapter public adapter; + + function setUp() public { + // 1) Deploy the interpreter + interpreter = new Push3Interpreter(); + + // 2) Deploy the adapter, referencing the interpreter address + adapter = new Push3IntAdapter(address(interpreter)); + } + + /** + * @notice Test run1In1Out with a minimal code array. + * For example, we might do a program that just leaves its input unchanged + * (like a NOOP). + */ + function test_run1In1Out_noop() public view { + bytes memory code = hex"03000100"; + + int256 inputVal = 123; + int256 result = adapter.run1In1Out(code, inputVal); + + // We expect the code does nothing => output == input + assertEq(result, inputVal, "Expected same value out"); + } + + /** + * @notice Test run2In1Out. We'll push 5, 7, do an op that leaves 12. + */ + function test_run2In1Out_plus() public view { + bytes memory code = hex"03000101"; + + int256 in1 = 5; + int256 in2 = 7; + int256 result = adapter.run2In1Out(code, in1, in2); + + assertEq(result, 12, "Expected 5 + 7 = 12"); + } + + /** + * @notice Test run1In2Out. Push a constant to a stack with 1. + * For now, we do a placeholder code. + */ + function test_run1In2Out() public view { + bytes memory code = hex"0300050200000003"; + + int256 inVal = 42; + (int256 top, int256 secondTop) = adapter.run1In2Out(code, inVal); + + assertEq(top, 3, "top stack val mismatch"); + assertEq(secondTop, 42, "second top stack val mismatch"); + } + + /** + * @notice Test run0InNOut. + * We'll feed a code that pushes some constants, and see the final stack. + */ + function test_run0InNOut() public view { + + bytes memory code = hex"03000A02000000030200000005"; + + int256[] memory stackResult = adapter.run0InNOut(code); + + assertEq(stackResult.length, 2, "Expected 2 final stack items"); + assertEq(stackResult[0], 3, "Item 0 mismatch"); + assertEq(stackResult[1], 5, "Item 1 mismatch"); + } +} diff --git a/onchain/test/Push3Interpreter.t.sol b/onchain/test/Push3Interpreter.t.sol new file mode 100644 index 0000000..8ef693f --- /dev/null +++ b/onchain/test/Push3Interpreter.t.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {Push3Interpreter} from "../src/Push3Interpreter.sol"; + +/** + * @title Push3InterpreterTest + * @dev Foundry test contract for the updated Push3Interpreter + */ +contract Push3InterpreterTest is Test { + Push3Interpreter public interpreter; + + function setUp() public { + interpreter = new Push3Interpreter(); + } + + /** + * @notice Test a small program that: + * 1) Pushes literal 10 (int32) + * 2) Pushes literal 32 (int32) + * 3) Calls INTEGER_PLUS + * + * We'll store everything in a single SUBLIST: + * 0x03 => SUBLIST + * 0x00 0x0B => subLen=11 + * Then the next 11 bytes are: + * 0x02 + (00 00 00 0A) => INT_LITERAL(10) + * 0x02 + (00 00 00 20) => INT_LITERAL(32) + * 0x01 => INTEGER_PLUS + * + * So total = 1 + 2 + (1+4 + 1+4 + 1) = 14 bytes + * => 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01 + */ + function test_LiteralsAndPlus() public view { + // The final code array (14 bytes): + // [0] = 0x03 + // [1..2] = 0x000B + // [3] = 0x02 + // [4..7] = 0x0000000A => 10 + // [8] = 0x02 + // [9..12] = 0x00000020 => 32 + // [13] = 0x01 => plus + + bytes memory code = hex"03000B020000000A020000002001"; + + // Or fully concatenated: + // 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01 + + // We'll parse offset=0, length=14 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 14, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + + // Run it + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // Should produce finalIntStack = [42] + assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); + assertEq(finalIntStack[0], 42, "Should be 10+32=42"); + } + + /** + * @notice Test pushing literal 10, literal 3, then INTEGER_MINUS => (10 - 3) = 7 + * + * The plan: + * 0x03 => SUBLIST token + * 0x00 0x0B => subLen=11 + * + * Then 11 bytes: + * 0x02 + (00 00 00 0A) => INT_LITERAL(10) + * 0x02 + (00 00 00 03) => INT_LITERAL(3) + * 0x04 => INTEGER_MINUS (assuming 0x04 is assigned to minus) + */ + function test_Minus() public view { + // We'll produce 14 total bytes, same structure as the plus example + // Format: [0x03, 0x000B, 0x02(10), 0x03(3), 0x04 => minus] + + bytes memory code = hex"03000B020000000A020000000304"; + // Breaking it down: + // 0x03 => SUBLIST + // 0x00 0x0B => length=11 + // 0x02 0000000A => INT_LITERAL(10) + // 0x02 00000003 => INT_LITERAL(3) + // 0x04 => INTEGER_MINUS + + // We'll parse offset=0, length=14 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 14, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + + // Run + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // Check final result => should be [7] + assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); + assertEq(finalIntStack[0], 7, "Should be 10 - 3 = 7"); + } + + /** + * @notice Test pushing literal 10, literal 3, then INTEGER_MULT => (10 * 3) = 30 + * + * The plan: + * 0x03 => SUBLIST token + * 0x00 0x0B => subLen=11 + * + * Then 11 bytes: + * 0x02 + (00 00 00 0A) => INT_LITERAL(10) + * 0x02 + (00 00 00 03) => INT_LITERAL(3) + * 0x05 => INTEGER_MULT + */ + function test_Mult() public view { + // We'll produce 14 total bytes, same structure as the plus example + // Format: [0x03, 0x000B, 0x02(10), 0x03(3), 0x05 => mul] + + bytes memory code = hex"03000B020000000A020000000305"; + // Breaking it down: + // 0x03 => SUBLIST + // 0x00 0x0B => length=11 + // 0x02 0000000A => INT_LITERAL(10) + // 0x02 00000003 => INT_LITERAL(3) + // 0x05 => INTEGER_MUL + + // We'll parse offset=0, length=14 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 14, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + + // Run + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // Check final result => should be [30] + assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); + assertEq(finalIntStack[0], 30, "Should be 10 * 3 = 30"); + } + + function test_Dup() public view { + + bytes memory code = hex"030006020000000A06"; + + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, + 9, + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + + // Run + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + assertEq(finalIntStack.length, 2, "finalIntStack should have length 1"); + assertEq(finalIntStack[0], finalIntStack[1], "The top 2 values on the stack should be equal."); + } + + function test_Pop() public view { + + bytes memory code = hex"030006020000000A07"; + + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, + 9, + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + + // Run + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + assertEq(finalIntStack.length, 0, "finalIntStack should be empty"); + } + + /** + * @notice Test with an unknown token (0xF0). + * We expect the parser to treat it as "unknown => NOOP" + * and produce a NOOP instruction descriptor. + * We'll confirm finalIntStack remains empty (or unchanged). + */ + function test_UnknownToken() public view { + // We'll create a 5-byte code: + // 0x03 => SUBLIST + // 0x00 0x01 => length=1 + // 0xF0 => unknown token + bytes memory code = hex"030001F0"; + + // We'll parse offset=0, length=4 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 4, // length + 0 + ); + + // init stacks + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + int256[] memory initIntStack = new int256[](0); + + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // finalIntStack should remain empty => no literal was pushed + assertEq(finalIntStack.length, 0, "Expected finalIntStack to remain empty with unknown token => NOOP"); + } + + /** + * @notice Test a partial literal: we claim to have 4 bytes, but the code array ends early. + * This should trigger the `else { break; }` branch in parseSublist + * when trying to read 4 bytes for the INT_LITERAL. + */ + function test_OutOfRangeLiteral() public view { + // 0x03 => SUBLIST + // 0x00 0x04 => length=4 + // 0x02 => INT_LITERAL token + // Then we have only 1 byte left instead of 4 => insufficient + // + // total = 1 + 2 + 1 + 1 = 5 bytes + // The parser tries to read 4 bytes after seeing 0x04, fails, hits break. + bytes memory code = hex"03000402FF"; + + // parse offset=0, length=5 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, + 5, + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + int256[] memory initIntStack = new int256[](0); + + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // Because the parse breaks early, we expect finalIntStack is empty or unchanged + assertEq(finalIntStack.length, 0, "Expected parse to break on out-of-range literal => no ints pushed"); + } + + /** + * @notice Test "not enough int args" scenario: + * We'll push only 1 literal, then do INTEGER_PLUS which needs 2 integers on the stack. + * In the main loop, it sees intTop < 2 => it does nothing. + */ + function test_NotEnoughIntArgs() public view { + bytes memory code = hex"030006020000000305"; + // Breaking it down: + // 0x03 => SUBLIST + // 0x00 0x06 => length=6 + // 0x02 00000003 => INT_LITERAL(3) + // 0x05 => INTEGER_MUL + + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, + 9, + 0 + ); + + // set up stacks + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + int256[] memory initIntStack = new int256[](0); + + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // We expect finalIntStack => [5], + // because the PLUS instruction sees only 1 int => does nothing => 5 remains + assertEq(finalIntStack.length, 1, "Should have 1 int left"); + assertEq(finalIntStack[0], 3, "Expected stack top=3 after insufficient-args plus"); + } + + /** + * @notice Test sublist length overflow: + * subLen claims bigger than the code array. Should trigger else { break; } + * in the sublist parsing for that sublist. + */ + function test_SublistLengthOverflow() public view { + + bytes memory code = hex"050010FFFFFF"; + + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, + 6, + 0 + ); + + // init + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + int256[] memory initIntStack = new int256[](0); + + ( + , + , + int256[] memory finalIntStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + + // Expect no items pushed => finalIntStack empty + assertEq(finalIntStack.length, 0, "Expected empty stack if sublist length is out of range => parse break"); + } +}