Skip to main content

Variables

Variables connect source-level identifiers to runtime locations. They're the key to showing developers meaningful values instead of raw bytes.

Terminology note

In ethdebug/format, "variable" means different things in different contexts:

  • Program variables (this page): source-level identifiers with types and pointers
  • Pointer variables: named bindings for use in expressions (like loop indices in list collections)

See the glossary for complete definitions.

Variable structure

A variable has up to four optional fields (at least one required):

{
"identifier": "balance",
"type": { "kind": "uint", "bits": 256 },
"pointer": {
"location": "storage",
"slot": 0
}
}
  • identifier: the variable name from source code
  • declaration: source range where the variable was declared
  • type: the structure of the data (an ethdebug/format type)
  • pointer: where to find the data (an ethdebug/format pointer)

Types describe structure

The type field describes what shape the data takes. A debugger uses this to:

  1. Understand the data's structure (scalar, struct, array, etc.)
  2. Know how to interpret the bytes once located (signed vs unsigned, field order, etc.)
  3. Display the value in a readable format

For complex types, the type definition guides the debugger through nested structures:

{
"identifier": "user",
"type": {
"kind": "struct",
"contains": [
{ "name": "id", "type": { "kind": "uint", "bits": 256 } },
{ "name": "active", "type": { "kind": "bool" } }
]
},
"pointer": { ... }
}

Pointers tell debuggers where to look

The pointer field specifies the data's location. This can be simple:

{
"pointer": {
"location": "stack",
"slot": 0
}
}

Or complex, for data spread across multiple locations:

Struct members in packed storageSchema:ethdebug/format/pointer/collection/group
{
"pointer": {
"group": [
{ "name": "id", "location": "storage", "slot": 5 },
{ "name": "active", "location": "storage", "slot": 5, "offset": 31 }
]
}
}

Scope and lifetime

Variables appear in context when they're valid. The instruction's context represents what's true after that instruction executes.

A variable might:

  • Become available when a function is entered
  • Change location as it moves from stack to memory
  • Go out of scope when a block ends

The instruction list captures these transitions. As a debugger steps through execution, it accumulates and discards variables based on each instruction's context.

Example: local variable lifecycle

Consider this Solidity snippet:

function transfer(address to, uint256 amount) {
uint256 balance = balances[msg.sender];
// ... use balance ...
}

The compiled bytecode might have:

  1. Instruction at offset 50: balance comes into scope, stored on stack
  2. Instructions 51-100: balance remains in scope
  3. Instruction at offset 101: balance leaves scope (function returns)

The program captures this by including balance in the variables list for instructions 50-100 and omitting it afterward.

Multiple variables

An instruction's context can declare multiple variables:

Multiple variables in contextSchema:ethdebug/format/program/instruction
{
"offset": 75,
"context": {
"variables": [
{
"identifier": "sender",
"type": { "kind": "address" },
"pointer": { "location": "stack", "slot": 2 }
},
{
"identifier": "value",
"type": { "kind": "uint", "bits": 256 },
"pointer": { "location": "stack", "slot": 1 }
},
{
"identifier": "success",
"type": { "kind": "bool" },
"pointer": { "location": "stack", "slot": 0 }
}
]
}
}

Learn more