Repository navigation
ast: record the line a block's opener sits on - #46
Merged
Merged
Conversation
A Block had no line of its own. Program.Lines stamps STATEMENT nodes, and a
Block is not one -- it is not even a Node, being a field of Call -- so a
consumer had to take the line of whatever statement enclosed the block. That
is the same line only when the block opens on that statement's first line.
Measured against ruby 4.0.5, which reports the opener's line as a block's
location.first_lineno:
h = { # line 1
:a => proc { # line 2
1
},
:b => proc { 2 }, # line 5
:c => proc { # line 6
},
}
h[:a].source_location[1] # ruby 2 from the statement, 1
h[:b].source_location[1] # ruby 5 from the statement, 1
h[:c].source_location[1] # ruby 6 from the statement, 1
The body's first line is NOT a substitute, which is why this needs the
opener and not a cheaper approximation: ruby answers 2 for :a, whose first
statement is on line 3, and :c has no body statement at all.
So Block carries a Line field rather than a position field being added to all
55 node types, and rather than Program.Lines being stretched to keys that are
not Nodes. 0 means "not recorded".
Three sites set it: the `{` of parseBraceBlock, the `do` of parseDoBlock, and
the `->` of parseLambda -- the arrow itself, since that is the line ruby
reports for an arrow lambda (measured; they coincide in all but deliberately
odd layouts).
Observable downstream as Proc#source_location, Method#source_location for a
define_method, and the line a Proc prints when it inspects itself.
Also gofmt'd paren_singleton_def_test.go, which was unformatted on main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
to go-embedded-ruby/ruby
that referenced
this pull request
Oct 4, 2026
…statement's (#763) A block ISeq's FirstLine is MRI's location.first_lineno -- the line the block's `{`, `do` or `->` sits on, which Ruby shows through Proc#source_location. The compiler took it from the enclosing scope's current line (c.push seeds firstLine from the parent's curLine), and that is the same answer only when the block opens on the enclosing statement's first line. So every block inside a multi-line literal reported the literal's OPENING line: h = { # line 1 :a => proc { # line 2 1 }, :b => proc { 2 }, # line 5 :c => proc { # line 6 }, } h[:a].source_location[1] ruby 4.0.5 2 before 1 h[:b].source_location[1] ruby 4.0.5 5 before 1 h[:c].source_location[1] ruby 4.0.5 6 before 1 and the same for a multi-line array, a `do…end`, an arrow lambda and a default in a multi-line parameter list -- 1, 8, 13 where ruby says 4, 10, 15. The body's first line would NOT have done, which is why this needed the parser and not a cheaper guess here: ruby answers 2 for :a, whose first statement is on line 3, and :c has no body statement at all. The line was not in the tree, so this could not be fixed downstream alone; parser v0.11.0 records it as ast.Block.Line (go-ruby-parser/parser#46). Block.Line == 0 means the parser recorded none -- a hand-built AST -- and the inherited line stands, which is what it always was. The control is that INSTRUCTION lines do not move. Backtraces, __LINE__ and Kernel#caller inside a block were already right, and the fix also seeds the block builder's curLine, so it could have shifted them. Measured unchanged, and pinned: for a block opening on line 2 whose one statement is on line 3, the scope's FirstLine is 2 and the instruction's line is 3. The frozen prelude was regenerated, as its drift test requires. The same `go generate ./internal/vm` also rewrites aot_e2e_gen_test.go, which has no drift test and was already stale on main by the vcall change of #742 -- see #762; its regeneration rides along here because the two generators share one directive. Closes #758 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.
What
A
Blockhad no line of its own.Program.Linesstamps statement nodes,and a
Blockis not one — it is not even aNode, being a field ofCall—so a consumer had to take the line of whatever statement enclosed the block.
That is the same line only when the block opens on that statement's first line.
Measured
ruby 4.0.5reports the opener's line as a block'slocation.first_lineno:The body's first line is not a substitute, which is why this needs the
opener rather than a cheaper approximation: ruby answers
2for:a, whosefirst statement is on line 3, and
:chas no body statement at all.Shape
Block.Line, a field — not a position field on all 55 node types, and notProgram.Linesstretched to keys that are notNodes (it cannot be:Blockdoes not implement the interface).
0means "not recorded".Three sites set it: the
{ofparseBraceBlock, thedoofparseDoBlock,and the
->ofparseLambda— the arrow itself, since that is the line rubyreports for an arrow lambda (measured; they coincide in all but deliberately
odd layouts).
The
Linesdoc comment now records this as its one deliberate exception, withthe measurement that forced it.
Why it matters downstream
It is observable as
Proc#source_location,Method#source_locationfor adefine_method, and the line aProcprints when it inspects itself. In rbgothe block ISeq's
FirstLinewas taken from the enclosing scope's current line,so
proc { }inside any multi-line literal reported the literal's opening line—
1,8,13where ruby says4,10,15. That cannot be fixeddownstream without this, because the information was not in the tree.
Green
go test ./... -count=1exit 0 (4 packages),go vetclean,gofmtclean,and the
parserpackage stays at 100.0% of statements.Five new cases, each
wantbeing ruby 4.0.5's ownProc#source_location: themulti-line hash (the discriminating one — body-first-line would give 3, 6, 7),
a multi-line array with
do…end, the arrow lambda, a single-line hash as thecontrol that nothing moved, and a method call whose
dolands on a later linethan its receiver.
Also
gofmt'dparen_singleton_def_test.go, which was unformatted onmain.🤖 Generated with Claude Code