Debugging: DAP for the machine layer, nREPL for the language layer
Do not extend nREPL with registers -- the protocol is trivial, the implementation is a whole debugger. Emit DWARF from the LLVM backend and use lldb-dap with dape; no adapter is written. C layout with no object headers means DWARF describes Flan structs exactly, so lldb needs no plugin or formatters. Reload mostly works with DAP for free: lldb watches the loader rendezvous and re-resolves breakpoint specs against new modules. The gotcha is our own never-unload rule -- N redefinitions leave N copies all claiming the same source line, so one breakpoint resolves to N locations. Needs per-generation DWARF source identity plus a reload agent that disables superseded locations. Cuts the instrumentation-based step debugger, the most expensive piece of milestone 8. Knock-on: &env stops being urgent, and the shadow stack shrinks to serving nREPL backtraces and restart enumeration.
This commit is contained in:
parent
c64e91bdfa
commit
5a8c2327c6
69
plan.org
69
plan.org
@ -266,8 +266,9 @@ Compile-time overload resolution on argument types. No precedence lists, no meth
|
||||
combination, no MOP, no runtime dispatch table — see Types.
|
||||
|
||||
** Macros
|
||||
- Needs a ~&env~ equivalent: macros must see lexical environment (names of
|
||||
locals in scope). Required by the step debugger. Decide now, painful to retrofit.
|
||||
- ~&env~ equivalent: macros seeing the lexical environment (names of locals in
|
||||
scope). No longer urgent — its "decide now" status came from the
|
||||
instrumentation-based step debugger, which is cut (see Tooling). Milestone 5.
|
||||
- Hygiene model: open decision.
|
||||
|
||||
* Host language
|
||||
@ -542,10 +543,66 @@ Clojure-coupled) — reference it. ~clojure-mode~-derived major mode, overlay
|
||||
rendering, hydra for stepping bindings (~transient~ is the maintained
|
||||
alternative).
|
||||
|
||||
** Step debugger
|
||||
Instrumentation-based, like CIDER's — macroexpansion wraps subforms with a
|
||||
breakpoint that messages the editor and blocks. No native debug info needed.
|
||||
Limit: only instrumented code.
|
||||
** Debugging: two layers, two protocols
|
||||
Do not extend nREPL with registers or memory. The protocol is the easy part —
|
||||
adding an op is trivial; /implementing/ it means becoming a debugger (ptrace,
|
||||
trap handling, frame unwinding), duplicating an enormous existing project.
|
||||
|
||||
| Capability | DAP (lldb-dap + dape) | nREPL |
|
||||
|------------------------------------------+-----------------------+-------|
|
||||
| Registers, memory, native stack | yes | never |
|
||||
| Breakpoints, watchpoints, stepping | yes | — |
|
||||
| Eval Flan expressions with real semantics | no | yes |
|
||||
| Redefine a function and continue | no | yes |
|
||||
| Invoke a restart | no | yes |
|
||||
| Pause /before/ unwinding | no | yes |
|
||||
|
||||
Neither covers the other's column, so this is not a choice between them.
|
||||
|
||||
*DAP is nearly free.* No debug adapter is written: emit DWARF from the LLVM
|
||||
backend and point ~lldb-dap~ at the binary; dape speaks to that. lldb and gdb
|
||||
both ship DAP interfaces already.
|
||||
|
||||
This is where /no object headers/ pays off a second time. Flan structs *are* C
|
||||
structs — real machine types, no tag words, no boxing — so DWARF describes them
|
||||
with no impedance mismatch and lldb shows a ~Cursor~ correctly with no plugin
|
||||
and no formatters.
|
||||
|
||||
*** Reload and DAP
|
||||
Mostly automatic. lldb watches the loader rendezvous structure, so a ~dlopen~'d
|
||||
generation is noticed and its DWARF read; and lldb keeps breakpoints as
|
||||
unresolved /specs/, re-resolving them against newly loaded modules, so a
|
||||
breakpoint on ~sand.flan:85~ binds to the new function by itself.
|
||||
|
||||
*The gotcha is our own never-unload rule.* After N redefinitions there are N
|
||||
copies of a function, each with DWARF claiming ~sand.flan:85~, so one breakpoint
|
||||
resolves to N locations and stops in stale generations. Needs glue, and it is
|
||||
small: give each generation a distinct source identity in its DWARF
|
||||
(~sand.flan#7~), and have the reload agent disable breakpoint locations in
|
||||
superseded modules on each reload. This is the one piece that cannot just be
|
||||
wired up and left alone.
|
||||
|
||||
Not available at any price: if execution is paused /inside/ the old function,
|
||||
redefinition does not move it — register allocation belongs to the old
|
||||
compilation (see "What redefinition cannot do"). The current frame finishes in
|
||||
the old code; the next call enters the new one. Also, dape reads source from
|
||||
disk, so editing further after a generation was compiled drifts the highlighted
|
||||
line from what is executing.
|
||||
|
||||
*** Cut: the instrumentation-based step debugger
|
||||
Previously planned as CIDER's model — macroexpansion wrapping subforms with
|
||||
breakpoints. Dropped. It is the most expensive piece of milestone 8
|
||||
(instrumentation, an overlay protocol, ~&env~, a bespoke Emacs client) and
|
||||
DAP+DWARF covers most of its use. What DAP cannot cover — pausing before the
|
||||
stack unwinds with restarts available — comes from ~handler-bind~ over nREPL.
|
||||
|
||||
Consequences: *~&env~ loses its urgency* (its "decide now" status came from the
|
||||
step debugger requiring it — now an ordinary milestone-5 decision), and *the
|
||||
shadow stack shrinks* to serving only nREPL backtraces and restart enumeration.
|
||||
|
||||
Forcing the live-programming workflow into DAP would repeat the CIDER mistake in
|
||||
the other direction: DAP's model is stop/step/inspect, with no vocabulary for
|
||||
"invoke ~use-placeholder~ and resume". That stays on nREPL.
|
||||
|
||||
** Pause on exception
|
||||
Better than CIDER's, because ~handler-bind~ has not unwound the stack. Dev-mode
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user