Skip to content

Show types, whole objects and Set Value in the debugger's Variables - #123

Merged
jgfoster merged 1 commit into
mainfrom
eric/inspector
Oct 13, 2026
Merged

jgfoster merged 1 commit into
mainfrom
eric/inspector

Conversation

@ericwinger

Copy link
Copy Markdown
Member

Why

When a run stops at breakpoint(), the Variables pane is where a Python developer looks first, and GemDB's showed less than VS Code's Python debugger or PyCharm:

  • no type on a row;
  • an object opened only on its own __dict__, with a class burying its methods among __module__, __dict__ and the other dunders;
  • nothing could be changed.

This is a first step toward making a paused program's objects easy to inspect, and it's confined to the Variables pane. Evaluating expressions (the Debug Console, watches, hover) and an inspector view of its own are later steps.

What

Types. Every row leads with its type, and a container's with its size, as PyCharm shows them:

  • {Point} Point(3, 4), {int} 7, {list: 3} [...];
  • {list: 150} for a container too big to show.

Copy Value copies the value alone. The adapter declares supportsClipboardContext, and VS Code's Copy Value then asks it, through evaluate in the clipboard context, what to copy. The adapter answers by taking the badge off the row's text, without a query. Every other evaluate is still refused.

Whole objects. An object opens on everything dir() lists:

  • its own attributes, its class's, its properties (read through their getters) and its __slots__;
  • dunders folded under special variables and methods under function variables, as VS Code's Python debugger does;
  • an instance whose data all comes from its class opens too, while a function or an empty object stays a leaf.

Along with it:

  • Modules now land under module variables. Grail gives each module a class of its own, so the old type-name test never caught one.
  • A getter that runs past the two-second budget reads <getter took too long>, and no further getters run, so one slow property can't hold up the pause.

Set Value. Right-click a local, a global, an attribute, a list item or a dict entry. The value is a Python expression:

  • it can use the notebook's globals, or __main__'s for a .py run;
  • on a Locals row, the frame's locals sit on top, so a local shadows a global of the same name.

A local is written back into the paused frame, so the run continues with it. This uses GsProcess>>_frameAt:tempAt:put:, because _frameAt:tempNamed:put: fails on a local a block closes over, which lives in a VariableContext.

What can't be changed:

  • A statement such as n = 5, and an empty value, are refused. Unwrapped, Grail would run n = 5 as a module and store None.
  • A method's receiver (self, or whatever the def calls it), a tuple's items, group rows and a saved stack's values are marked read-only, so VS Code doesn't offer Set Value on them.
  • Python's own refusals, such as a property with no setter, come back with Python's reason.

Worth checking

  • Listing speed. Whether a row can open is still decided by the old cheap check: one __dict__ load, plus one __slots__ load when there's no __dict__. The full dir() runs only when a row is opened.
    • An earlier version called dir() on every row, and a page of 500 objects took about 980 ms. This version measured 13–14 ms in the gem against 13 for main, timed inside the query.
    • An instance whose data all comes from its class is found by a check cached per class per query.
    • The first object opened in a session takes about 200 ms, while Grail warms up dir().
  • Grail-private selectors. Set Value writes a local into the frame at the levels the stack walk recorded (indexOfIdentical: against ___framesAndLevelsOfSuspendedProcess___'s levels). The listing reads __dir__ and ___pyAttrLoad___:. docs/grail.md records what was measured, including two Grail facts the tests now depend on:
    • a classmethod's cls never appears in Locals;
    • a plain function's parameter spelled self is hidden by ___isInternalTempName___.
  • Native code. The frame write was tested only on a local gem, which runs with GemNativeCodeEnabled=0; CI runs 2. The Grail locals test notes the same gap for reading, and CI is where this gets its first native-code run.

Testing

  • 16 new tests.
    • 7 in breakpoint.test.ts, against a real 4.0.0.a4 stone:
      • object shape;
      • Set Value on each kind of row;
      • writes into an outer frame, into a closed-over local and inside try:;
      • receivers are read-only;
      • tuple and namedtuple items are read-only;
      • the globals in Set Value;
      • slow getters.
    • 9 unit tests in debugger.test.ts.
  • Each test was seen failing on the code before its fix.
  • npm test (769 passed), breakpoint.test.ts (22 passed), lint, format check and both typechecks all pass.
  • cli.test.ts and repl.test.ts fail on my machine with or without this change. They drive out/gemdb-shell.js as a process.

🤖 Generated with Claude Code

Every Variables row now leads with its type, and a container's with its
size, as PyCharm shows them ({Point} Point(3, 4), {list: 3} [...]). Copy
Value still copies the value alone: the adapter answers VS Code's
clipboard evaluate with the badge taken off.

An object opens on everything dir() lists - its own attributes, its
class's, properties and __slots__ - with dunders folded under special
variables and methods under function variables. dir() runs only when a
row is opened, so listing a page costs what it did before. An instance
whose data all comes from its class opens too; functions and empty
objects stay leaves. Modules now land under module variables, and a
getter that runs past the time budget reads <getter took too long>
instead of holding up the pause.

Set Value changes a local, a global, an attribute, a list item or a dict
entry. The value is a Python expression that can use the notebook's (or
__main__'s) globals, with a frame's locals on top. A local is written
back into the paused frame with GsProcess>>_frameAt:tempAt:put:, so the
run continues with it. Statements and empty values are refused, and a
method's receiver, a tuple's items, group rows and saved stacks are
read-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ericwinger
ericwinger requested a review from jgfoster October 12, 2026 21:41
@jgfoster
jgfoster added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 4b7f9d8 Oct 13, 2026
5 checks passed
@jgfoster
jgfoster deleted the eric/inspector branch October 13, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants