In our previous article, we introduced the Deck, a new gameplay mechanic in the CyberVoid universe that merges gaming and coding. In this article we’re going to take a closer look at the interpreter that makes it all possible: the RWInterpreter. This is still an ongoing coding and review. We are still missing some key operations.
The RWInterpreter is a smart contract that operates a stack-based bytecode language.
RW Overview
RWInterpreter Contract
The RWInterpreter contract is written in Solidity making it compatible with any EVM. Is heavily inspired by EVM itself.
Error Handling
The contract has several custom errors defined for robust error handling. These include ProgramOutOfBounds
, StackUnderflow
, DivisionByZero
, NotEnoughElements
, and InvalidOpcode
, which helps pinpoint issues in the program flow.
OPCodes
The contract defines several operation codes (OPCodes) that represent different operations in the bytecode language. There are stack operations, arithmetic operations, bit operations, and branching operations. Each operation is assigned a unique byte value, making it "easy" to read and write programs.
The run
Function
The run
function is the heart of the RWInterpreter. It takes a byte array representing a program as input, and it returns the top value of the stack when the program finishes executing. Inside the function, the program is iterated over, and each opcode is identified and executed
Stack Operations
Stack operations manipulate the stack's state. The OP_PUSH
operation pushes a value onto the stack, OP_POP
removes the top value from the stack, OP_DUP
duplicates the top value of the stack, and OP_SWP
swaps the two top values on the stack.
Arithmetic Operations
Arithmetic operations perform mathematical operations on the two top values of the stack. The operations include addition, subtraction, multiplication, division, and modulo.
Bit Operations
Bit operations perform bitwise operations on the stack's top values. These operations include bitwise AND, OR, XOR, and NOT.
Branching Operations
Branching operations control the flow of the program based on certain conditions. The OP_JMP
operation jumps to a specific address, OP_JZ
jumps if the top of the stack is zero, and OP_JNZ
jumps if the top of the stack is not zero.
Execution
The program iterates over each opcode and performs the appropriate action. If an invalid opcode is encountered, the contract reverts with an InvalidOpcode
error. If the OP_STOP
opcode is encountered, the execution breaks and the function returns the top value of the stack.
Examples of execution
Arithmetical
// compute (3+4)*2
function testCompleteProgram1() public {
bytes memory program = hex"0103010414010216";
uint256 topElement = interpreter.run(program);
assertEq(topElement, 14, "top element should be 14");
}
Bit operation
function testOpCodeOr_ThreeWords() public {
// PUSH 03 -> PUSH 02 -> PUSH 01 -> OR -> OR
uint256 topElement = interpreter.run(hex"0103010301012A2A");
assertEq(topElement, 3, "top element should be 3");
}
Branching Operation
function testOpCodeJMP_Forward() public {
// OP_CODE JMP 0x0D
uint256 topElement = interpreter.run(hex"0102");
assertEq(topElement, 2, "top element should be 2");
topElement = interpreter.run(hex"01020D0103");
assertEq(topElement, 2, "top element should be 2");
topElement = interpreter.run(hex"01020D01030104");
assertEq(topElement, 4, "top element should be 4");
}
Let's break this example down
This test is verifying the correct operation of the OP_JMP
opcode, which stands for "jump" in the RWInterpreter's bytecode language.
The OP_JMP
opcode is used to jump forward in the bytecode by a specified number of steps. This is done by adding the operand's value to the program counter (pc
). The operand in this case comes from the next byte in the program (right after the OP_JMP
opcode).
Step by Step
topElement =
interpreter.run
(hex"0102");
: This part is testing theOP_PUSH
opcode. It pushes the value 2 onto the stack (01
being theOP_PUSH
opcode and02
is the value to be pushed). TheassertEq
function checks if the top element of the stack is 2, which it should be as it's the value that was just pushed onto the stack.topElement =
interpreter.run
(hex"01020D0103");
: This part is testing theOP_JMP
opcode. It first pushes the value 2 onto the stack (with0102
), then it jumps forward one instruction (0D01
), skipping the nextOP_PUSH
instruction which would have pushed 3 onto the stack. TheassertEq
function checks if the top element of the stack is 2, which it should be becauseOP_JMP
operation didn't affect the stack and the push of the number 3 was skipped.topElement =
interpreter.run
(hex"01020D01030104");
: This part extends the previous test. After theOP_JMP
opcode, adds anotherOP_PUSH
opcode which pushes 4 onto the stack. SinceOP_JMP
operation jumped one step, it skipped the push of the number 3, and the top element should be 4 after execution, which is checked with theassertEq
function.
RWInterpreter Specification Alpha v0.1
Properties
Stack and Reverse Polish Notation (RPN): The RWInterpreter uses a stack data structure to process bytecode programs. The stack operates using Reverse Polish Notation (RPN).
The stack has 256 elements for v0.1.
Words are 8 bits long for v0.1.
OPCodes: The bytecode language used in the RWInterpreter defines a range of operation codes (OPCodes) that specify different operations that can be executed. These include:
Stack operations (8bits):
OP_PUSH (0x01)
: push value to stackOP_POP (0x02)
: pop value from stackOP_DUP (0x03)
: duplicate top of stackOP_SWP (0x04)
: swap top two values on stack
Arithmetic operations (8bits):
OP_ADD (0x14)
: add top two values on stackOP_SUB (0x15)
: subtract top two values on stackOP_MUL (0x16)
: multiply top two values on stackOP_DIV (0x17)
: divide top two values on stackOP_MOD (0x18)
: modulo top two values on stack
Bitwise operations (8bits):
OP_AND (0x29)
: bitwise and top two values on stackOP_OR (0x2A)
: bitwise or top two values on stackOP_XOR (0x2B)
: bitwise xor top two values on stackOP_NOT (0x2C)
: bitwise not top value on stack
Branching operations (8bits):
OP_JMP (0x0D)
: jump to addressOP_JZ (0x0E)
: jump to address if top of stack is zeroOP_JNZ (0x0F)
: jump to address if top of stack is not zero
Stop operation:
OP_STOP (0xFF)
: stop execution
Each opcode has a unique byte value and programs are written and interpreted as byte arrays.
The opcode values are spaced out in hexadecimal values, allowing for easy expansion of the interpreter. The gaps between opcode ranges (0x01-0x04, 0x14-0x18, 0x29-0x2C, 0x0D-0x0F, and 0xFF
) leave room for future additions in each category, while keeping related opcodes grouped.
Error Handling: The RWInterpreter defines several custom error types for managing exceptions that occur during program execution:
ProgramOutOfBounds
StackUnderflow
DivisionByZero
NotEnoughElements
InvalidOpcode
These errors help identify and debug issues while making the interpreter safer and more reliable.
Functionalities
run
: The core of the RWInterpreter is the run
function. This function accepts a byte array, representing a program, as an argument and returns the top value of the stack after the program has finished execution.
The run
function carries out several operations:
It sets up a memory stack of 256 elements, Stack Pointer and Program Counter
It iterates over each byte in the input program, identifying the opcode and carrying out the corresponding operation.
It handles different types of operations, including stack operations, arithmetic operations, bitwise operations, and branching operations.
If an invalid opcode is encountered, it reverts with an
InvalidOpcode
error. IfOP_STOP
opcode is encountered, it halts execution and returns the top value of the stack.
Wrapping Up
Throughout this blog post, we've explored the workings of the RWInterpreter, a key component of CyberVoid's Deck gameplay mechanic. From stack operations to arithmetic and branching operations, we've uncovered the inner work of its stack-based bytecode language. We've also delved into its error handling and the run
function, both integral to the contract's functionality.
Still, this is just the beginning of our journey. We are continuously improving and enhancing the RWInterpreter and have plans to implement new opcodes like OP_EQ
for equality checks and OP_STORE
and OP_LOAD
for memory operations.
Introducing memory operations will allow programs to store and retrieve values from memory during their execution. This significant step forward will enable more complex and dynamic programs, making your Deck even more powerful and versatile.
In parallel with these developments, we're working on a tool named RWLang, a high-level language interpreter written in JavaScript. Our goal with RWLang is to make it easier for players to write and understand programs for their Decks.
The RWInterpreter is a reflection of our commitment to creating an immersive, strategic and intellectually stimulating gameplay environment. Your Deck isn't just a tool; it's an evolving manifestation of your strategy.
Until next time, keep exploring the void.