Appendix D — The Debugger
Nex includes an interactive debugger in the CLI REPL. This appendix condenses the main commands from docs/md/DEBUGGER.md into a tutorial-oriented quick reference.
D.1 Starting the Debugger
Start the REPL:
nexEnable debugging:
:debug on
Check status:
:debug status
Disable:
:debug off
D.2 Breakpoints
Create breakpoints:
:break <spec>
:break <spec> if <expr>
:tbreak <spec>
Common breakpoint forms:
Class.methodClass.method:42file.nex:42field:statusOrder#status
The line-qualified forms are accepted by the debugger command parser, but on the current REPL/interpreter path they are not yet reliable enough to treat as the primary workflow. In practice, the dependable breakpoint forms today are Class.method, field watch/write breakpoints, synthetic function breakpoints such as name_Function.call1, and hit-count breakpoints.
List breakpoints:
:breaks
Remove them:
:clearbreak <id>
:clearbreak <spec>
:clearbreak all
Enable or disable without removing:
:enable <id>
:disable <id>
For code defined as standalone functions rather than class methods, the most reliable current form is the synthetic function class breakpoint:
- a function
square_plus_one(...)is represented internally assquare_plus_one_Function - its callable entry point is
call0,call1,call2, and so on, depending on arity - so a one-argument function can be broken on with
:break square_plus_one_Function.call1
D.3 Watchpoints
Watchpoints pause when an expression’s value changes.
:watch <expr>
:watch <expr> if <expr>
:watches
:clearwatch <id|all>
:enablewatch <id|all>
:disablewatch <id|all>
Useful for fields or derived state that change across a long execution.
D.4 Break-On Policies
Pause automatically on failures:
:breakon exception on
:breakon contract on
Show status:
:breakon status
Filters:
:breakon exception on <substring>
:breakon contract on <pre|post|invariant>
These are particularly helpful in a contract-heavy language because they let you stop exactly when a precondition, postcondition, or invariant fails.
D.5 Debug Prompt Commands
When execution pauses, the prompt changes to dbg>.
Execution control:
:continueor:c:stepor:s:nextor:n:finishor:f
Inspection:
:where:frames:frame <n>:locals:print <expr>
The most useful first commands at a breakpoint are usually:
:where:locals:print <expr>
D.6 Example Sessions
The quickest way to learn the debugger is to see a few realistic sessions.
D.6.1 Session 0: Debug A Standalone Function In The REPL
Standalone functions do not currently have a direct :break function_name form. Instead, break on the generated function class and its callN method.
Suppose you define:
function square_plus_one(n: Integer): Integer
do
let squared: Integer := n * n
result := squared + 1
end
Then a simple debugging session looks like:
nex> :debug on
nex> :break square_plus_one_Function.call1
nex> print(square_plus_one(4))
dbg> :where
dbg> :locals
dbg> :print n
dbg> :next
dbg> :locals
dbg> :continue
The important point here is:
square_plus_one_Function.call1is the internal callable form ofsquare_plus_one- once paused, the normal debugger commands work the same way as for methods
:localsshows function parameters such asn- after the first
:next, locals introduced inside the function body, such assquared, become visible
D.6.2 Session 1: Stop At A Routine And Step Through It
Suppose you define:
class Counter
create
make() do
total := 0
end
feature
total: Integer
add(n: Integer) do
let old_total: Integer := total
total := old_total + n
let new_total: Integer := total
end
end
Then a simple stepping session looks like:
nex> :debug on
nex> :break Counter.add
nex> let c := create Counter.make
nex> c.add(5)
dbg> :where
dbg> :locals
dbg> :print total
dbg> :next
dbg> :locals
dbg> :next
dbg> :print total
dbg> :continue
What this tells you:
:whereconfirms that you stopped inCounter.add:localsshows the argumentnand the currentthis:print totalbefore:nextshows the current field binding- the first
:nextadvances to the next statement in the routine :localsthen showsold_totalbefore the assignment is applied- the second
:nextadvances past the assignment :print totalnow shows the updated field value
One subtle point: inside a paused method, bare field names such as total refer to the live field bindings in the method environment. By contrast, this.total reads from the object value itself, which is not rebuilt until the routine returns unless you explicitly assign through this.field := ....
This is the basic “what changed on this line?” workflow.
D.6.3 Session 2: Stop On A Contract Failure
Suppose Wallet.spend has a precondition and an invariant:
class Wallet
create
make(initial_money: Real) do
money := initial_money
end
feature
money: Real
spend(amount: Real)
require
enough: amount <= money
do
money := money - amount
ensure
decreased: money = old money - amount
end
invariant
non_negative: money >= 0.0
end
Now enable contract breaks and trigger a failure:
nex> :debug on
nex> :breakon contract on
nex> let w := create Wallet.make(10.0)
nex> w.spend(25.0)
dbg> :where
dbg> :locals
dbg> :print w.money
dbg> :print 25.0 <= w.money
This is a good pattern when you already know the failure is a contract problem and want to confirm the caller-side state:
:whereshows the paused call site:localsshows the caller bindings that led to the failure:print w.moneylets you inspect the receiver state directly:print 25.0 <= w.moneylets you test the failing condition from the caller context
One current limitation is important here: for a precondition failure raised by a call such as w.spend(25.0), the debugger pauses at the caller context, not inside Wallet.spend. That means names such as amount are not available at this pause point.
If you need to inspect callee-side names before the precondition fails, combine contract breaking with a normal method breakpoint:
nex> :debug on
nex> :break Wallet.spend
nex> :breakon contract on
nex> let w := create Wallet.make(100.0)
nex> w.spend(25.0)
dbg> :print amount
dbg> :print money
dbg> :continue
This lets you inspect the method arguments and field bindings at routine entry, before the precondition violation is reported.
D.6.4 Session 3: Watch A Value Across Several Calls
Watchpoints are useful when the suspicious state changes gradually rather than at a single obvious line.
Using the same Counter class:
nex> :debug on
nex> let c := create Counter.make
nex> :watch c.total
nex> c.add(2)
dbg> :print c.total
dbg> :continue
nex> c.add(3)
dbg> :print c.total
dbg> :continue
nex> :watches
This is useful when:
- one field changes in many places
- you care about the moment its value changes
- setting many separate breakpoints would be noisy
D.6.5 Session 4: Inspect The Stack And Move Between Frames
When one routine calls another, the most useful question is often not “where am I?” but “who called me, and with what values?”
Suppose:
class Pricing
feature
discount(price: Real): Real do
result := price * 0.9
end
checkout(subtotal: Real): Real do
result := this.discount(subtotal)
end
end
Then a stack-oriented session looks like:
nex> :debug on
nex> :break Pricing.discount
nex> let p := create Pricing
nex> print(p.checkout(80.0))
dbg> :frames
dbg> :locals
dbg> :frame 1
dbg> :locals
dbg> :print subtotal
dbg> :frame 0
dbg> :print price
dbg> :finish
Here:
- frame
0is the current routine - frame
1is its caller :frame <n>changes which context:localsand:printuse:finishis often faster than repeated:nextwhen you only care about the return from the current routine
These five sessions cover most day-to-day debugging:
- stop at a routine
- inspect the active frame
- step through state changes
- stop automatically on contract failures
- watch one changing value
- move up and down the call stack
D.6.6 Session 5: Load A File, Then Debug It
Suppose demo.nex contains:
class Pricing
feature
discount(price: Real): Real do
let base: Real := price
result := base * 0.9
end
end
One practical workflow is:
nex> :load demo.nex
nex> :debug on
nex> :break Pricing.discount
nex> let p := create Pricing
nex> print(p.discount(10.0))
dbg> :where
dbg> :locals
dbg> :print price
dbg> :next
dbg> :locals
dbg> :continue
This is often the most convenient way to debug file-based code in the REPL:
:loadbrings the class or function definitions into the current session- the breakpoint is still set with the ordinary supported form such as
Class.method - once execution pauses, the debugger works the same way as for code defined directly at the REPL
The line-qualified forms Class.method:42 and file.nex:42 are accepted by the debugger command parser. If you experiment with them, use :where after any successful pause to confirm the exact source and line shape the debugger is reporting.
D.7 Hit-Frequency Controls
Breakpoints can be tuned:
:ignore <id> <n>
:every <id> <n>
Use these when a loop or frequently called routine hits too often to inspect comfortably.
D.8 Saving and Scripting Debug State
Persist breakpoints and watchpoints:
:breaksave path/to/debug_state.edn
:breakload path/to/debug_state.edn
Drive the debugger from a command file:
:debugscript path/to/commands.dbg
:debugscript status
:debugscript off
D.9 Limits to Remember
- Stepping is statement-level, not expression-level.
- Line-specific breakpoints such as
file:lineandClass.method:lineare accepted syntactically, but they are not yet reliable enough to be the primary REPL debugging workflow. - Standalone functions do not currently have a dedicated named breakpoint form such as
:break foo; use the syntheticname_Function.callNform instead. - Breakpoints are session-local unless saved.
:print <expr>runs in the paused context and may have side effects.
D.10 Further Reading
For the complete command set and current implementation notes, see docs/md/DEBUGGER.md.