11. Known limitations

What this debugger does not do. Some of these are limits of the underlying debug-info format, some are deliberate refusals to guess, and some are simply not built yet — they are marked as such, because which one it is changes what you should do about it.

Targets and processes

  • Child processes are not tracked. The debuggee is launched with DEBUG_ONLY_THIS_PROCESS; anything it spawns runs without a debugger. Attach to the child separately if you need it.
  • Locals and parameters are unavailable on optimized 32-bit builds. With -$O+ the compiler routinely omits the frame pointer and there is nothing left to resolve them against. Run control — breakpoints, stepping, call stacks — works on optimized 32-bit builds either way. Build 32-bit targets with -$O- if you want to see variables.

Symbols and sources

  • threadvar variables are not resolved, on either bitness. A threadvar has no fixed address in the image. The debugger reports this rather than displaying a wrong value.
  • Same-basename source files in different directories can resolve to the wrong file. TD32’s names table stores only the basename, with no directory, so two Oracle.pas files in different folders are indistinguishable to the debugger. This is a property of the compiler’s debug information; it cannot be fixed downstream.

Not implemented

  • Watch-panel inline editing. The DAP setExpression request is not implemented — values can be edited in the Variables panel only. See chapter 5.
  • Function breakpoints — breaking by routine name rather than by location — are not supported.
  • No “step into a specific call target” chooser when a line contains several calls.

That is the whole list. Every other DAP request the editor makes, the adapter answers — including modules, which is what the Delphi Modules view is built on.

Deliberate refusals

These look like limitations and are reported as failures, but they are the debugger declining to give you an answer it cannot prove. A wrong answer here would be worse than no answer, because nothing downstream would reveal it was wrong.

  • Stepping out of an exception is refused where the enclosing handler cannot be proven from the binary — which covers every try/finally, and every bare except on a 32-bit target. See chapter 4.
  • Instruction-granularity step out is refused without a provable return address (.pdata on x64, [EBP+4] on x86).
  • Backward disassembly is refused unless a known boundary decodes forward onto the target address exactly. See chapter 7.
  • An address breakpoint in a module that is not loaded is refused with a reason rather than planted speculatively.
  • A fifth hardware watchpoint is refused by name — there are four CPU debug registers and no software fallback. So is a read-only watch, which x86 and x64 do not implement. See chapter 3.
  • Locating an exception handler’s block by address needs .pdata, so it is x64 only — an on E: alias in a project’s .dpr main block, and a bare except’s $exception staying live past the initial raise, both report why rather than silently not appearing on x86. An alias inside an ordinary function or procedure is unaffected, on either bitness. See chapter 4.

Bounds, not bugs

Everything that renders a value is capped, because an uncapped debugger freezes on the first pathological data structure it meets. What matters is that none of these truncate silently — the total is always shown beside what was rendered:

  • 1024 children per expansion, 50 elements in a collapsed array preview.
  • 4096 characters per string.
  • 32 levels of enclosing routines when climbing nested-procedure scopes.
  • A symbol index gets a fixed slice of time per request; if it is not ready the answer comes back incomplete and fills in on the next one, rather than blocking the editor.

To see past a cap, ask for the thing directly — a watch on Arr[50000] rather than scrolling a tree to it. Full details are in chapter 5.

Things that are easy to trip over

  • Hovering evaluates, with no guard. Hovering an identifier that is a function or a getter runs it in the debuggee, with its side effects, without asking. The safelist that protects the Variables panel does not apply to hover. See chapter 5.
  • Force-killing an attached session leaves INT3 bytes behind in the target process, surfacing much later as External exception 80000003. Always detach. See chapter 10.
  • The shared exception-rules file is strict JSON. A comment or a trailing comma silently yields zero rules. See chapter 4.
  • Saving in the exception-rules editor leaves the file unsaved. It writes the rules and opens the file beside the editor; saving that file is still yours to do. See chapter 4.
  • A breakpoint condition that cannot be evaluated stops every time and reports why — including bypassing the hit count. That is intended: it is the debugger telling you the condition is broken, not the breakpoint misbehaving. See chapter 3.
  • The shipped executables are unsigned, so SmartScreen warns on first run.

The current list

This page is the user-facing subset and is written by hand, so it lags reality. The complete and current list of known issues, along with the roadmap, lives in the repository README — which is also where bug reports belong. Attach the adapter log from chapter 8 when you file one.