diff --git a/.gitignore b/.gitignore index b79784c..f14a111 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ onchain/out/ offchain/target/ -.direnv \ No newline at end of file +.direnv +.env diff --git a/onchain/src/Push3IntAdapter.sol b/onchain/src/Push3IntAdapter.sol index 40f4c39..5929de7 100644 --- a/onchain/src/Push3IntAdapter.sol +++ b/onchain/src/Push3IntAdapter.sol @@ -28,9 +28,9 @@ contract Push3IntAdapter { view returns (uint256) { - // CodeTag.SUBLIST is typically enum = 3, but - // let's fetch it via - // (uint8)Push3Interpreter.CodeTag.SUBLIST + // CodeTag.SUBLIST is typically enum = 4, 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: @@ -69,9 +69,12 @@ contract Push3IntAdapter { int256[] memory initIntStack = new int256[](1); initIntStack[0] = in1; + // No bool stack + bool[] memory initBoolStack = new bool[](0); + // 4) Run - (, , int256[] memory finalIntStack) = - interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + (, , int256[] memory finalIntStack,) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // 5) Return top of final int stack require(finalIntStack.length > 0, "No integer output produced"); @@ -96,11 +99,13 @@ contract Push3IntAdapter { initIntStack[0] = in1; initIntStack[1] = in2; - (, , int256[] memory finalIntStack) = - interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + bool[] memory initBoolStack = new bool[](0); + + (, , int256[] memory finalIntStack,) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); require(finalIntStack.length > 0, "No integer output"); - + out1 = finalIntStack[finalIntStack.length - 1]; } @@ -121,8 +126,10 @@ contract Push3IntAdapter { int256[] memory initIntStack = new int256[](1); initIntStack[0] = in1; - (, , int256[] memory finalIntStack) = - interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + bool[] memory initBoolStack = new bool[](0); + + (, , int256[] memory finalIntStack,) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); require(finalIntStack.length >= 2, "Not enough outputs"); out1 = finalIntStack[finalIntStack.length - 1]; @@ -130,7 +137,7 @@ contract Push3IntAdapter { } /** - * @notice Zero int input, returns entire final int stack + * @notice Zero int input, returns entire final int stack */ function run0InNOut(bytes calldata code) external @@ -143,9 +150,10 @@ contract Push3IntAdapter { uint256[] memory initCodeStack = new uint256[](0); int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); - (, , int256[] memory finalIntStack) = - interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + (, , int256[] memory finalIntStack,) = + interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); return finalIntStack; } diff --git a/onchain/src/Push3Interpreter.sol b/onchain/src/Push3Interpreter.sol index 3871fab..61d5644 100644 --- a/onchain/src/Push3Interpreter.sol +++ b/onchain/src/Push3Interpreter.sol @@ -9,6 +9,15 @@ import "forge-std/console.sol"; * a token-based bytecode approach. */ contract Push3Interpreter { + // ReadUint`numBits` out of the code range. + error ReadUintOutOfRange(uint256 numBits); + // ----------------------------------------------------- + // 0. CONSTANTS + // ----------------------------------------------------- + uint8 internal constant OPCODE_INTEGER_OFFSET = uint8(OpCode.INTEGER_PLUS); + uint8 internal constant OPCODE_BOOL_OFFSET = uint8(OpCode.BOOL_DUP); + uint8 internal constant OPCODE_LAST = uint8(OpCode.BOOL_RAND); + // ----------------------------------------------------- // 1. ENUMS // ----------------------------------------------------- @@ -16,16 +25,38 @@ contract Push3Interpreter { NO_TAG, // 0 INSTRUCTION, // 1 INT_LITERAL, // 2 - SUBLIST // 3 + BOOL_LITERAL, // 3 + SUBLIST // 4 } enum OpCode { - NOOP, // 0 - INTEGER_PLUS, // 1 - INTEGER_MINUS, // 2 - INTEGER_MULT, // 3 - INTEGER_DUP, // 4 - INTEGER_POP // 5 + NOOP, // 0 + NOOP_1, // 1 - empty for INSTRUCTION + NOOP_2, // 2 - empty for INT_LITERAL + NOOP_3, // 3 - empty for BOOL_LITERAL + NOOP_4, // 4 - empty for SUBLIST + INTEGER_PLUS, // 5 - OPCODE_INTEGER_OFFSET + INTEGER_MINUS, // OPCODE_INTEGER_OFFSET + 1 + INTEGER_MULT, // OPCODE_INTEGER_OFFSET + 2 + INTEGER_DUP, // OPCODE_INTEGER_OFFSET + 3 + INTEGER_POP, // OPCODE_INTEGER_OFFSET + 4 + BOOL_DUP, // 10 - OPCODE_BOOL_OFFSET + BOOL_POP, // OPCODE_BOOL_OFFSET + 1 + BOOL_SWAP, // OPCODE_BOOL_OFFSET + 2 + BOOL_FLUSH, // OPCODE_BOOL_OFFSET + 3 + BOOL_STACKDEPTH, // OPCODE_BOOL_OFFSET + 4 + BOOL_NOT, // OPCODE_BOOL_OFFSET + 5 + BOOL_AND, // OPCODE_BOOL_OFFSET + 6 + BOOL_OR, // OPCODE_BOOL_OFFSET + 7 + BOOL_EQ, // OPCODE_BOOL_OFFSET + 8 + BOOL_FROMFLOAT, // OPCODE_BOOL_OFFSET + 9 + BOOL_FROMINTEGER,// OPCODE_BOOL_OFFSET + 10 + BOOL_ROT, // OPCODE_BOOL_OFFSET + 11 + BOOL_SHOVE, // OPCODE_BOOL_OFFSET + 12 + BOOL_YANK, // OPCODE_BOOL_OFFSET + 13 + BOOL_YANKDUP, // OPCODE_BOOL_OFFSET + 14 + BOOL_DEFINE, // OPCODE_BOOL_OFFSET + 15 + BOOL_RAND // OPCODE_BOOL_OFFSET + 16 } // ----------------------------------------------------- @@ -85,56 +116,81 @@ contract Push3Interpreter { return makeDescriptor(CodeTag.INT_LITERAL, 0, 0, masked); } + function makeBoolLiteral(bool val) internal pure returns (uint256) { + uint256 masked; + if (val) masked = uint256(1); + return makeDescriptor(CodeTag.BOOL_LITERAL, 0, 0, masked); + } + function extractInt32(uint256 desc) internal pure returns (int32) { // read the low 32 bits return int32(uint32(getLow184(desc))); } + function extractBool(uint256 desc) internal pure returns (bool) { + return desc & 1 == 1; + } + // ----------------------------------------------------- // 3. HELPER READS // ----------------------------------------------------- + /** + * @dev Read `x` bytes from `code` at `start` as uint. + */ + function readUint(bytes calldata code, uint32 start, uint256 numBytes) internal pure returns (uint256 word) { + uint256 numBits = numBytes * 8; + if (start + numBytes > code.length) revert ReadUintOutOfRange(numBits); + assembly { + let buf := mload(0x40) // free memory + // copy exactly x bytes from code into buf + calldatacopy(buf, add(code.offset, start), numBytes) + word := mload(buf) + } + word >>= 256 - numBits; + } + /** * @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); + val = uint32(readUint(code, start, 4)); } /** * @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); + val = uint16(readUint(code, start, 2)); + } + + /** + * @dev Read 1 byte from `code` at `start` as uint8. + */ + function readUint8(bytes calldata code, uint32 start) internal pure returns (uint8 val) { + val = uint8(readUint(code, start, 1)); + } + + /** + * @dev Read 1 byte from `code` at `start` as bool. + */ + function readBool(bytes calldata code, uint32 start) internal pure returns (bool val) { + val = readUint(code, start, 1) & 1 == 1; } // ----------------------------------------------------- // 4. SUBLIST PARSING // ----------------------------------------------------- /** - * Token format: - * 0x00 => NOOP - * 0x01 => INTEGER_PLUS + * Token format is combination of ordered CodeTag and OpCode: + * 0x00 => NOOP/NO_TAG + * 0x01 => INSTRUCTION => read next 1 byte => OpCode * 0x02 => INT_LITERAL => read next 4 bytes => int32 - * 0x03 => SUBLIST => read next 2 bytes => subLen => parse that - * 0x04 => INTEGER_MINUS + * 0x03 => BOOL_LITERAL => read next 1 byte => bool + * 0x04 => SUBLIST => read next 2 bytes => subLen => parse that + * 0x05 => INTEGER_PLUS + * 0x06 => INTEGER_MINUS + * ... opcode list with type offsets */ function parseSublist(bytes calldata code, uint32 off, uint32 len) internal @@ -160,9 +216,15 @@ contract Push3Interpreter { count++; } else if (tokenType == 0x01) { - // INTEGER_PLUS - temp[count] = makeInstruction(OpCode.INTEGER_PLUS); - count++; + // INSTRUCTION + if (cur + 1 <= endPos && (cur + 1) <= code.length) { + uint8 opcode = readUint8(code,cur); + if (opcode < OPCODE_INTEGER_OFFSET || opcode > OPCODE_LAST) opcode = uint8(0); // NOOP + temp[count] = makeInstruction(OpCode(opcode)); + count++; + } else { + break; + } } else if (tokenType == 0x02) { // INT_LITERAL => 4 bytes @@ -177,6 +239,17 @@ contract Push3Interpreter { } } else if (tokenType == 0x03) { + // BOOL_LITERAL + if (cur + 1 <= endPos && (cur + 1) <= code.length) { + bool val = readBool(code, cur); + cur += 1; + temp[count] = makeBoolLiteral(val); + count++; + } else { + break; + } + } + else if (tokenType == 0x04) { // SUBLIST => read 2 bytes => length if (cur + 2 <= endPos && (cur + 2) <= code.length) { uint16 subLen = readUint16(code, cur); @@ -194,22 +267,9 @@ contract Push3Interpreter { 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); + else if (tokenType <= OPCODE_LAST) { + // OPCODE + temp[count] = makeInstruction(OpCode(tokenType)); count++; } else { @@ -229,17 +289,19 @@ contract Push3Interpreter { // 5. MAIN INTERPRETER // ----------------------------------------------------- function runInterpreter( - bytes calldata code, - uint256[] calldata initCodeStack, - uint256[] calldata initExecStack, - int256[] calldata initIntStack + bytes calldata code, // non-empty - genetic agent (DNA) + uint256[] calldata initCodeStack, // empty + uint256[] calldata initExecStack, // descriptors + int256[] calldata initIntStack, // 32bit word + bool[] calldata initBoolStack // bool array ) external - pure + view returns ( uint256[] memory finalCodeStack, uint256[] memory finalExecStack, - int256[] memory finalIntStack + int256[] memory finalIntStack, + bool[] memory finalBoolStack ) { // A) CODE STACK @@ -263,6 +325,13 @@ contract Push3Interpreter { intStack[k] = initIntStack[k]; } + // D) BOOL STACK + bool[] memory boolStack = new bool[](initBoolStack.length + 256); + uint256 boolTop = initBoolStack.length; + for (uint256 k = 0; k < initBoolStack.length; k++) { + boolStack[k] = initBoolStack[k]; + } + // D) MAIN LOOP while (execTop > 0) { uint256 topDesc = execStack[execTop - 1]; @@ -272,49 +341,172 @@ contract Push3Interpreter { if (tag == CodeTag.INSTRUCTION) { OpCode op = getOpCode(topDesc); - if (op == OpCode.NOOP) { - // do nothing + if (uint8(op) < OPCODE_INTEGER_OFFSET) { + // 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 (uint8(op) < OPCODE_BOOL_OFFSET) { + // INTEGER OPCODES + 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 (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; + else if (uint8(op) <= OPCODE_LAST) { + // BOOL OPCODES + if (op == OpCode.BOOL_DUP) { + // dup top + if (boolTop > 0) { + boolStack[boolTop] = boolStack[boolTop - 1]; + boolTop++; + } + } + else if (op == OpCode.BOOL_POP) { + // pop top + if (boolTop > 0) { + boolTop--; + } + } + else if (op == OpCode.BOOL_SWAP) { + // swap top 2 + if (boolTop > 1) { + (boolStack[boolTop - 1], boolStack[boolTop - 2]) = (boolStack[boolTop - 2], boolStack[boolTop - 1]); + } + } + else if (op == OpCode.BOOL_FLUSH) { + // empty stack + boolTop = 0; + } + else if (op == OpCode.BOOL_STACKDEPTH) { + // push bool depth onto int stack + intStack[intTop] = int256(boolTop); 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.BOOL_NOT) { + // push NOT of top + if (boolTop > 0) { + boolStack[boolTop - 1] = !boolStack[boolTop - 1]; + } } - } - else if (op == OpCode.INTEGER_DUP) { - if (intTop >= 1) { - int256 a = intStack[intTop - 1]; - intStack[intTop] = a; - intTop++; + else if (op == OpCode.BOOL_AND) { + // push AND of top 2 + if (boolTop > 1) { + boolStack[boolTop - 2] = boolStack[boolTop - 1] && boolStack[boolTop - 2]; + boolTop--; + } } - } - else if (op == OpCode.INTEGER_POP) { - if (intTop >= 1) { - intTop -= 1; + else if (op == OpCode.BOOL_OR) { + // push OR of top 2 + if (boolTop > 1) { + boolStack[boolTop - 2] = boolStack[boolTop - 1] || boolStack[boolTop - 2]; + boolTop--; + } + } + else if (op == OpCode.BOOL_EQ) { + // push True if top 2 equal + if (boolTop > 1) { + boolStack[boolTop - 2] = boolStack[boolTop - 1] == boolStack[boolTop - 2]; + boolTop--; + } + } + else if (op == OpCode.BOOL_FROMFLOAT) { + // FLOAT is not implemented yet + } + else if (op == OpCode.BOOL_FROMINTEGER) { + // push True if top int not eq 0 + if (intTop > 0) { + boolStack[boolTop] = intStack[intTop - 1] != int256(0); + boolTop++; + intTop--; + } + } + else if (op == OpCode.BOOL_ROT) { + // rotate [top - 3, top - 2, top - 1] into -> [top - 2, top - 1, top - 3] + if (boolTop > 2) { + (boolStack[boolTop - 1], boolStack[boolTop - 2], boolStack[boolTop - 3]) = (boolStack[boolTop - 3], boolStack[boolTop - 1], boolStack[boolTop - 2]); + } + } + else if (op == OpCode.BOOL_SHOVE) { + // Task: Inserts the top BOOLEAN "deep" in the stack, at the position indexed by the top INTEGER. + // Questions: what does "inserts" mean? does it mean that we pop top value and then insert it + // at index? does it mean that array size does not change? + // what happens with the previous element at this index? + // is it correct that this element stays in array? + // should it be moved in top or bottom direction from index (index + || -)? + // does it mean that we need to reindex the whole stack? + // is the simplest solution to reindex to iterate through the whole stack? + // do we really want have such loops? + } + else if (op == OpCode.BOOL_YANK) { + // Task: Removes an indexed item from "deep" in the stack and pushes it on top of the stack. The index is taken from the INTEGER stack. + // Same question as for SHOVE is valid here. + // For example, in our implementation currently we are not really poping + // element from an array in POP, we only adjusting the counter. + // For removing index we would need to loop over the array to reindex. + } + else if (op == OpCode.BOOL_YANKDUP) { + // Task: Pushes a copy of an indexed item "deep" in the stack onto the top of the stack, without removing the deep item. The index is taken from the INTEGER stack. + uint256 index = uint256(intStack[intTop - 1]); + if (boolTop > 2) { + // pop int stack + intTop--; + // index in range (reverse array indexes) + index++; // adjust as boolTop is length, highest index is boolTop - 1 + if (index > boolTop) { + index = 0; + } else { + index = boolTop - index; + } + // push value on bool stack + boolStack[boolTop] = boolStack[index]; + boolTop++; + } + } + else if (op == OpCode.BOOL_DEFINE) { + // NAME is not implemented yet + } + else if (op == OpCode.BOOL_RAND) { + // Pushes a random BOOLEAN. + uint256 randomNum = uint256(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, msg.sender))); + boolStack[boolTop] = extractBool(randomNum); + boolTop++; } } } @@ -323,6 +515,11 @@ contract Push3Interpreter { intStack[intTop] = int256(val); intTop++; } + else if (tag == CodeTag.BOOL_LITERAL) { + bool val = extractBool(topDesc); + boolStack[boolTop] = val; + boolTop++; + } else if (tag == CodeTag.SUBLIST) { // parse sublist => push in reverse uint32 off = getOffset(topDesc); @@ -330,8 +527,8 @@ contract Push3Interpreter { 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]; + for (uint256 p = 0; p < parsed.length; p++) { // index 0 1 2 + execStack[execTop] = parsed[parsed.length - 1 - p]; // [0x01, 0x20, 0x0a] execTop++; } } @@ -356,5 +553,10 @@ contract Push3Interpreter { for (uint256 i = 0; i < intTop; i++) { finalIntStack[i] = intStack[i]; } + + finalBoolStack = new bool[](boolTop); + for (uint256 i = 0; i < boolTop; i++) { + finalBoolStack[i] = boolStack[i]; + } } } diff --git a/onchain/test/Push3IntAdapter.t.sol b/onchain/test/Push3IntAdapter.t.sol index 4ecd13c..664e008 100644 --- a/onchain/test/Push3IntAdapter.t.sol +++ b/onchain/test/Push3IntAdapter.t.sol @@ -23,12 +23,12 @@ contract Push3IntAdapterTest is Test { } /** - * @notice Test run1In1Out with a minimal code array. - * For example, we might do a program that just leaves its input unchanged - * (like a NOOP). + * @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"; + bytes memory code = hex"04000100"; int256 inputVal = 123; int256 result = adapter.run1In1Out(code, inputVal); @@ -41,21 +41,21 @@ contract Push3IntAdapterTest is Test { * @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"; + bytes memory code = hex"04000105"; 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. + * For now, we do a placeholder code. */ function test_run1In2Out() public view { - bytes memory code = hex"0300050200000003"; + bytes memory code = hex"0400050200000003"; int256 inVal = 42; (int256 top, int256 secondTop) = adapter.run1In2Out(code, inVal); @@ -65,12 +65,12 @@ contract Push3IntAdapterTest is Test { } /** - * @notice Test run0InNOut. + * @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"; + + bytes memory code = hex"04000A02000000030200000005"; int256[] memory stackResult = adapter.run0InNOut(code); diff --git a/onchain/test/Push3Interpreter.t.sol b/onchain/test/Push3Interpreter.t.sol index c18526b..7caa2ef 100644 --- a/onchain/test/Push3Interpreter.t.sol +++ b/onchain/test/Push3Interpreter.t.sol @@ -23,30 +23,30 @@ contract Push3InterpreterTest is Test { * 3) Calls INTEGER_PLUS * * We'll store everything in a single SUBLIST: - * 0x03 => SUBLIST + * 0x04 => 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 + * 0x05 => 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 + * => 0x04 00 0B 02 00 00 00 0A 02 00 00 00 20 05 */ function test_LiteralsAndPlus() public view { // The final code array (14 bytes): - // [0] = 0x03 + // [0] = 0x04 // [1..2] = 0x000B // [3] = 0x02 // [4..7] = 0x0000000A => 10 // [8] = 0x02 // [9..12] = 0x00000020 => 32 - // [13] = 0x01 => plus + // [13] = 0x05 => plus - bytes memory code = hex"03000B020000000A020000002001"; + bytes memory code = hex"04000B020000000A020000002005"; // Or fully concatenated: - // 0x03 00 0B 02 00 00 00 0A 02 00 00 00 20 01 + // 0x04 00 0B 02 00 00 00 0A 02 00 00 00 20 05 // We'll parse offset=0, length=14 uint256 sublistDesc = interpreter.makeDescriptor( @@ -61,13 +61,15 @@ contract Push3InterpreterTest is Test { initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); // Run it ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // Should produce finalIntStack = [42] assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); @@ -78,25 +80,25 @@ contract Push3InterpreterTest is Test { * @notice Test pushing literal 10, literal 3, then INTEGER_MINUS => (10 - 3) = 7 * * The plan: - * 0x03 => SUBLIST token + * 0x04 => 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) + * 0x06 => INTEGER_MINUS (assuming 0x06 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] + // Format: [0x04, 0x000B, 0x02(10), 0x03(3), 0x06 => minus] - bytes memory code = hex"03000B020000000A020000000304"; + bytes memory code = hex"04000B020000000A020000000306"; // Breaking it down: - // 0x03 => SUBLIST + // 0x04 => SUBLIST // 0x00 0x0B => length=11 // 0x02 0000000A => INT_LITERAL(10) // 0x02 00000003 => INT_LITERAL(3) - // 0x04 => INTEGER_MINUS + // 0x06 => INTEGER_MINUS // We'll parse offset=0, length=14 uint256 sublistDesc = interpreter.makeDescriptor( @@ -111,13 +113,15 @@ contract Push3InterpreterTest is Test { initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); // Run ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // Check final result => should be [7] assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); @@ -128,25 +132,25 @@ contract Push3InterpreterTest is Test { * @notice Test pushing literal 10, literal 3, then INTEGER_MULT => (10 * 3) = 30 * * The plan: - * 0x03 => SUBLIST token + * 0x04 => 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 + * 0x07 => 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] + // Format: [0x04, 0x000B, 0x02(10), 0x02(3), 0x07 => mul] - bytes memory code = hex"03000B020000000A020000000305"; + bytes memory code = hex"04000B020000000A020000000307"; // Breaking it down: - // 0x03 => SUBLIST + // 0x04 => SUBLIST // 0x00 0x0B => length=11 // 0x02 0000000A => INT_LITERAL(10) // 0x02 00000003 => INT_LITERAL(3) - // 0x05 => INTEGER_MUL + // 0x07 => INTEGER_MUL // We'll parse offset=0, length=14 uint256 sublistDesc = interpreter.makeDescriptor( @@ -161,13 +165,15 @@ contract Push3InterpreterTest is Test { initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); // Run ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // Check final result => should be [30] assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); @@ -176,12 +182,12 @@ contract Push3InterpreterTest is Test { function test_Dup() public view { - bytes memory code = hex"030006020000000A06"; + bytes memory code = hex"040009020000000A0803010A"; uint256 sublistDesc = interpreter.makeDescriptor( Push3Interpreter.CodeTag.SUBLIST, - 0, - 9, + 0, + 12, 0 ); @@ -190,26 +196,30 @@ contract Push3InterpreterTest is Test { initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); // Run ( , , - int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + int256[] memory finalIntStack, + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); assertEq(finalIntStack.length, 2, "finalIntStack should have length 1"); - assertEq(finalIntStack[0], finalIntStack[1], "The top 2 values on the stack should be equal."); + assertEq(finalIntStack[0], finalIntStack[1], "The top 2 values on the int stack should be equal."); + assertEq(finalBoolStack.length, 2, "finalBoolStack should have length 2"); + assertEq(finalBoolStack[0], finalBoolStack[1], "The top 2 values on the bool stack should be equal."); } function test_Pop() public view { - bytes memory code = hex"030006020000000A07"; + bytes memory code = hex"040009020000000A0903010B"; uint256 sublistDesc = interpreter.makeDescriptor( Push3Interpreter.CodeTag.SUBLIST, - 0, - 9, + 0, + 12, 0 ); @@ -218,33 +228,36 @@ contract Push3InterpreterTest is Test { initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); // Run ( , , - int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + int256[] memory finalIntStack, + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); assertEq(finalIntStack.length, 0, "finalIntStack should be empty"); + assertEq(finalBoolStack.length, 0, "finalBoolStack should be empty"); } /** * @notice Test with an unknown token (0xF0). - * We expect the parser to treat it as "unknown => NOOP" + * 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 + // 0x04 => SUBLIST // 0x00 0x01 => length=1 // 0xF0 => unknown token - bytes memory code = hex"030001F0"; + bytes memory code = hex"040001F0"; // We'll parse offset=0, length=4 uint256 sublistDesc = interpreter.makeDescriptor( - Push3Interpreter.CodeTag.SUBLIST, + Push3Interpreter.CodeTag.SUBLIST, 0, // offset 4, // length 0 @@ -255,12 +268,14 @@ contract Push3InterpreterTest is Test { uint256[] memory initExecStack = new uint256[](1); initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // finalIntStack should remain empty => no literal was pushed assertEq(finalIntStack.length, 0, "Expected finalIntStack to remain empty with unknown token => NOOP"); @@ -268,18 +283,18 @@ contract Push3InterpreterTest is Test { /** * @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 + * 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 + // 0x04 => 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"; + bytes memory code = hex"04000402FF"; // parse offset=0, length=5 uint256 sublistDesc = interpreter.makeDescriptor( @@ -293,12 +308,14 @@ contract Push3InterpreterTest is Test { uint256[] memory initExecStack = new uint256[](1); initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // 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"); @@ -310,12 +327,12 @@ contract Push3InterpreterTest is Test { * In the main loop, it sees intTop < 2 => it does nothing. */ function test_NotEnoughIntArgs() public view { - bytes memory code = hex"030006020000000305"; + bytes memory code = hex"040006020000000307"; // Breaking it down: - // 0x03 => SUBLIST + // 0x04 => SUBLIST // 0x00 0x06 => length=6 // 0x02 00000003 => INT_LITERAL(3) - // 0x05 => INTEGER_MUL + // 0x07 => INTEGER_MUL uint256 sublistDesc = interpreter.makeDescriptor( Push3Interpreter.CodeTag.SUBLIST, @@ -329,14 +346,16 @@ contract Push3InterpreterTest is Test { uint256[] memory initExecStack = new uint256[](1); initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); - // We expect finalIntStack => [5], + // 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"); @@ -344,12 +363,12 @@ contract Push3InterpreterTest is Test { /** * @notice Test sublist length overflow: - * subLen claims bigger than the code array. Should trigger else { break; } + * 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"; + + bytes memory code = hex"050010FFFFFF"; uint256 sublistDesc = interpreter.makeDescriptor( Push3Interpreter.CodeTag.SUBLIST, @@ -363,14 +382,526 @@ contract Push3InterpreterTest is Test { uint256[] memory initExecStack = new uint256[](1); initExecStack[0] = sublistDesc; int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); ( , , int256[] memory finalIntStack - ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack); + , + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); // Expect no items pushed => finalIntStack empty assertEq(finalIntStack.length, 0, "Expected empty stack if sublist length is out of range => parse break"); } + + // BOOL STACK + + /** + * @notice Test pushing literal True, literal False, then BOOL_SWAP => [False, True] + * + * The plan: + * 0x04 => SUBLIST token + * 0x00 0x05 => subLen=5 + * + * Then 5 bytes: + * 0x03 + (01) => BOOL_LITERAL(True) + * 0x03 + (00) => BOOL_LITERAL(False) + * 0x0C => BOOL_SWAP + */ + function test_BoolSwap() public view { + // We'll produce 8 total bytes, same structure + // Format: [0x04, 0x0005, 0x03(01), 0x03(00), 0x0C => bool_swap] + + bytes memory code = hex"040005030103000C"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x05 => length=5 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x0C => BOOL_SWAP + + // We'll parse offset=0, length=8 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 8, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be [True, False] swapped into [False, True] + assertEq(finalBoolStack.length, 2, "finalBoolStack should have length 2"); + assertEq(finalBoolStack[0], false, "Should be False"); + assertEq(finalBoolStack[1], true, "Should be True"); + } + + /** + * @notice Test pushing literal True, literal False, then BOOL_FLUSH => [] + * Result should be empty stack. + */ + function test_BoolFlush() public view { + // We'll produce 8 total bytes, same structure + // Format: [0x04, 0x0005, 0x03(01), 0x03(00), 0x0D => bool_flush] + + bytes memory code = hex"040005030103000D"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x05 => length=5 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x0D => BOOL_FLUSH + + // We'll parse offset=0, length=8 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 8, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be empty bool stack + assertEq(finalBoolStack.length, 0, "finalBoolStack should have length 0"); + } + + /** + * @notice Test pushing literal True, literal False, then BOOL_STACKDEPTH => intStack[2] + * Result should be number of pushed literals as intStack top. + */ + function test_BoolStackDepth() public view { + // We'll produce 8 total bytes, same structure + // Format: [0x04, 0x0005, 0x03(01), 0x03(00), 0x0E => bool_stackdepth] + + bytes memory code = hex"040005030103000E"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x05 => length=5 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x0E => BOOL_STACKDEPTH + + // We'll parse offset=0, length=8 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 8, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + int256[] memory finalIntStack, + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be empty bool stack + assertEq(finalIntStack.length, 1, "finalIntStack should have length 1"); + assertEq(finalBoolStack.length, 2, "finalBoolStack should have length 2"); + assertEq(finalIntStack[0], 2, "IntStack top value should be 2, as we pushed 2 literals onto bool stack"); + } + + /** + * @notice Test pushing literal True, then BOOL_NOT => boolStack[False] + * Result should be False on top of bool stack. + */ + function test_BoolNot() public view { + // We'll produce 6 total bytes, same structure + // Format: [0x04, 0x0003, 0x03(01), 0x0F => bool_not] + + bytes memory code = hex"04000303010F"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x03 => length=3 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x0F => BOOL_NOT + + // We'll parse offset=0, length=6 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 6, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be False on top of bool stack + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], false, "Expected to be False after logical NOT on True"); + } + + /** + * @notice Test pushing literal True, literal True, then BOOL_AND => boolStack[True] + * Result should be True on top of bool stack. + */ + function test_BoolAnd() public view { + // We'll produce 8 total bytes, same structure + // Format: [0x04, 0x0005, 0x03(01), 0x03(01) 0x10 => bool_and] + + bytes memory code = hex"0400050301030110"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x05 => length=5 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x10 => BOOL_AND + + // We'll parse offset=0, length=8 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 8, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be True on top of bool stack + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], true, "Expected to be True after logical AND on True and True"); + } + + /** + * @notice Test pushing literal True, literal False, then BOOL_OR => boolStack[True] + * Result should be True on top of bool stack. + */ + function test_BoolOr() public view { + // We'll produce 8 total bytes, same structure + // Format: [0x04, 0x0005, 0x03(01), 0x03(00) 0x11 => bool_or] + + bytes memory code = hex"0400050301030011"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x05 => length=5 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x11 => BOOL_OR + + // We'll parse offset=0, length=8 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 8, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be True on top of bool stack + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], true, "Expected to be True after logical OR on True and False"); + } + + /** + * @notice Test pushing literal True, literal False, then BOOL_EQ => boolStack[False] + * Result should be False on top of bool stack. + */ + function test_BoolEq() public view { + // We'll produce 8 total bytes, same structure + // Format: [0x04, 0x0005, 0x03(01), 0x03(00) 0x12 => bool_eq] + + bytes memory code = hex"0400050301030012"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x05 => length=5 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x12 => BOOL_EQ + + // We'll parse offset=0, length=8 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 8, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be False on top of bool stack + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], false, "Expected to be False as True is not equal False"); + } + + /** + * @notice Test pushing literal 0x2145, then BOOL_FROMINTEGER => boolStack[True] + * Result should be True on top of bool stack. + */ + function test_BoolFromInteger() public view { + // We'll produce 9 total bytes, same structure + // Format: [0x04, 0x0004, 0x02(0x00002145), 0x14 => bool_frominteger] + + bytes memory code = hex"040006020000214514"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x04 => length=6 + // 0x02 0x00002145 => INT_LITERAL(0x2145) + // 0x14 => BOOL_FROMINTEGER + + // We'll parse offset=0, length=9 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 9, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + int256[] memory finalIntStack, + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be True on bool stack + assertEq(finalIntStack.length, 0, "finalIntStack should have length 0"); + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], true, "boolStack top value should be True, as we pushed non-zero literal onto int stack"); + } + + /** + * @notice Test pushing literal True, literal False, literal True, then BOOL_ROT => boolStack[False, True, True] + * Result should be rotated bool stack from [True, False, True] into [False, True, True]. + */ + function test_BoolRot() public view { + // We'll produce 10 total bytes, same structure + // Format: [0x04, 0x0007, 0x03(01), 0x03(00), 0x03(01), 0x15 => bool_rot] + + bytes memory code = hex"04000703010300030115"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x07 => length=7 + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x15 => BOOL_ROT + + // We'll parse offset=0, length=10 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 10, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be [False, True, True] bool stack + assertEq(finalBoolStack.length, 3, "finalBoolStack should have length 3"); + assertEq(finalBoolStack[0], false, "Expected to be False as value from middle position moved to bottom of the stack"); + assertEq(finalBoolStack[1], true, "Expected to be True as value from top position moved to middle of the stack"); + assertEq(finalBoolStack[2], true, "Expected to be True as value from bottom position moved to top of the stack"); + } + + /** + * @notice Test pushing literal 0x02, literal True, literal False, literal True, then BOOL_YANKDUP => boolStack[True, False, True, True] + * Result should be copied True on top of bool stack [True, False, True, True] + */ + function test_BoolYankDup() public view { + // We'll produce 15 total bytes, same structure + // Format: [0x04, 0x000C, 0x02(00 00 00 02) 0x03(01), 0x03(00), 0x03(01), 0x18 => bool_yankdup] + + bytes memory code = hex"04000C020000000203010300030118"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x0C => length=12 + // 0x02 0x00000002 => INT_LITERAL(0x02) + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x03 0x00 => BOOL_LITERAL(False) + // 0x03 0x01 => BOOL_LITERAL(True) + // 0x18 => BOOL_YANKDUP + + // We'll parse offset=0, length=15 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 15, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be [True, False, True, True] bool stack + assertEq(finalBoolStack.length, 4, "finalBoolStack should have length 4"); + assertEq(finalBoolStack[0], true, "Expected to be True as first value from bool literal"); + assertEq(finalBoolStack[1], false, "Expected to be False as second value from bool literal"); + assertEq(finalBoolStack[2], true, "Expected to be True as third value from bool literal"); + assertEq(finalBoolStack[3], true, "Expected to be True as copied value at index 2, which is index 0 for an array"); + } + + /** + * @notice Test pushing BOOL_RAND => boolStack[True] or boolStack[False] + * Result should be either False or True on top of bool stack. + */ + function test_BoolRand() public { + // We'll produce 4 total bytes, same structure + // Format: [0x04, 0x0001, 0x1A => bool_rand] + + bytes memory code = hex"0400011A"; + // Breaking it down: + // 0x04 => SUBLIST + // 0x00 0x01 => length=1 + // 0x1A => BOOL_RAND + + // We'll parse offset=0, length=4 + uint256 sublistDesc = interpreter.makeDescriptor( + Push3Interpreter.CodeTag.SUBLIST, + 0, // offset + 4, // length + 0 + ); + + uint256[] memory initCodeStack = new uint256[](0); + uint256[] memory initExecStack = new uint256[](1); + initExecStack[0] = sublistDesc; + + int256[] memory initIntStack = new int256[](0); + bool[] memory initBoolStack = new bool[](0); + + // Run + ( + , + , + , + bool[] memory finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + + // Check final result => should be random bool on top of bool stack + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], false, "Expected to be False for default prevrandao, timestamp and msg.sender"); + // move to another timestamp to get different random result + vm.warp(1736055577); + ( + , + , + , + finalBoolStack + ) = interpreter.runInterpreter(code, initCodeStack, initExecStack, initIntStack, initBoolStack); + assertEq(finalBoolStack.length, 1, "finalBoolStack should have length 1"); + assertEq(finalBoolStack[0], true, "Expected to be True for configured timestamp and default prevrandao, msg.sender"); + } }