diff --git a/script/Counter.s.sol b/script/Counter.s.sol index cdc1fe9..f12da74 100644 --- a/script/Counter.s.sol +++ b/script/Counter.s.sol @@ -1,19 +1,19 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +/* // SPDX-License-Identifier: UNLICENSED */ +/* pragma solidity ^0.8.13; */ -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; +/* import {Script, console} from "forge-std/Script.sol"; */ +/* import {Counter} from "../src/Counter.sol"; */ -contract CounterScript is Script { - Counter public counter; +/* contract CounterScript is Script { */ +/* Counter public counter; */ - function setUp() public {} +/* function setUp() public {} */ - function run() public { - vm.startBroadcast(); +/* function run() public { */ +/* vm.startBroadcast(); */ - counter = new Counter(); +/* counter = new Counter(); */ - vm.stopBroadcast(); - } -} +/* vm.stopBroadcast(); */ +/* } */ +/* } */ diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/Push3Interpreter.sol b/src/Push3Interpreter.sol new file mode 100644 index 0000000..6b7be32 --- /dev/null +++ b/src/Push3Interpreter.sol @@ -0,0 +1,303 @@ +// 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. We fix the assembly + * endianness issues by copying exact bytes for 2- and 4-byte reads. + */ +contract Push3Interpreter { + // ----------------------------------------------------- + // 1. ENUMS + // ----------------------------------------------------- + enum CodeTag { + NO_TAG, // 0 + INSTRUCTION, // 1 + INT_LITERAL, // 2 + SUBLIST // 3 + } + + enum OpCode { + NOOP, // 0 + INTEGER_PLUS // 1 + } + + // ----------------------------------------------------- + // 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"); + assembly { + let buf := mload(0x40) + calldatacopy(buf, add(code.offset, start), 2) + let word := mload(buf) + val := and(word, 0xffff) + } + } + + // ----------------------------------------------------- + // 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 + */ + 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 { + // 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 (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/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/Push3Interpreter.t.sol b/test/Push3Interpreter.t.sol new file mode 100644 index 0000000..4ec2683 --- /dev/null +++ b/test/Push3Interpreter.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.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 { + // 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 + ( + uint256[] memory finalCodeStack, + uint256[] memory finalExecStack, + 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"); + } +}