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.

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