Interactive Debugger
Ora ships with a source-level EVM debugger that survives compiler optimizations. Set breakpoints on Ora source lines, step through statements, inspect bindings and machine state, and see exactly how your code lowers to SIR and EVM bytecode — even when the optimizer has reordered, hoisted, duplicated, or folded your code.
The debugger is pre-release. The workflow is usable today, but some features are intentionally conservative about what they expose.
Quick start
Build Ora, then launch a debug session:
zig build
ora debug contracts/vault.ora \
--signature 'deposit(u256)' \
--arg 1000
For contracts with constructors:
ora debug contracts/vault.ora \
--init-signature 'init(u256,u256)' \
--init-arg 1000 \
--init-arg 250 \
--signature 'deposit(u256)' \
--arg 200
The compiler generates debug artifacts, then launches the interactive TUI.
Statement identity, not line numbers
Traditional debuggers map each bytecode PC to a source line and step when the line number changes. This breaks under optimization: hoisted guards land on the wrong line, duplicated branches appear to jump randomly, and folded expressions vanish without explanation.
Ora takes a different approach. The compiler assigns each source-level statement a stable statement_id that propagates through every lowering phase — AST, MLIR, SIR, bytecode. Stepping advances by statement identity transitions, not line changes. The debugger tracks which statement is conceptually active, which runtime work belongs to it, and how the optimizer transformed it.
This means:
- Stepping remains stable under optimization. A hoisted guard still knows which source statement it protects. A duplicated branch knows it's the second execution of statement 42.
- Every line carries provenance. The source pane annotates each line with its relationship to the runtime: direct execution, synthetic (compiler-generated), mixed, guard, or removed.
- Origin tracking connects transformed code to source intent. When you stop on a synthetic operation,
stmt=42 origin=38tells you that statement 42 was derived from the source statement at line 38.
The source pane renders this visually: direct statements get normal text, removed lines appear grayed out, and the gutter marker tells you at a glance whether each line executes as written, was generated by the compiler, or was optimized away entirely.
Compilation and artifacts
ora debug runs the full compiler pipeline with debug metadata enabled, then launches the TUI against the resulting artifacts.
What the compiler emits differently
In debug mode, the compiler enables debug_info metadata emission. This adds statement identity, provenance flags, and scope/binding information to the lowered output without disabling optimizations. The optimizer runs normally — statement identity is what makes the debugger survive the transformations, not avoiding them.
The metadata propagated through the pipeline includes:
statement_id— stable identity for each source statementorigin_statement_id— which source statement a synthetic or lowered operation derives fromexecution_region_id— which contiguous bytecode region implements a specific instantiation of a statementstatement_run_index— the N-th execution of a statement within a function (for duplicated branches)is_synthetic,is_hoisted,is_duplicated— provenance flags explaining what the optimizer did
Generated artifacts
For a contract vault.ora, ora debug writes:
artifacts/vault/
vault.hex bytecode (creation code with constructor args appended)
vault.sourcemap.json PC → (file, line, col, statement_id, origin, region, provenance flags, SIR line)
vault.debug.json lexical scopes, locals, visibility per op, runtime classification
vault.sir.txt emitted SIR text (shown in the SIR pane)
abi/vault.abi.json ABI used to encode calldata from --signature and --arg
You do not need to manage these files. ora debug generates them, passes them to the TUI, and the TUI loads them automatically. They matter when you save or share sessions (see Sessions).
Constructor deployment
For contracts with init(...), the compiler appends ABI-encoded constructor arguments to the creation bytecode. The local EVM deploys the contract (executing the constructor), then the debugger begins at the first statement of the target function.
ora debug vault.ora \
--init-signature 'init(u256,u256)' \
--init-arg 1000 \
--init-arg 250 \
--signature 'deposit(u256)' \
--arg 200
The constructor runs to completion before the debugger pauses. Storage is initialized, and the target function's calldata is encoded from the ABI and --arg values.
Auto-building the TUI binary
The first time you run ora debug, the compiler checks for the ora-evm-debug-tui binary under lib/evm/zig-out/bin/. If it is missing, Ora builds it automatically by running zig build install in lib/evm/. Subsequent runs reuse the cached binary.
UI layout
The debugger TUI has six areas:
runtime direct/direct | stmt 42 | ora 11 => sir 18..23 | pc 0x2f..0x5a
+--[ Ora Source ]----------+--[ SIR Text lines 18..23 stmt 42 region 3.1 ]--+
| 10 . let fee = ... | sir.mul ... |
| 11> . let net = ... | >sir.sub ... |
| 12 . total += net | |sir.sload ... |
| 13 ~ (guard) | <sir.cmp ... |
| 14 - (removed) | |
+--------------------------+--[ Bindings ]-------------------------------------+
| | fee = 50 [folded] |
| | net = 950 |
| | total = 1000 [storage] |
+--[ Machine ]-------------+--[ State: Stack Memory Storage TStore Cdata ]-+
| PC: 0x2f GAS: 999200 | [0] 0x3e8 |
| depth: 0 fn: deposit | [1] 0x32 |
+--[ step-in => direct at line 11 ]--------------------------------------------+
| stmt=42 origin=42 region=3.1 kind=runtime/direct |
| direct coverage: executes as written |
+------------------------------------------------------------------------------+
- Ora Source — your contract with the current statement highlighted. The gutter shows provenance markers and line numbers. Removed lines appear grayed out. A summary header shows the current runtime mapping (statement kind, provenance, statement ID, SIR/PC ranges).
- SIR Text — the lowered SIR intermediate representation, synchronized to the current Ora line. The header shows the SIR line range, statement ID, and execution region. Range markers (
>,|,<) in the SIR gutter show the exact SIR lines for the current statement. - Bindings — visible Ora names at the current stop point: params, locals, storage fields, comptime-folded constants. Each binding shows its storage class and current value.
- Machine — EVM frame state: PC, opcode, gas, call depth, function name.
- State — tabbed EVM machine state: Stack, Memory, Storage, Transient Storage, Calldata.
- Console — stepping trace with provenance detail, command input, and rolling result trail.
Source pane gutter markers
The left margin of the Ora source pane marks each line with its relationship to runtime execution:
| Marker | Meaning | Visual style |
|---|---|---|
. | Direct — executes as written | muted |
~ | Synthetic — compiler-generated lowering | dim |
+ | Mixed — both direct and synthetic ops on this line | highlight |
! | Guard — runtime guard check (requires/invariant) | dim |
- | Removed — declared in source but optimized away entirely | gray/dead |
* | Breakpoint set | emphasis |
^ | Origin line — the source statement that produced the current synthetic stop | emphasis |
> | Current line with breakpoint | emphasis |
| Non-executable line (comments, blank lines, declarations) | muted |
Removed lines (-) are rendered in gray text, making it visually clear which declarations have no runtime presence.
Stepping
| Key | Command | Behavior |
|---|---|---|
s | :step | Step in — advance to next statement at any call depth |
n | :next | Step over — advance to next statement at same or lower depth |
o | :out | Step out — run until call depth decreases |
c | :continue | Continue — run until breakpoint or execution end |
p | :prev | Previous — step backward one debugger stop (replay-based) |
x | :op | Opcode step — execute exactly one EVM opcode |
Stepping operates on statement identity transitions, not line number changes. A single Ora statement like total += net compiles to dozens of EVM opcodes (loads, arithmetic, stores). The debugger skips through them and stops at the next source-level statement boundary.
Under optimization, the same source statement may produce multiple execution regions (if the optimizer duplicated a branch) or execute out of source order (if a guard was hoisted). The debugger tracks this via execution_region_id and statement_run_index, so stepping lands on the correct conceptual statement even when the runtime order diverges from source order.
Opcode stepping (x) is available when you need to inspect mid-statement stack or memory changes.
Reverse stepping (p) works by replaying execution from the beginning to one stop before the current position. It is deterministic but not a full time-travel engine.
Stepping trace
Each step prints a trace line in the console showing where execution landed and why:
step-in => direct at line 11
stmt=42 origin=42 region=3.1 kind=runtime/direct
direct coverage: executes as written
For synthetic or hoisted work:
step-in => synthetic hoisted region from stmt 38, line 13, origin line 8
stmt=38 origin=24 region=2.1 kind=runtime_guard/synthetic
synthetic coverage: reached through compiler-generated lowering
Provenance inspection
The debugger exposes commands for understanding what the optimizer did to specific lines:
| Command | Behavior |
|---|---|
:line-info <N> | Explain line N: statement kind, provenance, statement ID, origin, function |
:why-line <N> | Same as :line-info |
:where | Explain the current stop: provenance, kind, statement ID, origin, region |
:why-here | Same as :where |
:origin | Jump to the origin source line (when current stop is lowered/synthetic) |
:line-info output includes the statement classification and provenance detail:
line 13: runtime guard statement; synthetic coverage: reached through compiler-generated lowering; stmt=38; origin=24; fn=deposit
:where gives the same information for the current stop point:
here: synthetic hoisted; runtime guard statement; stmt=38; line=13; origin line=8; origin stmt=24; region=2.1
SIR text view
When SIR text is available, the debugger shows the lowered intermediate representation side-by-side with Ora source. This is the representation between Ora MLIR and EVM bytecode — the last stage before the assembler produces raw opcodes.
The SIR pane header shows the full mapping context:
lowered region | stmt 42 | region 3.1 | direct | ora 11 => sir 18..23 | idx 12..17 | pc 98..134 | effect storage
This tells you: statement 42, execution region 3.1, maps to SIR lines 18-23, op indices 12-17, PCs 98-134, and writes to storage.
When the current statement was derived from another source line, the header includes the origin:
lowered region | stmt 38 | origin 8 | region 2.1 | synthetic hoisted | ora 13 => sir 8..12 | idx 4..8 | pc 32..64 | effect none
| Key | Command | Behavior |
|---|---|---|
J/K | Scroll the SIR pane independently | |
= | :syncsir | Re-sync SIR pane to the current Ora line |
:sirline <N> | Jump to a specific SIR line | |
:sirfollow | Same as = |
By default, the SIR pane auto-follows as you step. Scrolling with J/K disables auto-follow; = re-enables it.
Range markers in the SIR gutter show the exact SIR line range for the current Ora statement:
| Marker | Meaning |
|---|---|
> | Start of SIR range (or single-line range) |
| | Middle of range |
< | End of SIR range |
Breakpoints
:break 27 set breakpoint on line 27
:break file.ora:27 set breakpoint with explicit file
:delete 27 remove breakpoint
:info break list all breakpoints (with provenance labels)
Breakpoint markers appear in the source gutter: * for a breakpoint, > for the current line with a breakpoint.
:info break shows each breakpoint with its line provenance:
breakpoints (2): L27[direct] L35[synthetic]
Inspecting values
Ora bindings
:print total print a visible binding by name
:print gas print gas remaining
The Bindings pane shows all names visible at the current stop point. Each binding includes its runtime classification:
- Storage fields — read from contract storage, shown with current value
- Memory fields — read from the reserved debug memory band
- Transient storage fields — read from transient storage
- Comptime-folded constants — shown with
[folded]tag and their compile-time value - SSA locals/params — visible but may show as opaque if the optimizer eliminated their storage location
Raw machine state
:print stack[0] top of stack
:print slot 0x00 raw storage slot by id
:print mem 0x80 4 memory words starting at offset
:print storage all storage slots for current address
:print tstore all transient storage slots
:print calldata full calldata of current frame
Mutating state
The debugger supports limited live mutation for what-if exploration:
:set total = 1337 set a writable rooted binding
:set gas = 750000 change gas remaining
:set slot 0x00 = 7 set a raw storage slot
:set mem 0x80 = 42 set a raw memory word
Only rooted bindings (storage, memory, transient storage fields) are writable. Plain SSA locals cannot be mutated — the debugger does not pretend optimized-away values are stable slots.
Navigation
| Key | Command | Behavior |
|---|---|---|
j/k | Scroll Ora source up/down | |
J/K | Scroll SIR text up/down | |
| PageUp/PageDown | Scroll by 8 lines | |
:line <N> | Jump to Ora source line N | |
:sirline <N> | Jump to SIR line N | |
:origin | Jump to origin source line (for synthetic/lowered stops) |
Checkpoints
Save and restore execution positions:
:checkpoint save current position
:checkpoints list saved checkpoints
:restart 1 restore checkpoint 1
Checkpoints record the replay position plus UI state (scroll, focus, active tab). Restoring a checkpoint replays execution to that point.
Backtrace and frames
Inspect nested calls:
:bt show call stack
:frame 0 select top frame
:frame 1 select parent frame
The Machine and State panes follow the selected frame. Frame 0 is the top (innermost) frame.
Sessions
Sessions let you save a debugging position and come back to it later, or share it with someone else.
Saving a session
:write-session artifacts/vault/session.json
:ws artifacts/vault/session.json
:ws is shorthand for :write-session. The session is written as JSON to the path you specify.
Loading a session
:load-session artifacts/vault/session.json
:ls artifacts/vault/session.json
:ls is shorthand for :load-session. Loading a session replaces the current debugger state entirely — it reloads the artifacts, replays the step history, restores breakpoints and checkpoints, and returns you to the exact position you saved.
What a session contains
A saved session records:
- Artifact paths — bytecode, source map, debug info, SIR text, ABI
- Calldata — the encoded function call (so the same transaction replays)
- Step history — the sequence of step commands (
in,over,out,continue,opcode) that reached the current position - Breakpoints — all active breakpoints by PC
- Checkpoints — saved positions with their replay index and UI state
- UI state — Ora source scroll position, SIR scroll position, SIR follow mode, focused line, active state tab
Sessions are replayable, not snapshots. They do not dump EVM memory or storage. Instead, they record the step commands that produced the current state and replay them on load. This means sessions are small, deterministic, and human-readable JSON.
Sharing sessions
Sessions are portable across machines if the referenced artifacts exist at the same paths. To share a session:
- Save the session into the
artifacts/directory alongside the build output - Share the
artifacts/<name>/directory (it contains the bytecode, source map, debug info, SIR text, ABI, and session file) - The recipient runs
:load-session artifacts/<name>/session.json
Session files do not yet validate artifact hashes, so if the artifacts change between save and load, the replay may diverge.
Restarting
:run restart from the beginning (clears step history)
:rerun same as :run
:r same as :run
:run rebuilds the EVM session from scratch and replays to the initial stop point, preserving your breakpoints. This is useful after mutating state to get back to a clean starting point.
Keyboard reference
Stepping
s step in
n step over
o step out
c continue
p previous stop
x opcode step
Source navigation
j/k scroll Ora source
J/K scroll SIR text
PgUp/PgDn scroll by 8 lines
= sync SIR to current Ora line
EVM state tabs
1..5 jump to tab (Stack/Memory/Storage/TStore/Calldata)
[/] cycle through tabs
Commands
: enter command mode
q quit
Source gutter markers
. direct — executes as written
~ synthetic — compiler-generated lowering
+ mixed — both direct and synthetic ops
! guard — runtime guard (requires/invariant)
- removed — optimized away (shown in gray)
* breakpoint
^ origin line — source of current synthetic stop
> current line with breakpoint
SIR range markers
> start of SIR range (or single-line)
| middle of range
< end of SIR range
CLI reference
ora debug <file.ora> [options]
Function call
Specify which function to debug and its arguments:
ora debug vault.ora --signature 'deposit(u256)' --arg 1000
| Option | Description |
|---|---|
--signature 'fn(type,...)' | Function to call after deployment |
--arg <value> | Positional argument (repeatable, order matters) |
--calldata-hex <hex> | Raw calldata bytes instead of signature + args |
Arguments are ABI-encoded using the contract's generated ABI. Types in the signature must match the Ora function's parameter types.
Constructor
If the contract has an init(...) function, pass constructor arguments separately:
ora debug vault.ora \
--init-signature 'init(u256,u256)' \
--init-arg 1000 \
--init-arg 250 \
--signature 'deposit(u256)' \
--arg 200
| Option | Description |
|---|---|
--init-signature 'init(type,...)' | Constructor signature |
--init-arg <value> | Constructor argument (repeatable, order matters) |
--init-calldata-hex <hex> | Raw constructor calldata bytes |
Verification
By default, ora debug skips Z3 verification to speed up compilation. To include verification:
ora debug vault.ora --verify --signature 'deposit(u256)' --arg 1000
How comptime values appear
Comptime-evaluated expressions produce no bytecode. In the debugger:
- Lines that were fully folded at compile time are marked as removed (
-) in the gutter and rendered in gray — the debugger skips them during stepping - Comptime-folded constants appear in the Bindings pane with a
[folded]tag and their compile-time value - The SIR view shows where folded values are inlined as constants
comptime const FEE: u256 = 2 + 3 + 5; // no bytecode — gutter shows `-`, line grayed out
pub fn apply(amount: u256) -> u256
requires(amount > FEE)
{
return amount - FEE; // FEE appears as [folded] = 10 in Bindings
}
Current limitations
- Reverse stepping is replay-based, not full time-travel
- Source and bindings reflect the current stop point, not arbitrary frame reconstruction
- Session files do not yet enforce artifact hash validation
- Mutation is limited to rooted bindings and raw machine locations
- Multi-contract calls enter unmapped bytecode (shown as
[external call]) - Comptime trace viewer (showing the evaluation tree, not just folded values) is planned but not yet shipped
Example contracts
The ora-example/debugger/ directory contains contracts designed for debugger testing:
constructor_value.ora— minimal constructor, single storage fieldstate_walkthrough.ora— stateful contract with helper calls andrequiresguardsledger_walkthrough.ora— constructor args, branching, storage updates, helper functionsoptimizer_probe.ora— comptime constants, runtime guards, branch-dependent behavior, storage slot inspectioncomptime_debug_probe.ora— comptime folding, debug visibility of folded constants, removed-line behavior