From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-09-10T07:50:39+00:00 Subject: [ruby-core:126639] [Ruby Bug#22302] Prism and parse.y disagree on the `:line` event for a bare `nil` method body Issue #22302 has been updated by Eregon (Benoit Daloze). I have tried to make Prism match parse.y, but after doing so I think that makes little sense: * I think this is a parse.y issue, parse.y treats an explicit `nil` like an implicit `nil` and drops it entirely from the AST (no body, `body: nil`): ```ruby irb(main):001' RubyVM::AbstractSyntaxTree.parse('def m irb(main):002' nil irb(main):003> end') => (SCOPE@1:0-3:3 tbl: [] args: nil body: (DEFN@1:0-3:3 mid: :m body: (SCOPE@1:0-3:3 tbl: [] args: (ARGS@1:5-1:5 pre_num: 0 pre_init: nil opt: nil first_post: nil post_num: 0 post_init: nil rest: nil kw: nil kwrest: nil block: nil) body: nil))) irb(main):004' RubyVM::AbstractSyntaxTree.parse('def m irb(main):005' true irb(main):006> end') => (SCOPE@1:0-3:3 tbl: [] args: nil body: (DEFN@1:0-3:3 mid: :m body: (SCOPE@1:0-3:3 tbl: [] args: (ARGS@1:5-1:5 pre_num: 0 pre_init: nil opt: nil first_post: nil post_num: 0 post_init: nil rest: nil kw: nil kwrest: nil block: nil) body: (TRUE@2:2-2:6)))) ``` * This also means line Coverage is missing the `nil` the user wrote, that seems clearly undesirable, for cases like: ```ruby def m nil end ``` See `Instruction sequences` above, there is anyway a `putnil` so it should be attributed to the user `nil`. and ```ruby def foo(expected, actual) if expected == actual nil elsif ... ... ``` * `prism_compile.c` would need to do extra work just to mimic parse.y: https://github.com/ruby/ruby/compare/master...eregon:ruby:prism-tail-nil-line-event * newline marking in Prism (available for both Ruby & Java) would need to do extra work just to mimic parse.y: https://github.com/ruby/prism/commit/f6b413ec4bac4f908a26c921893545baef6e1827. That means extra subtree walks, which is not OK performance-wise at least for TruffleRuby & JRuby line marking. Claude's reasoning and repro follows: I think Prism is correct here and parse.y has the bug: the `nil` line *is* executed, so it should produce a `:line` event and be counted by line coverage. Line coverage makes this concrete. With: ```ruby # body.rb def m nil end m ``` ```ruby require "coverage" Coverage.start(lines: true) load "./body.rb" p Coverage.result.find { |k,| k.end_with?("body.rb") }.last[:lines] ``` ``` $ ruby --parser=prism run.rb [1, 1, nil, 1] $ ruby --parser=parse.y run.rb [1, nil, nil, 1] ``` Under Prism, line 2 is reported as executed (`1`); under parse.y it is reported as non-executable (`nil`), even though calling `m` runs `putnil` on line 2. So parse.y under-counts coverage and a debugger cannot break on that line. The same happens for a `nil` reached through a conditional branch that is the method's value, e.g. `def m(x); if x; nil; end; end` ��� Prism records line coverage for the `nil`, parse.y does not. The cause is that parse.y eliminates the trailing `nil` at the AST level, so the information that line 2 is executable is simply lost: ``` $ ruby --parser=parse.y -e 'pp RubyVM::AbstractSyntaxTree.parse("def m\n nil\nend")' (SCOPE@1:0-3:3 ... (DEFN@1:0-3:3 mid: :m body: (SCOPE@1:0-3:3 tbl: [] args: (...) body: nil))) ``` The method body is `nil` (empty), so parse.y compiles a synthetic `putnil` that it attributes to the definition line rather than to the `nil`'s own line. Prism keeps the explicit `NilNode` and attributes execution to the correct line. So rather than changing Prism to drop the event, I'd suggest treating this as a parse.y bug (a missing `:line` event / coverage entry for an executed line). Prism's behavior is the more useful one for TracePoint, coverage, and debuggers. ---------------------------------------- Bug #22302: Prism and parse.y disagree on the `:line` event for a bare `nil` method body https://bugs.ruby-lang.org/issues/22302#change-118894 * Author: Eregon (Benoit Daloze) * Status: Open * ruby -v: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## 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. ```ruby def m nil end ``` - With `--parser=prism`, executing `m` (or setting a `TracePoint(: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 ```ruby 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")) ``` ``` $ ruby --parser=prism repro.rb [1, 2] $ ruby --parser=parse.y repro.rb [1] ``` ### Runtime `:line` events ```ruby 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 ``` ``` $ ruby --parser=prism rt.rb [1, 2, 4] $ ruby --parser=parse.y rt.rb [1, 4] ``` 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 `nil` at 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 `nil` is reached through the branch of a conditional, e.g. a method whose body is `if 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 ``` ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux] ``` Also reproduced on ruby 3.4.0 with `--parser=prism` / `--parser=parse.y`. (found in https://github.com/ruby/prism/pull/4223) -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/