Skip to main content

Programs

This page explains the mental model behind ethdebug/format program representations. For reference documentation on instructions, variables, and tracing, see the Programs reference.

Programs map bytecode to high-level context​

When a compiler transforms source code into bytecode, it creates a gap between what developers wrote and what actually executes. A program bridges this gap by providing high-level context for each bytecode instruction.

Programs enable debuggers to answer:

  • What source code corresponds to this instruction?
  • What variables are in scope right now?
  • What function are we in?
  • Should this instruction be treated as "stepping into" a call?

Programs correspond to bytecode​

Each program is associated with a specific piece of bytecode:

  • Call bytecode — executed when a contract receives a message
  • Create bytecode — executed during contract deployment

The same contract typically has both: create bytecode runs once during deployment, call bytecode runs whenever the contract is invoked afterward.

Think of it as: "you have this bytecode → here's its program." A program references the compilation that produced the bytecode, linking back to source files and compiler metadata through the info/resources schema.

Instruction listings​

Programs contain a sequential list of instructions, one for each machine instruction in the bytecode. Each instruction specifies:

  • offset — the byte position in the bytecode (equal to the program counter on non-EOF EVMs)
  • context — high-level information about this point in execution

Instructions are ordered to match the bytecode, enabling fast lookup by offset. Not every byte offset has an entry — only positions where opcodes begin.

{
"instructions": [
{ "offset": 0, "context": { /* ... */ } },
{ "offset": 1, "context": { /* ... */ } },
{ "offset": 4, "context": { /* ... */ } }
]
}

Context information​

Each instruction's context describes what's true at that point in execution. Context information may include:

Source ranges​

Which source code relates to this instruction:

{
"code": {
"source": { "id": "source-1" },
"range": { "offset": 150, "length": 25 }
}
}

Variables​

What variables are in scope and where to find their values:

{
"variables": [
{
"identifier": "balance",
"type": { "kind": "uint", "bits": 256 },
"pointer": { "location": "storage", "slot": 0 }
}
]
}

Each variable has an identifier, a type, and a pointer. The pointer tells the debugger where to find the variable's current value.

Compilation frame​

Specifies which compilation frame the context applies to. This supports compilers with distinct stages (e.g., source language vs. intermediate representation):

The frame value is a string naming the relevant compilation frame (e.g., "source", "ir"), allowing the same instruction to carry context for different compiler stages.

Context is valid after instruction execution​

An instruction's context describes the state that exists after that instruction completes. This timing is important:

  • Before the instruction runs, the previous context applies
  • After the instruction runs, this context applies

For example, if an instruction stores a value in a variable, the variable's pointer in that instruction's context points to where the value now lives.

Contexts as state transitions​

A debugger maintains a model of the high-level program state as it steps through execution. Each context encountered serves as a state transition:

  1. Debugger observes the program counter
  2. Looks up the instruction at that offset
  3. Reads the context to learn what changed
  4. Updates its high-level state model
  5. Continues to the next instruction

Contexts can be composed using:

  • gather — combine multiple context pieces together
  • pick — choose a context based on a runtime condition
  • remark — add metadata without changing scope

This composition enables describing complex scenarios like conditional variable assignments or function inlining.

Function call contexts​

Programs answer "what function are we in?" through three context types that track function boundaries during execution:

  • invoke — marks an instruction that enters a function. Indicates the invocation kind (internal jump, external message call, or contract creation) and provides pointers to call arguments, target address, gas, and value as appropriate.
  • return — marks an instruction associated with a successful return from a function. Provides a pointer to the return data.
  • revert — marks an instruction associated with a failed call. May include a pointer to revert reason data or a numeric panic code.

All three extend a common function identity schema with optional fields for the function's name, declaration source range, and type. This lets compilers provide as much or as little attribution as available — from a fully identified transfer call down to an anonymous indirect invocation through a function pointer.

{
"invoke": {
"identifier": "transfer",
"jump": true,
"target": {
"pointer": { "location": "stack", "slot": 0 }
},
"arguments": {
"pointer": {
"group": [
{ "name": "to", "location": "stack", "slot": 2 },
{ "name": "amount", "location": "stack", "slot": 3 }
]
}
}
}
}

A debugger uses these contexts to reconstruct call stacks, show function names in stepping UI, and display argument/return values alongside source code.

What tracing enables​

By following contexts through execution, debuggers can provide:

  1. Source mapping — show the current line in source code
  2. Variable inspection — display current values of in-scope variables
  3. Call stacks — reconstruct function call history
  4. Data structure visualization — present arrays and mappings meaningfully
  5. Control flow insight — indicate loop iterations, function boundaries

The program schema provides the compile-time guarantees that make runtime debugging possible.

Next steps​