A bulleted docstring moved the reported frame line - #656
Merged
Conversation
`___derivePythonLineForMethod___:ip:` and its span companion find the Python
line by locating the caret `GsNMethod >> _sourceAtIp:` inserts, then taking the
last ``___curPos___ := N'' at or above it. The caret is marked
* ^1 *******
and both scans identified it as "the first line whose first non-blank character
is an asterisk".
That is not sufficient. A Python DOCSTRING is emitted as a MULTI-LINE Smalltalk
string literal, so its own lines land in the generated source verbatim, and a
bullet list in one is indistinguishable from the marker:
___curPos___ := #(5 4 5 5 ' """Summary line.').
'Summary line.
* first bullet
'.
___curPos___ := #(11 8 nil 8 ' a = 1').
Since the scan takes the FIRST match, a bullet ABOVE the real caret wins, the
caret is located too early, and the line reported comes from higher up the
function. Measured deterministically: a four-line function with a bulleted
docstring reported 40 where CPython reports 46.
___isCaretLine___ now requires ``^'' followed by a digit after the asterisk --
the marker always carries the ip, and prose does not.
COMMENTS ARE NOT AFFECTED, and that asymmetry is why this took a docstring to
find: a Python comment never reaches the generated Smalltalk, while a string
literal does. Grail's own hand-written Smalltalk comments use the same bullet
style -- 11 of 1060 probed methods have such a line -- so this is ordinary
input rather than a contrived one.
It is the DETERMINISTIC half of the intermittent frame-walk family: when the
misplaced caret lands above EVERY ___curPos___ the scan answers nil rather than
a wrong number, and a nil DROPS the frame, which is how sys._getframe comes to
raise ``call stack is not deep enough''.
Pinned by tests/python/frame_line_bulleted_docstring.py -- 5 checks, expected
values CPython 3.14.6's, of which 3 flip when the fix is reverted; the 2 that
hold are the controls (a plain docstring, and a bulleted COMMENT). Driven from
TracebackTestCase rather than a new class on purpose: a new class would move the
class->shard mapping, and this family is sensitive to which classes share a
session.
It does NOT close the intermittent failures. docs/Issues.md records what the new
test saw when it flaked -- ``got (46, 108) want (46, 77)'', where the fixture is
100 lines long, so 108 is not a line in it at all -- together with the two
candidate mechanisms and the measurement that argues against the first of them.
Gates (tier 2 -- BaseException.gs is on every raise and every traceback):
* CPython suite: 0 regressions, 0 improvements; scoreboard byte-identical.
* SUnit: 5670 / 5670, 0 failed, 0 errors.
* fixture gate: 207 files, 2728 OK, 34 XFAIL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Continues the frame-walk work after #653. That PR fixed a transient source-read fault; this fixes a deterministic wrong-line bug in the same machinery, found by re-measuring the reproduction from #651 against post-#653
main.The bug
___derivePythonLineForMethod___:ip:(and its span companion) find the Python line by locating the caretGsNMethod >> _sourceAtIp:inserts, then taking the last___curPos___ := Nat or above it. The caret is markedand both scans identified it as "the first line whose first non-blank character is an asterisk".
That is not sufficient — because a Python docstring is emitted as a multi-line Smalltalk string literal, so its own lines land in the generated source verbatim:
A bullet is indistinguishable from the marker, the scan takes the first match, so a bullet above the real caret wins and the caret is located too early. The line then comes from higher up the function.
___isCaretLine___now requires^followed by a digit after the asterisk — the marker always carries the ip, prose does not.Comments are not affected, and that asymmetry is why this took a docstring to find. A Python comment never reaches the generated Smalltalk; only the one-line source excerpt inside each position literal does. Grail's own hand-written Smalltalk comments use the same bullet style — 11 of 1060 probed methods have such a line — so this is ordinary input, not contrived.
Why it belongs to the flake family
This is the deterministic half of the intermittent frame-walk failures. When the misplaced caret lands above every
___curPos___, the scan answersnilrather than a wrong number — and anildrops the frame, which is exactly howsys._getframe()comes to raiseValueError: call stack is not deep enough. One cause, two symptoms, which is part of why the family looked like two unrelated bugs.It does not close the intermittent failures — and here is the new evidence
Measured with
GRAIL_TEST_SHARDS="0 1" ./scripts/run_tests.sh, the whole-suite reproduction from #651:mainbefore #653mainafter #653GeneratorStackFrameTestCase)So #653 helped materially and something remains.
The residual bug now has a much sharper probe. The new test asserts exact line numbers and reports the wrong value, where the older tests only report that something differed. When it flaked:
The inner frame is right; the outer frame reported
108. The fixture file is 100 lines long — so 108 is not a line in it at all, which rules out a mis-scan of that method's own source, caret or otherwise, and says the number came from somewhere else.Two candidates, written up in
docs/Issues.md, neither confirmed:aMethod asOopwith no liveness guarantee (GrailIpLineCache,GrailIpSpanCache,GrailFnNameCache,GrailSoleFnNameCache,GrailPyMethodCache); a recycled OOP would answer from an unrelated method, which fits "a line in an unrelated function" exactly. But I measured against it: 25 generations of loading and dropping the same fixture module produced 0 OOP collisions. Not demonstrated, so not claimed.___parsePositionLiteral___:from:' own comment records a previous bug of this exact shape (concatenating a line with an adjacent literal to derive37133718).108is what#(10 8 ...)yields if the digit scan fails to stop at the space.One thing for you to decide
My new test flakes at the same rate as the family it probes (1 in 5 under the shard split). I kept it whole rather than trimming the nested-frame assertion, because it is a correct expectation and trimming it to get a green run is the same move as widening a timing margin to suit one. It does not make CI measurably flakier — the family already failed at that rate — and when it does fire it now names the wrong value instead of shrugging.
Say the word and I will drop the
inner_callerrow to the documented-issue section instead.Gates — tier 2
BaseException.gsis on every raise and every traceback.Coordination
Another session has been investigating this family in parallel and has the deeper
frame_depth.pyevidence plus a faster repro (four concurrent sessions looping one class, ~12%/iteration, no suite). This PR deliberately does not touch what they are working on — it fixes the one deterministic defect I could prove, and hands over the sharper probe.