Skip to content

Commit 825e144

Browse files
authored
Modularize executor into opcode modules and reorganize project structure (#7)
* Modularize executor into opcode modules and reorganize project structure - Split monolithic executor.ex (942 lines) into thin dispatcher + 9 opcode modules under opcodes/ - Move Transaction, Block, Contract into context/ namespace (EEVM.Context.*) - Split monolithic test file into per-module test files - Add comprehensive @moduledoc, @doc, and @SPEC to all new modules - Update README architecture section and remove completed roadmap * Fix formatting lint issues in opcode modules
1 parent 4aed3a0 commit 825e144

28 files changed

Lines changed: 2311 additions & 1808 deletions

README.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,44 @@ iex> EEVM.disassemble(<<0x60, 0x01, 0x60, 0x02, 0x01, 0x00>>)
2626
| **Arithmetic** | `ADD` `MUL` `SUB` `DIV` `SDIV` `MOD` `SMOD` `ADDMOD` `MULMOD` `EXP` `SIGNEXTEND` |
2727
| **Comparison** | `LT` `GT` `SLT` `SGT` `EQ` `ISZERO` |
2828
| **Bitwise** | `AND` `OR` `XOR` `NOT` `BYTE` `SHL` `SHR` `SAR` |
29-
| **Stack** | `POP` `PUSH1``PUSH32` `DUP1``DUP16` `SWAP1``SWAP16` |
29+
| **Crypto** | `KECCAK256` |
30+
| **Stack** | `POP` `PUSH0` `PUSH1``PUSH32` `DUP1``DUP16` `SWAP1``SWAP16` |
3031
| **Memory** | `MLOAD` `MSTORE` `MSTORE8` `MSIZE` |
32+
| **Storage** | `SLOAD` `SSTORE` |
33+
| **Environment** | `ADDRESS` `BALANCE` `ORIGIN` `CALLER` `CALLVALUE` `CALLDATALOAD` `CALLDATASIZE` `CALLDATACOPY` `CODESIZE` `GASPRICE` `RETURNDATASIZE` `BLOCKHASH` `COINBASE` `TIMESTAMP` `NUMBER` `PREVRANDAO` `GASLIMIT` `CHAINID` `SELFBALANCE` `BASEFEE` `GAS` |
3134
| **Control Flow** | `JUMP` `JUMPI` `JUMPDEST` `PC` |
3235
| **System** | `STOP` `RETURN` `REVERT` `INVALID` |
3336

3437
## Architecture
3538

3639
```
3740
lib/
38-
├── eevm.ex # Public API — execute, disassemble, inspect
41+
├── eevm.ex # Public API — execute, disassemble, inspect
3942
└── eevm/
40-
├── stack.ex # LIFO stack (1024 depth, uint256 values)
41-
├── memory.ex # Byte-addressable linear memory
42-
├── machine_state.ex # Execution state (PC, stack, memory, gas)
43-
├── opcodes.ex # Opcode byte → name + stack metadata
44-
└── executor.ex # Fetch-decode-execute loop
43+
├── executor.ex # Thin dispatcher — fetch, decode, route to opcode modules
44+
├── machine_state.ex # Execution state (PC, stack, memory, gas)
45+
├── stack.ex # LIFO stack (1024 depth, uint256 values)
46+
├── memory.ex # Byte-addressable linear memory
47+
├── storage.ex # Persistent key-value storage
48+
├── gas.ex # Gas metering and per-opcode costs
49+
├── opcodes.ex # Opcode byte → name + stack metadata
50+
├── context/
51+
│ ├── transaction.ex # Transaction context (origin, gas price)
52+
│ ├── block.ex # Block context (coinbase, timestamp, number)
53+
│ └── contract.ex # Contract context (address, caller, value, calldata)
54+
└── opcodes/
55+
├── helpers.ex # Shared utilities (signing, modular exponentiation)
56+
├── arithmetic.ex # ADD, MUL, SUB, DIV, MOD, EXP, SIGNEXTEND
57+
├── comparison.ex # LT, GT, SLT, SGT, EQ, ISZERO
58+
├── bitwise.ex # AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR
59+
├── crypto.ex # KECCAK256
60+
├── stack_memory_storage.ex # POP, MLOAD, MSTORE, SLOAD, SSTORE, MSIZE
61+
├── environment.ex # ADDRESS, CALLER, CALLVALUE, CALLDATALOAD, ...
62+
├── control_flow.ex # JUMP, JUMPI, PUSH0–PUSH32, DUP, SWAP
63+
└── system.ex # STOP, RETURN, REVERT, INVALID
4564
```
4665

47-
The EVM is a **stack machine**. The executor reads one opcode at a time from bytecode, pops operands from the stack, computes, and pushes results back. All values are unsigned 256-bit integers. Memory is a separate byte-addressable space that expands on demand.
66+
The EVM is a **stack machine**. The executor reads one opcode at a time from bytecode and dispatches to the appropriate opcode module. Each module pops operands from the stack, computes, and pushes results back. All values are unsigned 256-bit integers. Memory is a separate byte-addressable space that expands on demand.
4867

4968
The architecture is intentionally flat — no processes, no GenServers, no OTP. Pure functions in, state out. This makes it easy to follow the execution flow and understand both the EVM and Elixir's functional style.
5069

@@ -79,16 +98,6 @@ This project is designed as a learning tool. Each module demonstrates specific E
7998
- **Module attributes**`@constants` and `@spec` type annotations
8099
- **Binary pattern matching** — Parsing raw bytecode with `<<>>` syntax
81100

82-
## Roadmap
83-
84-
- [ ] Gas metering (per-opcode costs)
85-
- [ ] Storage (`SLOAD` / `SSTORE`)
86-
- [ ] Environment opcodes (`CALLER`, `CALLVALUE`, `CALLDATA*`)
87-
- [ ] `LOG0``LOG4` events
88-
- [ ] Contract creation and `CALL`
89-
- [ ] Precompiled contracts
90-
- [ ] EVM test suite compatibility (ethereum/tests)
91-
92101
## License
93102

94103
MIT
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule EEVM.Block do
1+
defmodule EEVM.Context.Block do
22
@moduledoc """
33
Block-level context — information about the block being executed.
44
@@ -64,7 +64,7 @@ defmodule EEVM.Block do
6464
6565
## Example
6666
67-
iex> block = EEVM.Block.new(number: 18_000_000, chain_id: 1)
67+
iex> block = EEVM.Context.Block.new(number: 18_000_000, chain_id: 1)
6868
iex> block.number
6969
18000000
7070
"""
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule EEVM.Contract do
1+
defmodule EEVM.Context.Contract do
22
@moduledoc """
33
Contract/message-level context — the current call frame's environment.
44
@@ -24,7 +24,7 @@ defmodule EEVM.Contract do
2424
2525
- `caller` (CALLER/msg.sender) = who directly called this contract.
2626
Changes with each nested CALL.
27-
- `origin` (ORIGIN/tx.origin, in `EEVM.Transaction`) = the EOA that
27+
- `origin` (ORIGIN/tx.origin, in `EEVM.Context.Transaction`) = the EOA that
2828
signed the transaction. Never changes.
2929
3030
Example: EOA → Contract A → Contract B
@@ -62,7 +62,7 @@ defmodule EEVM.Contract do
6262
6363
## Example
6464
65-
iex> contract = EEVM.Contract.new(caller: 0xDEAD, callvalue: 1000)
65+
iex> contract = EEVM.Context.Contract.new(caller: 0xDEAD, callvalue: 1000)
6666
iex> contract.caller
6767
0xDEAD
6868
"""
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule EEVM.Transaction do
1+
defmodule EEVM.Context.Transaction do
22
@moduledoc """
33
Transaction-level context — information about the original transaction.
44
@@ -20,7 +20,7 @@ defmodule EEVM.Transaction do
2020
### Origin vs Caller
2121
2222
`origin` is always the EOA that signed the transaction — it never changes,
23-
even through nested contract calls. `caller` (in `EEVM.Contract`) is the
23+
even through nested contract calls. `caller` (in `EEVM.Context.Contract`) is the
2424
*direct* caller of the current frame and changes with each CALL.
2525
2626
## Elixir Learning Notes
@@ -47,7 +47,7 @@ defmodule EEVM.Transaction do
4747
4848
## Example
4949
50-
iex> tx = EEVM.Transaction.new(origin: 0xDEAD, gasprice: 20_000_000_000)
50+
iex> tx = EEVM.Context.Transaction.new(origin: 0xDEAD, gasprice: 20_000_000_000)
5151
iex> tx.origin
5252
0xDEAD
5353
"""

0 commit comments

Comments
 (0)