Skip to content

Commit 0360e78

Browse files
authored
Improve SPdb (#652)
Now that we have astcompile, we can finally fix a long time bug of SPdb: until now, from the spdb prompt you could only print symbols which were already in the symtable. This means that you could access e.g. a builtin only if it was already referenced from the body of the function you were inspecting. With astcompile, now we know that we are in interactive mode and we can insert a special `ast.NameInteractive`, which then do a fully dynamic lookup in the astframe. While we are at it, we also improve/simplify how values are printed from spdb.
2 parents 86e49fc + 6bd2b89 commit 0360e78

7 files changed

Lines changed: 150 additions & 19 deletions

File tree

spy/ast.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,21 @@ class NameImportRef(Expr):
447447
sym: Symbol
448448

449449

450+
@astnode(">= astcompiled")
451+
class NameInteractive(Expr):
452+
"""
453+
A Name lookup which is resolved dynamically.
454+
455+
This is generated only by astcompile_interactive, when the name is not found in the
456+
surrounding symtable. It's mostly meant to be used by SPdb.
457+
458+
See e.g. test_astcompile::test_NameInteractive and test_spdb::test_NameInteractive
459+
"""
460+
461+
precedence = 100
462+
id: str
463+
464+
450465
@astnode(">= astcompiled")
451466
class NameError(Expr):
452467
"""

spy/astcompile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def compile_expr_Name(self, name: ast.Name) -> ast.Expr:
570570
# the SPdb prompt, compiled against the symtable of a live frame), else
571571
# it means that there is a bug in symtable.
572572
assert self.interactive, "sym not found"
573-
return ast.NameError(name.loc, name.id)
573+
return ast.NameInteractive(name.loc, name.id)
574574

575575
if sym.impref is not None:
576576
return ast.NameImportRef(name.loc, sym)

spy/tests/compiler/test_spdb.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,13 @@ def foo(x: int, session: str) -> int:
253253
| spdb_interact(session)
254254
| |____________________|
255255
(spdb) print x
256-
static type: <spy type 'i32'>
257-
dynamic type: <spy type 'i32'>
256+
type: i32
258257
41
259258
(spdb) y
260-
static type: <spy type 'i32'>
261-
dynamic type: <spy type 'i32'>
259+
type: i32
262260
42
263261
(spdb) y * 2
264-
static type: <spy type 'i32'>
265-
dynamic type: <spy type 'i32'>
262+
type: i32
266263
84
267264
(spdb) continue
268265
"""
@@ -283,8 +280,7 @@ def foo(x: int, session: str) -> None:
283280
| spdb_interact(session)
284281
| |____________________|
285282
(spdb) x
286-
static type: <spy type 'i32'>
287-
dynamic type: <spy type 'i32'>
283+
type: i32
288284
42
289285
(spdb) y
290286
*** NameError: name `y` is not defined
@@ -293,6 +289,37 @@ def foo(x: int, session: str) -> None:
293289
mod = self.compile(src)
294290
mod.foo(42, session)
295291

292+
def test_NameInteractive(self):
293+
src = """
294+
from _test import spdb_interact
295+
296+
X = 10
297+
var Y = 20
298+
299+
def foo(x: int, session: str) -> None:
300+
spdb_interact(session)
301+
"""
302+
session = f"""
303+
--- entering applevel debugger ---
304+
[0] test::foo at {self.filename}:8
305+
| spdb_interact(session)
306+
| |____________________|
307+
(spdb) X # outer direct
308+
type: i32
309+
10
310+
(spdb) Y # outer cell
311+
type: i32
312+
20
313+
(spdb) Z # not found
314+
*** NameError: name `Z` is not defined
315+
(spdb) str # builtin
316+
type: type
317+
<spy type 'str'>
318+
(spdb) continue
319+
"""
320+
mod = self.compile(src)
321+
mod.foo(42, session)
322+
296323
def test_ParseError(self):
297324
src = """
298325
from _test import spdb_interact
@@ -330,8 +357,7 @@ def foo() -> None:
330357
3 x = 1
331358
4 -> raise ValueError("hello")
332359
(spdb) x
333-
static type: <spy type 'i32'>
334-
dynamic type: <spy type 'i32'>
360+
type: i32
335361
1
336362
(spdb) continue
337363
"""

spy/tests/test_astcompile.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
import pytest
55

6+
import spy.ast as ast
67
from spy.analyze.importing import ImportAnalyzer
8+
from spy.astcompile import astcompile_interactive
79
from spy.backend.spy import AST_FORMAT, FQN_FORMAT, SPyBackend
810
from spy.fqn import FQN
11+
from spy.parser import Parser
12+
from spy.tests.test_parser import assert_node_dump
913
from spy.util import print_diff
1014
from spy.vm.function import W_ASTFunc
1115
from spy.vm.vm import SPyVM
@@ -36,6 +40,20 @@ def write_src(self, src: str) -> None:
3640
src = textwrap.dedent(src)
3741
f.write(src)
3842

43+
def compile_interactive(self, src: str) -> ast.Expr:
44+
"""
45+
Parse a single expression and astcompile it in interactive mode against the
46+
symtable of `test::foo`. This is meant to be similar to what SPdb does when
47+
evaluating interactive exprs.
48+
"""
49+
fqn = FQN("test::foo")
50+
w_foo = self.vm.globals_w[fqn]
51+
assert isinstance(w_foo, W_ASTFunc)
52+
parser = Parser(src, "<test>")
53+
stmt = parser.parse_single_stmt()
54+
assert isinstance(stmt, ast.StmtExpr)
55+
return astcompile_interactive(stmt.value, w_foo.funcdef.symtable)
56+
3957
def assert_dump(
4058
self,
4159
expected: str,
@@ -215,3 +233,24 @@ def foo(obj: dynamic, val: i32) -> None:
215233
_$aug_target0.x = _$aug_target0.x + val
216234
"""
217235
self.assert_dump(expected)
236+
237+
def test_NameInteractive(self):
238+
self.compile_src("""
239+
X = 10
240+
241+
def foo() -> None:
242+
Y = 20
243+
""")
244+
expr_X = self.compile_interactive("X")
245+
expected = """
246+
NameInteractive(id='X')
247+
"""
248+
assert_node_dump(expr_X, expected)
249+
250+
expr_Y = self.compile_interactive("Y")
251+
expected = """
252+
NameLocalDirect(
253+
sym=Symbol('Y', 'const', 'direct'),
254+
)
255+
"""
256+
assert_node_dump(expr_Y, expected)

spy/tests/test_parser.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
from spy.vm.b import B
1212

1313

14+
def assert_node_dump(node: ast.Node, expected: str):
15+
dumped = dump(node, use_colors=False, fields_to_ignore=("symtable",))
16+
dumped = dumped.strip()
17+
expected = textwrap.dedent(expected).strip()
18+
if dumped != expected:
19+
print_diff(expected, dumped, "expected", "got")
20+
pytest.fail("assert_dump failed")
21+
22+
1423
@pytest.mark.usefixtures("init")
1524
class TestParser:
1625
@pytest.fixture
@@ -30,14 +39,9 @@ def expect_errors(self, src: str, main: str, *anns: MatchAnnotation):
3039
self.parse(src)
3140

3241
def assert_dump(self, node: ast.Node, expected: str):
33-
dumped = dump(node, use_colors=False, fields_to_ignore=("symtable",))
34-
dumped = dumped.strip()
35-
expected = textwrap.dedent(expected).strip()
3642
if "{tmpdir}" in expected:
3743
expected = expected.format(tmpdir=self.tmpdir)
38-
if dumped != expected:
39-
print_diff(expected, dumped, "expected", "got")
40-
pytest.fail("assert_dump failed")
44+
assert_node_dump(node, expected)
4145

4246
def test_Module(self):
4347
src = """

spy/vm/astframe.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,47 @@ def eval_expr_NameError(self, name: ast.NameError) -> W_MetaArg:
747747
name.loc,
748748
)
749749

750+
def eval_expr_NameInteractive(self, name: ast.NameInteractive) -> W_MetaArg:
751+
# NameInteractive is generated only during interactive sessions, like SPdb. See
752+
# e.g. test_astcompile::test_NameInteractive and
753+
# test_spdb::test_NameInteractive.
754+
#
755+
# We want to lookup a name which is NOT found in the symtable. We basically need
756+
# to do at runtime usually is done at ScopeAnalyzer time:
757+
w_val: Optional[W_Object]
758+
for level in range(1, len(self.closure) + 1):
759+
outervars = self.closure[-level]
760+
lv = outervars.get(name.id)
761+
if lv is None:
762+
continue
763+
if isinstance(lv.w_val, W_Cell):
764+
w_val = lv.w_val.get()
765+
else:
766+
w_val = lv.w_val
767+
assert w_val is not None
768+
return W_MetaArg(self.vm, lv.color, lv.w_T, w_val, name.loc)
769+
770+
# name not found. Let's try the builtins
771+
sym = SymTable.from_builtins().lookup_maybe(name.id)
772+
if sym is not None:
773+
assert sym.impref is not None
774+
w_val = self.vm.lookup_ImportRef(sym.impref)
775+
if w_val is None:
776+
# this is likely a builtin which triggers an implicit import. Do the
777+
# import and redo the lookup
778+
self.vm.import_(sym.impref.modname)
779+
w_val = self.vm.lookup_ImportRef(sym.impref)
780+
assert w_val is not None
781+
w_T = self.vm.dynamic_type(w_val)
782+
return W_MetaArg(self.vm, "blue", w_T, w_val, name.loc)
783+
784+
raise SPyError.simple(
785+
"W_NameError",
786+
f"name `{name.id}` is not defined",
787+
"not found in this scope",
788+
name.loc,
789+
)
790+
750791
def eval_expr_AssignExprConstError(
751792
self, node: ast.AssignExprConstError
752793
) -> W_MetaArg:

spy/vm/debugger/spdb.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,18 @@ def print_wam(
5353
if file is None:
5454
file = sys.stdout
5555
w_T = vm.dynamic_type(wam_arg.w_val)
56+
w_static_T = wam_arg.w_static_T
5657
wam_s = vm.repr_wam(wam_arg, loc=Loc.here())
5758
s = vm.unwrap_str(wam_s.w_val)
5859
#
5960
color = ColorFormatter(use_colors=use_colors)
60-
print(color.set("green", "static type: "), wam_arg.w_static_T, file=file)
61-
print(color.set("green", "dynamic type:"), w_T, file=file)
61+
T = w_T.fqn.human_name(vm)
62+
static_T = w_static_T.fqn.human_name(vm)
63+
if w_T is w_static_T:
64+
print(color.set("green", "type:"), T, file=file)
65+
else:
66+
print(color.set("green", "static type: "), static_T, file=file)
67+
print(color.set("green", "dynamic type:"), T, file=file)
6268
print(s, file=file)
6369

6470

0 commit comments

Comments
 (0)