debugger: conditional breakpoints, hit counts and logpoints #37
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
torb debugaccepts the three breakpoint refinements of the Debug Adapter Protocol, so that VS Code (and any DAP client) offers Add Conditional Breakpoint (expression, hit count) and Add Logpoint in the gutter of a.trbfile and they work in the VM:condition): the program stops at the breakpoint only when the condition is true.hitCondition): the program stops only on the hit(s) the hit count selects.logMessage): the program does not stop; a message with the values of expressions is written to the debug console.Design record
Seed impact
None. Everything is TorbScript in
compiler/src/debugger/(adapter.trb,debuggee.trb,evaluate.trb) andcompiler/src/vm/debug.trb, written in forms the seed already compiles. No syntax change, no new native or kernel operation (runtime/debug.cuntouched), no driver flag, no std name looked up by string. So no two-commit seed rule and no seed refresh.Size
One PR, two Conventional Commits:
feat(debugger): conditions, hit counts and logpoints on breakpoints- code, the three new sessions, updated.expectedfiles, unit tests.docs(debugger): conditional breakpoints, hit counts and logpoints- DEBUGGER.md,docs/tooling/torb-debug.md, regeneratedskills/.Estimate: about 300 lines of TorbScript plus tests. Independent of the open evaluate-as-snippet and
setVariableitems of section 20 (the condition grammar below stays inside the restricted evaluator).Specification
All of it is decided; the reasoning is in the loop's decision comment.
Capabilities (
adapter.trb,capabilities()):supportsConditionalBreakpoints: true, plussupportsHitConditionalBreakpoints: trueandsupportsLogPoints: true. Theinitializeline of everytests/debug/*.expectedchanges accordingly.setBreakpointsreadscondition,hitConditionandlogMessageof each source breakpoint, keeps them next to the bound statements, and answers each breakpoint with anid. AconditionorhitConditionthat does not parse is answeredverified: falsewithmessagesaying why; it binds nothing. One source breakpoint has one hit counter, however many chunks its line binds to.Condition = an expression of section 12 extended to a Bool-valued expression, evaluated read-only over the innermost frame at the breakpoint (no code runs, nothing is allocated in the program):
name,.field,.0,[3],["key"]) and literals (42,-1,2.5,"text",'a',true,false);==,!=,<,<=,>,>=between two operands of the same kind (integers with integers, floats with floats,StringandCharwith their own kind,Boolwith==/!=only),!,&&,||, parentheses; a bare Bool-valued operand is a condition on its own.Hit count (
hitCondition):N(stop on the Nth hit, same as== N),== N,>= N,> N,<= N,< N,% N(every Nth hit). A hit is a time the breakpoint's statement is reached with its condition true. The counter starts at 0 atlaunch, survives a re-sentsetBreakpointsof the same file for a breakpoint whose line, condition, hit condition and log message are unchanged, and restarts from 0 when any of them changed.Logpoint (
logMessage): the text with{expression}replaced by the shown value of a section 12 expression (the value asvariablesshows it);\{and\}are literal braces. Condition and hit count are applied first; if they select the hit, the message plus a line feed goes out as a DAPoutputevent, categoryconsole, withsourceandlineof the breakpoint; the program does not stop. An expression that cannot be evaluated is written<error: why>. A logpoint never produces astoppedevent.Order at a hit (
reachStatement,compiler/src/vm/debug.trb): pause / stop-on-entry first (unchanged); then, if the statement has a set bit, the VM asks the host (new callback field onDebugger, read through a binding likestop) whether the breakpoint stops; the host answers stop (reasonbreakpoint,hitBreakpointIdsin thestoppedevent) or go on. Go on behaves exactly like no breakpoint at that statement, so a step in progress may still end there with reasonstep. A program without breakpoints, and the statement path of any statement without a set bit, are unchanged; no new work in the loop of a program that is not debugged.Condition error: the program stops at the breakpoint (reason
breakpoint) and anoutputevent, categoryconsole, saysBreakpoint condition '<condition>' at <file>:<line> could not be evaluated: <why>.End of input:
runToTheEndalso drops the breakpoint metadata, so nothing is evaluated or logged once the adapter said nothing more comes.Acceptance criteria
initializeanswerssupportsConditionalBreakpoints,supportsHitConditionalBreakpointsandsupportsLogPointsastrue; the five existingtests/debug/*.expectedare updated for exactly that change (by hand: the worker does not bootstrap; CI runssh tools/debug.sh).tests/debug/condition(.dap,.expected, program dir): a breakpoint in a loop withconditioni == 3stops exactly once withi= 3 in the locals; a condition on a field (point.x > 1 && label == "a"); a condition with an unknown name stops and reports the condition error asoutput; a condition that is not a Bool stops and reports; an unparseable condition is answeredverified: falsewith a message.tests/debug/hit-count:hitCondition3stops on the third hit only;% 2stops on every second;>= 4stops from the fourth on; a condition and a hit count together count only hits where the condition holds; an invalid hit condition is answeredverified: false; re-sending the file's breakpoints unchanged keeps the count, a changed one restarts it.tests/debug/logpoint: a logpoint in a loop writes one consoleoutputline per hit with the interpolated values (and\{), the program runs to its end without astoppedevent; a logpoint with a condition logs only where it holds; an unevaluable expression in the message is written as<error: ...>.stoppedevents of a breakpoint carryhitBreakpointIdswith the idssetBreakpointsanswered. A step in progress still stops (reasonstep) at a statement whose condition was false.compiler/tests/debugger.test.trbfor the condition parser and evaluator (each operator, precedence, kinds, errors), the hit condition parser and the log message splitter.tools/debug-cost.sh(tier B) shows no change beyond the runner's noise in the "A breakpoint set" column; the statement path ofreachStatementgains no work for a statement without a set bit (section 19 stays true).docs/tooling/torb-debug.md(requests table, example) is updated;skills/regenerated (docs skill --checkstays green).aandbgreen in the forge CI of the PR (tools/debug.shis a gate of tier A; the debugged conformance run of tier B is unaffected).Dependencies
None. The snippet evaluator and
setVariableof section 20 stay separate and may later widen the condition language.Moved from the planning lists of the docs; the design record stays the specification. Refined by the loop, decisions in the comment below.
[loop:torbscript] intake: in scope (author orion, label loop). Implements DEBUGGER.md section 20 (conditional breakpoints, hit counts, logpoints over DAP); the design record is the specification, no collision with CONCEPT "Open Questions". Conditions reuse the evaluate path of section 12, so it likely shares work with the evaluate-as-snippet issue. Touches the debugger and its
tests/debug/sessions, seed impact to be confirmed in refine. next: refine (queued)[loop:torbscript] refine: still queued behind the intake order. next: waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
[loop:torbscript] claim refine by session:loop-torbscript-20261014 until 2026-10-14T00:48Z
[loop:torbscript] decided by the loop: the open points of DEBUGGER.md section 20 for this item are technical, so no owner question.
!,&&,||, parentheses and literals. Why: a bare section 12 expression can only name a Bool, but the main use (i == 3,point.x > 1) needs a comparison; it stays read-only (no code runs, nothing allocates), so it needs neither the snippet evaluator nor a seed change. Kinds must match, mixed kinds are an error rather than a silent coercion.hitConditionasN,== N,>= N,> N,<= N,< N,% N; plainNmeans== N. Counted only on hits whose condition holds (the usual debugger meaning); one counter per source breakpoint across chunks; kept across a re-sentsetBreakpointswhen the breakpoint is unchanged, because VS Code re-sends a whole file on every edit and would otherwise reset the counts.{expression}interpolation with\{\}escapes (the editors' convention), output event categoryconsolewith source and line, never stops; an unevaluable expression shows<error: why>so one bad expression does not drop the whole line.Debugger, read through a binding likestop), the host decides stop or go on. Frames are read only on such a hit, so a statement without a set bit and a program without breakpoints do no new work. Measured expectation: none to measure at the statement path;tools/debug-cost.sh"A breakpoint set" (breakpoint on a line that never runs) must stay within the runner's noise (section 19: 1.17 to 1.20 s fibonacci, 1.32 s loops best/median on the forge's runner) - checked in tier B CI, not locally.step.compiler/src/debugger/andcompiler/src/vm/debug.trb; no syntax, native, kernel operation, driver flag or std name looked up by string.[loop:torbscript] refine: issue rewritten to the definition of ready (goal, specification, 9 testable criteria, DEBUGGER.md sections 7/12/20, seed impact none, one PR in two commits); technical questions decided above, no owner question. Tokens: about 85k (sonnet, refine worker). next: implement
[loop:torbscript] claim implement by session:loop-torbscript-20261015 until 2026-10-15T04:00Z
[loop:torbscript] implement: conditions, hit conditions and logpoints on breakpoints are in PR #367 (2 feature/docs commits plus 5 test-fix commits that the maintainer may squash into the feat commit before landing; branch loop/37-conditional-breakpoints). Forge CI is green (gates incl. the three new debug sessions condition/hit-count/logpoint and the new unit tests). Unit tests read conditions against a small program without lists: a list constant in the debug-lowered unit-test VM run hits an internal error ("the VM reached code the IR says nothing reaches"), so item access (
xs[i]) is evaluated by the code but not covered by a unit test - worth a separate issue. Tokens used: roughly 4M. next: review[loop:torbscript] close: landed by the merge gate as
ce7af1f420(PR #367); issue closed. Follow-up for the uncovered list item access filed as a newloopissue. next: done