8. Expression evaluation

Watch, hover and the Debug Console share one Pascal expression evaluator. It is a real evaluator, not a variable lookup with arithmetic bolted on.

The grammar

CategorySupported
Identifiers Qualified identifiers, field and property access, array and open-array indexing
Arithmetic + - * /, div, mod, shl, shr, unary -. Mixed integer and float operands promote to Double.
Logic and, or, xor, not — logical for Boolean operands, bitwise for integer ones, as in Delphi
Comparison The full set, on every type that supports it
Literals True, False, nil, numbers, strings, and bare enum literals — Mode = wmPaused needs no qualifier
Strings Concatenation with +. This allocates a real string in the debuggee.
Sets Set algebra with +, - and *
Types Type casts with the correct semantics per case, and is / as, driven by RTTI
Intrinsics Length, SizeOf, Ord, Low, High, Assigned, Pred, Succ, Abs, Chr, Trunc, Round, Int, Frac, Copy, Pos, UpperCase, LowerCase
Addresses Unary @ — useful for feeding an address to a data breakpoint or the memory view

Operator precedence is Delphi’s, not C’s: and binds like a multiplication and or / xor like an addition, both tighter than any comparison. So Flags and MASK = 0 means what it means in your source, and a > 1 and b < 2 needs its parentheses here exactly as it does in the compiler.

Strings compare by their characters, ordinally, across all the string types and against Char. A nil string reads as empty, and comparing a string against something that is not text is refused with a reason rather than quietly compared as a number.

Intrinsics the debugger cannot provide

The eighteen listed above are the ones that exist. The rest of Delphi’s built-ins are compiler magic with no symbol in the binary to call, so the debugger cannot run them — but it says so by name instead of reporting an unresolved symbol, which would read as though your variable were wrong:

<Format is a Delphi compiler intrinsic, not a function in the binary,
 so the debugger cannot call it: it takes an open array of const, which
 cannot be built from outside the process -- concatenate with + instead>

Twenty-five are answered this way, each with the reason and, where one exists, the thing to write instead — Inc(X) tells you to write X + 1, Include points at x in S, Addr at @X. Several are refused on principle rather than for want of a symbol: New, Dispose, SetLength and Halt would allocate, free, mutate or end the debuggee, and evaluating a watch expression must not do any of that.

Calling methods and getters

Method calls and property getters in an expression execute in the debuggee. The stopped thread is hijacked to run them, and any state they change persists — a later evaluation sees the result of an earlier one. That is what makes it possible to ask a live object a question rather than reconstructing the answer from its fields.

It also means an expression can have side effects. Evaluating something that increments a counter increments the counter, for real.

If the call raises, or triggers an access violation, it is aborted cleanly: the thread context is restored and the evaluation returns a failure string. A broken expression does not take the session with it.

Hovering, and what exactly gets evaluated

Resting the mouse over code while stopped shows the value, as you would expect. What is worth knowing is how much of the line it takes. The identifier under the cursor is grown outwards into a whole Pascal expression — through . for a field path, through […] for an index, through ^ for a dereference — so hovering anywhere in Customer.Orders[3].Total evaluates the whole thing, not the one word you happened to be over.

And if you select text first, the selection wins. Select any expression, hover inside the selection, and that is what is evaluated. This is the Delphi IDE behaviour, and it is here on purpose — but nothing in the interface advertises it, so it is worth knowing it exists.

Hovering also evaluates, with no confirmation and no safelist — see chapter 5. On a codebase with side-effecting getters, this is the one place the debugger will run your code without being asked to.

Where the output goes

Three streams, deliberately kept apart, because mixing them is how you end up unable to tell what your program printed from what the debugger printed.

StreamDefault destination
The debuggee’s own output Debug Console
Debugger-generated output — notably logpoint text Debug Console, as a separate channel from the above
The debugger’s own diagnostics A separate “Delphi Debugger” channel in the Output panel, not the Debug Console. Change with diagnosticsLocation.
The Output panel, channel selector set to Delphi Debugger, showing diagnostics: MAP loading, launch, setBreakpoints per source file, configurationDone, module symbol status for kernel32.dll, ntdll.dll, kernelbase.dll, ucrtbase.dll and dbghelp.dll.
The Delphi Debugger channel, already selected — the dropdown itself is not shown open here, just the channel it names. A Delphi developer has no reason to know the Output panel exists at all, let alone that it carries this.

When something is wrong with the adapter

Set "diagnosticLog": true in the launch configuration, or the DAP_LOG=1 environment variable, and the adapter writes a verbose log to:

%TEMP%\dap_adapter.log

It is genuinely verbose and off by default for that reason, but it is the file to attach to a bug report, and the place where quietly-dropped things — a malformed exception rule, for instance — actually get reported.