Variables
Variables connect source-level identifiers to runtime locations. They're the key to showing developers meaningful values instead of raw bytes.
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 codedeclaration: source range where the variable was declaredtype: 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:
- Understand the data's structure (scalar, struct, array, etc.)
- Know how to interpret the bytes once located (signed vs unsigned, field order, etc.)
- 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:
{
"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:
- Instruction at offset 50:
balancecomes into scope, stored on stack - Instructions 51-100:
balanceremains in scope - Instruction at offset 101:
balanceleaves 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:
{
"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
- Types documentation for type structure details
- Pointers documentation for location specification
- Context schema for the full schema