Bug #22302
openPrism and parse.y disagree on the `:line` event for a bare `nil` method body
Description
Summary¶
The Prism compiler and the parse.y compiler produce a different set of :line trace points (line events) for a bare nil literal that is the value-producing body of a method. Prism emits a line event on the nil's own line; parse.y does not.
- With
--parser=prism, executingm(or setting aTracePoint(:line)/ coverage / a debugger breakpoint) reports a line event on line 2. - With
--parser=parse.y, there is no line event on line 2.
Reproduction¶
Compile-time line events¶
def line_events(iseq)
events = iseq.trace_points.select { |_, event| event == :line }.map(&:first)
iseq.each_child { |child| events.concat(line_events(child)) }
events.sort
end
p line_events(RubyVM::InstructionSequence.compile("def m\n nil\nend\n"))
Runtime :line events¶
seen = []
TracePoint.new(:line) { |tp| seen << tp.lineno if tp.path.end_with?("body.rb") }.enable do
File.write("body.rb", "def m\n nil\nend\nm\n")
load "./body.rb"
end
p seen.sort
Under Prism a line event fires on line 2 (the nil) when m is called; under parse.y it does not.
Instruction sequences¶
The method body iseq differs only in the line attributed to the putnil, and whether it carries the line event ([Li]):
# --parser=parse.y
0000 putnil ( 1)[Ca]
0001 leave ( 3)[Re]
# --parser=prism
0000 putnil ( 2)[LiCa]
0001 leave ( 3)[Re]
parse.y attributes the method's implicit-nil putnil to the definition line (1) with only the RUBY_EVENT_CALL flag, so no line event is emitted for line 2.
Prism attributes the putnil to the nil's own line (2) and emits a line event there.
Scope of the difference¶
The difference is specific to a bare nil in method (def/defs) tail position. Other cases behave the same under both parsers:
- Other literals in the same position emit a line event under both parsers (
def m; true; end,def m; 1; end,def m; :a; end,def m; "s"; end). - A bare
nilat the top level, or as the body of a block or lambda, emits a line event under both parsers. - The failing shape also occurs when the tail
nilis reached through the branch of a conditional, e.g. a method whose body isif cond; nil; elsif ...; end.
The behavior is consistent across Ruby versions: parse.y has never emitted the line event for this case (checked back to 2.3), and Prism has emitted it since its compiler could be selected. This is a standing difference between the two compilers rather than a recent change; it only became visible by default when Prism became the default parser.
Note on the cause¶
This difference stems from void-expression elimination: parse.y treats the trailing nil as the method's implicit nil return and elides it (compiling a synthetic putnil with no line event), whereas Prism keeps it as an explicit value expression that carries a line event.
It is not ideal that whether a :line event is emitted depends on whether an expression is eliminated as void, but that coupling is already the established behavior in both compilers — an unused bare literal in non-tail statement position (e.g. 1 or :a followed by another statement) likewise loses its line event under both parsers — so this is not unique to the case here, only the one place the two happen to disagree.
Question¶
Which behavior is intended — should a bare nil in method tail position emit a :line event or not? Currently the two parsers disagree, which affects TracePoint(:line), line coverage, and debuggers depending on which parser compiled the code.
Environment¶
Also reproduced on ruby 3.4.0 with --parser=prism / --parser=parse.y.
(found in https://github.com/ruby/prism/pull/4223)
No data to display