On this page:
1.1 A first program
1.2 The assembly dialect
1.3 Using the result from other modules
1.4 The programmatic API
assemble
run-source
evm-result
disassemble
asm-config
parse-evm
9.3

1 #lang evm-redex/asm — writing and running EVM assembly🔗

 #lang evm-redex/asm package: evm-redex

#lang evm-redex/asm is a small language whose source is an EVM program, written as assembly. Running the module — racket file.rkt, or Run in DrRacket — assembles the text to bytecode, executes it on the library’s interpreter (the same semantics as the rest of evm-redex), prints a summary, and provides the result for inspection.

This is a fragment runner, not a transaction simulator: no nonce checks, no intrinsic-gas charge, an empty starting world (so CALL / SLOAD against other contracts execute, but there is nothing there to reach). It is the shortest path from "here is some EVM code" to "here is what it does".

1.1 A first program🔗

#lang evm-redex/asm
PUSH1 0x05
PUSH1 0x03
ADD
STOP

Running it prints:

outcome:     stop

gas used:    9

stack:       [0x8]

returndata:  0x

outcome is the halt reason (stop, return, revert, out-of-gas, invalid-opcode, …); stack is the final stack, top element first, each a 256-bit word in hex; returndata is the bytes of a RETURN / REVERT.

1.2 The assembly dialect🔗

One instruction per token, one or more per line; mnemonics are case-insensitive. A ; begins a comment that runs to the end of the line.

Opcodes. Every opcode the interpreter implements is a mnemonic: ADD, MUL, SSTORE, JUMPI, KECCAK256, CALLDATALOAD, DUP1DUP16, SWAP1SWAP16, LOG0LOG4, RETURN, REVERT, and so on. An unknown mnemonic is a compile error.

PUSH. Three forms:

  • ExplicitPUSH1 0x05PUSH32 0x..: the operand is padded to exactly k bytes; an operand too wide for k is an error.

  • Auto-sized — a bare PUSH with a literal chooses the minimal width: PUSH 0x1234 becomes PUSH2. (PUSH 0 is PUSH1 0x00; the zero-byte PUSH0 opcode is written PUSH0.)

  • LabelPUSH name references a label (below) and is always emitted as PUSH2.

Operands are hexadecimal (0x..) or decimal.

Labels. A token name: defines a label at the current position; a bare name as a PUSH operand references it, and the assembler resolves it to that position’s program counter. This is how jumps are written without counting bytes:

#lang evm-redex/asm
.gas 100000
 
      PUSH1 0x00        ; counter = 0
loop: JUMPDEST
      PUSH1 0x01
      ADD               ; counter += 1
      DUP1              ; [counter, counter]
      PUSH1 0x0a        ; [10, counter, counter]
      GT                ; 10 > counter ?
      PUSH loop         ; the JUMPDEST's pc, resolved for you
      JUMPI             ; if so, loop
      STOP              ; leaves 10 on the stack

outcome:     stop

gas used:    293

stack:       [0xa]

Directives configure the run; they must come before any instruction:

  • .gas N — the gas budget (default 30,000,000).

  • .calldata 0x.. — the transaction calldata, for CALLDATALOAD / CALLDATASIZE / CALLDATACOPY.

#lang evm-redex/asm
.calldata 0x00000000000000000000000000000000000000000000000000000000000000ff
 
PUSH1 0x00
CALLDATALOAD      ; the first 32-byte word of calldata
PUSH1 0x00
MSTORE
PUSH1 0x20
PUSH1 0x00
RETURN            ; return memory[0..32)

outcome:     return

gas used:    21

stack:       []

returndata:  0x00000000000000000000000000000000000000000000000000000000000000ff

Raw bytecode. If the source begins with 0x, the whole thing is taken as a hex bytecode blob — handy for pasting solc output and disassembling or running it as-is. (A mnemonic program never starts with 0x, so there is no ambiguity.)

#lang evm-redex/asm
0x6005600301

1.3 Using the result from other modules🔗

A #lang evm-redex/asm module provides three bindings, so another module can require it and inspect what the program did — this is how the example programs are tested:

  • program-bytes — the assembled bytecode, a list of bytes.

  • result — an evm-result (outcome, gas, stack, return data, and the final machine).

  • final-machine — the final machine term, for reading anything else (memory, logs, world) with machine-field.

(require "arithmetic.rkt")
(evm-result-stack result)     ; => '(8)
program-bytes                 ; => '(96 5 96 3 1 0)

1.4 The programmatic API🔗

The same assembler, disassembler, and runner are available as ordinary functions through (require evm-redex/asm) — no #lang needed. This is what the language is built on, and what the unit tests exercise directly.

procedure

(assemble src)  
(listof byte?) asm-config?
  src : (or/c string? input-port?)
Parse and assemble EVM assembly text, returning the bytecode and the asm-config the directives asked for. Raises exn:fail:evm-asm? on a syntax or assembly error (unknown mnemonic, undefined/duplicate label, an operand too wide for an explicit PUSHk), with the source line and column in the message.

(define-values (code cfg) (assemble "PUSH1 0x05\nPUSH1 0x03\nADD\nSTOP"))
; code => '(96 5 96 3 1 0)

procedure

(run-source code config)  evm-result?

  code : (listof byte?)
  config : asm-config?
Run assembled bytecode on the interpreter and package the outcome. The wrapper pins the fast Racket executor and folds any Racket-level failure (e.g. fuel exhaustion) into an 'error outcome.

struct

(struct evm-result (outcome
    gas-used
    gas-left
    stack
    returndata
    machine
    err)
    #:transparent)
  outcome : symbol?
  gas-used : exact-nonnegative-integer?
  gas-left : exact-nonnegative-integer?
  stack : (listof exact-nonnegative-integer?)
  returndata : (listof byte?)
  machine : any/c
  err : (or/c #f string?)
What a run produced. stack is top-first; machine is the final machine term (read more from it with machine-field from (require evm-redex)).

procedure

(disassemble code)  string?

  code : (listof byte?)
The inverse of assemble for the byte level: one instruction per line, PUSH immediates shown inline as hex, immediates skipped the way the interpreter skips them. Re-assembling the result reproduces the same bytes.

(disassemble (list 97 0 86 0))
; => "PUSH2 0x0056\nSTOP"

struct

(struct asm-config (gas calldata)
    #:transparent)
  gas : exact-nonnegative-integer?
  calldata : (listof byte?)
The environment the directives select: the gas budget and the calldata.

procedure

(parse-evm input [#:source source])  parsed?

  input : (or/c string? input-port?)
  source : any/c = 'evm
The parser alone: assembly text -> an AST (parsed, carrying dir directives and a stream of label / instr items with source locations). Exposed for tools that want to walk a program without assembling it; assemble is parse-evm followed by assemble-parsed.