A debugging middle layer for the CPyte compiler (WEW). It turns an ordinary AOT-compiled CPyte binary into an AST-native debuggee so you can step over statements and expressions of the source program — not raw machine instructions — on macOS (lldb), Linux (ptrace), and Windows (Debug API).
The pipeline keeps the debug info inside the binary:
.cpy ──▶ debug_build() ──▶ .dbg (binary with cpdbd_ tracer stubs, boxes)
build_ast_model() ──▶ AST model (mirrors runtime node_nids)
build_sidecar() ──▶ .cpdb.json metadata (functions, steps, boxes, source)
│
binary + model + sidecar ──▶ CPDBSession ──▶ backend ∈ {lldb, ptrace, win}
# CLI (build, launch under lldb, drive with commands):
PYTHONPATH=$PWD python -m cpdb.cli examples/fib.cpy
# Same session, non-interactive:
python -m cpdb.cli examples/fib.cpy --commands "n;n;s;n;v;c" --quit-exit
# GUI (tkinter, Thonny-style):
python -m cpdb.gui examples/fib.cpypython -m cpdb.cli [options] file.cpy
| Flag | Meaning |
|---|---|
-o/--out PATH |
output binary/asset directory (default .cache) |
-O/--opt N |
opt level passed to debug_build |
--no-gc, --no-userspace |
build flags |
--no-build |
skip build; use --binary + existing sidecar |
--binary PATH |
run a pre-built binary |
-x/--commands "a;b;c" |
run a semicolon-separated command script, then exit |
--quit-exit |
exit 0 as soon as the target exits |
--echo-stops |
print every stop in the command script |
--input LINE |
queue a stdin line for the program (repeatable) |
REPL commands: n/<enter> step over · s step into · u step out ·
c/r continue · b [file:]LINE breakpoint · d ID delete breakpoint ·
on ID/off ID enable/disable breakpoint · heap/hp object-model view ·
v variables · p NAME print value · bt/w backtrace/stack ·
tree recursion tree · in TEXT feed a stdin line · l list ·
? help · q quit.
--input LINE may be repeated to pre-queue stdin lines before the program
starts.
While paused, the session exposes the whole process, not just the current statement:
- Call stack —
session.call_stack()runsthread backtraceand folds the pending tracer nid into the next user frame, dropping dyld/runtime frames.parse_backtraceturns the raw lines intoCallFrame(depth, function, nid). - Recursion tree —
session.recursion_tree()returns the current call path as nodes keyed(function, depth)withrec_depth(consecutive frames of this function) and cumulativevisits. Useful to see how deep a recursive call is and how many times each frame was entered. - Variable changes —
read_locals()diffs against the last read per function and tags eachDebugVariablenew/changed/same; changed values carry their previous value so you can watch state mutate between steps. - Insight — the detailed trail that plain variable reads cannot show:
session.variable_history(fn)lists every value a name ever held (reusing a name accumulates instead of overwriting),decision_trail()logs each valued step (if/while/fortruth, assignments) with its node id, andcall_ledger()/function_ledger()track enter/return transitions (including recursion) per function, with the final frame unwind recorded at exit. The GUI Insight panel does this each stop; the CLI hasinsight/ih.
Programs that call input() still work. Type a line in the GUI Input box
(or pass --input LINE / use in LINE in the CLI) — the line is queued and
flushed into the inferior's stdin (a file the session owns) right before the
next run/continue. input() past the end of the flushed data returns its
EOF default (0 / empty) — it never blocks, so nothing ever stalls.
python -m cpdb.gui file.cpy opens a tkinter app with the source view
(current statement highlighted, breakpoint markers), a Call Stack and
Recursion Tree panel, a variables tree that highlights new/changed values
(green new, yellow changed, italic arguments), a Watch Expressions
panel (re-evaluated every stop against the current frame's locals, so names
like n + 1 or len(s) > 3 work), an Insight panel with the per-name
reuse history, decision trail and call/return ledger, a breakpoints editor, a
program-input box, and a program-output console.
Source-pane gestures: click a line to toggle a breakpoint there, or
double-click to run-to-cursor (resume to that line, then stops). Clicking
a Call Stack row shows that frame's locals in the Variables panel — for
recursive frames the per-function debug box forces a "live innermost copy"
note (see AGENTS.md). The GUI drives the exact same CPDBSession as the CLI,
so stepping behavior is identical.
| Platform | Backend | Mechanism | Status |
|---|---|---|---|
| macOS | lldb |
pipe to lldb MI; temp AST breakpoints by symbol |
working (end-to-end, tested) |
| Linux | ptrace |
ctypes PTRACE_TRACEME/GETREGSET; 0xCC / BRK#0 traps; ELF symbol parsing for PIE bias; /proc/<pid>/mem reads |
implemented; parser unit-tested (needs Linux host to run) |
| Windows | win |
CreateProcessW(...DEBUG_ONLY_THIS_PROCESS), WaitForDebugEvent/ContinueDebugEvent, OutputDebugString capture |
lifecycle implemented; breakpoint machinery stubbed (NotImplementedError) |
cpdb.backends.base.factory() selects by sys.platform.
cpdb.values.decode_value turns a live box slot (an alloca address recorded
in the sidecar as type/size) into a readable DebugValue:
- integer-like types (i8/i32/int/long/i64/double) → signed number
- string types → bounded, NUL-terminated C string read via the backend
- dynamic variables → best-effort string/pointer render
- anything else → hex pointer
PYTHONPATH=$PWD python -m pytest tests -qPure unit tests run anywhere (values, ELF parser, GUI helpers). End-to-end tests build a real binary and drive lldb; they auto-skip where lldb is not available.
cpdb/
session.py CPDBSession: step engine, breakpoints, locals, plan/schedule
traverse.py TraversalEngine: AST walking, current statement, rdzv table
astmodel.py Python mirror of the compiled AST (nid -> node/func)
compiler.py debug_build(), Sidecar serialization (functions/steps/boxes/source)
values.py decode_value()
cli.py / gui/ product surface
backends/ lldb.py / ptrace.py / win.py / base.py (DebugBackend ABC)
tests/ unit + end-to-end suites
examples/ fib.cpy, loops.cpy, hello.cpy
Note: the compiler-side debug metadata emitter lives in the WEW source tree
(cpyte/bytecoding.py, tracer contract cpdbd_*); this repo consumes it.