-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
py/objcode: Implement PEP626 to add co_lines to code objects on settrace builds. #16989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #16989 +/- ##
==========================================
+ Coverage 98.54% 98.56% +0.02%
==========================================
Files 169 169
Lines 21877 21945 +68
==========================================
+ Hits 21558 21631 +73
+ Misses 319 314 -5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Code size report:
|
Related to #16797? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks like a nice addition!
I think we should then deprecate co_lnotab
following CPython.
And it would be good to add another test to this PR, to test the actual numeric values from co_lines()
, to make sure they match the expected bytecode offsets for a simple test function (that test would need a corresponding .py.exp
file because you can't use CPython to work out the right answer).
Hmm, actually, looking at the compilation result, it looks like |
There we are, having it return the new line_info value instead of an increment value for it manages to get it fully elided: https://godbolt.org/z/bEY6ffTK8 Though, that makes it much less clear what's going on --- and there's similar differences in the other contexts with how incrementing (Also, clang gets the exact same compilation result regardless of whether or which way I refactor it.) I'll leave the PR as-is unless we really feel like it's important to turn the screws as tight as possible just to win back 2 opcodes here. |
The Was initially concerned if there was some sort of overflow making |
Signed-off-by: Anson Mansfield <[email protected]>
Signed-off-by: Anson Mansfield <[email protected]>
Signed-off-by: Anson Mansfield <[email protected]>
Signed-off-by: Anson Mansfield <[email protected]>
Signed-off-by: Anson Mansfield <[email protected]>
Signed-off-by: Anson Mansfield <[email protected]>
5c31dc4
to
c27a6ad
Compare
I've been playing around with these tests; at the moment the test I added also covers the cases where a correct The current outputs from co_lines are still meaningful and correct in some sense, though --- is that summing behavior actually something this needs to implement? Or can these extra coverage cases just be dropped (or made into cpydiffs or something)? Also, should I add coverage for a case where the table needs to skip more than 2048 lines at once? (e.g. to overflow the 11-bit line number increment in the 2-byte format, and require a multi-entry encoding for it like |
c27a6ad
to
45623a4
Compare
Ok, in the most recent variant of the test I've tried, I'm now really confused, and suspect I may have found a bug with the emitter for the underlying line info tables. The function in question, which I've annotated with what seems to be the line-number encodings, is: def f(x, y, obj, obj2, obj3):
a = x + y # line 4: bc+4 line+4
b = x - y # line 5: bc+4 line+1
# line 6
# line 7
# line 8
# line 9
# line 10
# line 11
# line 12
# line 13
# line 14
# line 15
# line 16
# line 17
# line 18
# line 19
c = a * b # line 20: bc+4 line+15
obj.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.fun() # line 21: bc+31 line+1; bc+27 line+0
# line 22
# line 23
# line 24: bc+0 line+3
# line 25
# line 26
# line 27: bc+0 line+3
# line 28
# line 29
obj2.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.fun() # line 30: bc+31 line+3; bc+27 line+0
# line 31
# line 32
# line 33: bc+0 line+3
# line 34
# line 35
# line 36
# line 37
# line 38
# line 39
# line 40
# line 41
# line 42
# line 43
# line 44
# line 45
# line 46
# line 47
# line 48
# line 49
# line 50
# line 51
# line 52
# line 53
# line 54
# line 55
# line 56
# line 57
# line 58
# line 59
obj3.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.fun() # line 60: bc+31 line+27; bc+27 line+0
return c # line 61: bc+2 line+1 I'd expect line 60 to get encoded as I've not dug in to see if this is related to the SyntaxError this causes in the unix minimal and zephyr ports, but it doesn't seem far-fetched that this might also be part of that. |
It's correct. The line number encoding for
And that decodes to the following
|
I'm not sure what you mean by "sum together multiple successive table entries"?
No, I think it's fine without that case. |
Currently, running the fun_code_colines test from c27a6ad on a build with the appropriate features enabled for it results in a test failure --- in that, the corresponding However, the output of line 4 start: 0
line 4 end: 4
line 5 start: 4
line 5 end: 8
line 20 start: 8
line 20 end: 12
line 21 start: 12
+line 21 end: 43
+line 21 start: 43
line 21 end: 70
+line 24 start: 70
+line 24 end: 70
+line 27 start: 70
+line 27 end: 70
line 30 start: 70
+line 30 end: 101
+line 30 start: 101
line 30 end: 128
+line 33 start: 128
+line 33 end: 128
line 60 start: 128
+line 60 end: 159
+line 60 start: 159
line 60 end: 186
line 61 start: 186
line 61 end: 188 There's a sense in which this output is still correct --- in that it describes the same mapping, and it's not difficult for an automated tool to accommodate this quirk. However, it would also be pretty easy to modify |
I've done some digging to see what the spec says and what CPython does here: It seems like the spurious
The rest of PEP626 / the-new-co-lines-method-of-code-objects also requires that the bytecode ranges be contiguous and in order; and that does imply that line numbers would need to therefore not be unique or in order in an implementation that does instruction re-ordering. So, on a technical level, I think the implementation does actually meet the letter of the specification. CPython's current implementation specifically checks for and combines multiple entries together in the code it uses to parse its line-number table --- but this is actually a difference at least between the Python 3.12 and 3.10 versions I checked to confirm. Running that same test case file on CPython, but with the def f(x, y, obj, obj2, obj3):
...
# ... all of the blank lines skipped ...
# line 15283
obj3().a[obj3().a0().a1().a2().a3().a4().a5().a6().a7().a8().a9]().b[obj3().b0().b1().b2().b3().b4().b5().b6().b7().b8().b9]().c[obj3().c0().c1().c2().c3().c4().c5().c6().c7().c8().c9]().d[obj3().d0().d1().d2().d3().d4().d5().d6().d7().d8().d9]().e[obj3().e0().e1().e2().e3().e4().e5().e6().e7().e8().e9]().f[obj3().f0().f1().f2().f3().f4().f5().f6().f7().f8().f9]().g[obj3().g0().g1().g2().g3().g4().g5().g6().g7().g8().g9]().h[obj3().h0().h1().h2().h3().h4().h5().h6().h7().h8().h9]().i[obj3().i0().i1().i2().i3().i4().i5().i6().i7().i8().i9]().j[obj3().j0().j1().j2().j3().j4().j5().j6().j7().j8().j9]().k[obj3().k0().k1().k2().k3().k4().k5().k6().k7().k8().k9]().l[obj3().l0().l1().l2().l3().l4().l5().l6().l7().l8().l9]().m[obj3().m0().m1().m2().m3().m4().m5().m6().m7().m8().m9]().n[obj3().n0().n1().n2().n3().n4().n5().n6().n7().n8().n9]().o[obj3().o0().o1().o2().o3().o4().o5().o6().o7().o8().o9]().p[obj3().p0().p1().p2().p3().p4().p5().p6().p7().p8().p9]().q[obj3().q0().q1().q2().q3().q4().q5().q6().q7().q8().q9]().r[obj3().r0().r1().r2().r3().r4().r5().r6().r7().r8().r9]().s[obj3().s0().s1().s2().s3().s4().s5().s6().s7().s8().s9]().t[obj3().t0().t1().t2().t3().t4().t5().t6().t7().t8().t9]().u[obj3().u0().u1().u2().u3().u4().u5().u6().u7().u8().u9]().v[obj3().v0().v1().v2().v3().v4().v5().v6().v7().v8().v9]().w[obj3().w0().w1().w2().w3().w4().w5().w6().w7().w8().w9]().x[obj3().x0().x1().x2().x3().x4().x5().x6().x7().x8().x9]().y[obj3().y0().y1().y2().y3().y4().y5().y6().y7().y8().y9]().z[obj3().z0().z1().z2().z3().z4().z5().z6().z7().z8().z9]().fun() # line 15284
return c # line 15285 On Python 3.10.18, this produces multiple entries:
But on Python 3.12.3 it produces only a single entry:
Key Question: Should this feature implement logic to (a) skip lineinfo entries for blank lines, and (b) combine multiple lineinfo entries for a single line together, like newer (but not older) versions of CPython do? Or should I modify the test to be invariant to this detail (in the way that tools that consume |
Signed-off-by: Anson Mansfield <[email protected]>
45623a4
to
74c0cc2
Compare
Summary
This PR implements PEP 626 – Precise line numbers for debugging and other tools..
This adds one method,
co_lines
, that returns an iterator over(begin, end, line_no)
tuples of line numbers for applicable bytecode ranges.Testing
This PR includes automated tests of all of the currently-implemented code object attributes, including the existing attributes that previously had no test coverage.
The actual values in
co_lines
can't be compared directly to CPython's, but all of the invariants that the method is meant to observe are checked.In addition to the github workflow checks, I've also manually verified that all tests pass when run against a unix
MICROPY_PY_SYS_SETTRACE
build. (In particular, I've checked to make sure they're not skipped).Trade-offs and Alternatives
As in CPython, this is implemented as its own distinct iterator object. Potentially, this function could be functionally replicated by instead eagerly building a list of the tuples and returning an iterator over that list, at a slightly smaller code size; but the iterator approach allows for a tighter behavioral match with CPython (
fun_code_full.py:25
), and code size is not at nearly the same premium in settrace builds.