Bug #22299
closedopt_new emits a duplicate :line trace point for Const.new following a branch
Description
Summary¶
On Ruby master (4.1.0dev), compiling an expression of the form Constant.new(...)
that is the first statement of a basic block reached after a branch (e.g. immediately
after a return unless/next unless guard) produces two :line trace points for
that one line instead of one.
The extra event only appears when the opt_new specialized instruction is generated, so it depends on:
- a constant receiver (
Foo.new,A::B.new— notfoo.new), - the method being
newwithopt_newapplicable (no block;Foo.new { }is fine), - the call sitting at the head of a basic block after a control-flow merge, and
RubyVM::InstructionSequence.compile_option[:specialized_instruction]beingtrue
(the default).
Reproduction¶
def line_events(iseq)
ev = iseq.trace_points.select { |_, e| e == :line }.map(&:first)
iseq.each_child { |c| ev.concat(line_events(c)) }
ev
end
src = <<~RUBY
return unless x
Foo.new
RUBY
[true, false].each do |spec|
RubyVM::InstructionSequence.compile_option = { specialized_instruction: spec }
events = line_events(RubyVM::InstructionSequence.compile(src))
puts "specialized_instruction: #{spec.inspect}\t=> line events #{events.inspect}"
end
Output¶
Ruby 4.1.0dev (master):
specialized_instruction: true => line events [1, 2, 2]
specialized_instruction: false => line events [1, 2]
Ruby 4.0.6 (and earlier):
specialized_instruction: true => line events [1, 2]
specialized_instruction: false => line events [1, 2]
Line 2 (Foo.new) is reported twice on master with the default compile options.
Analysis¶
With specialized_instruction: true, the opt_new codepath prepends a putnil
(reserving the slot for the object) that carries the statement's line, and the
following opt_getconstant_path also carries a :line trace point — so line 2 is
emitted twice:
0007 putnil ( 2)[Li] # statement start
0008 opt_getconstant_path <Foo>[Li] # duplicate :line on the same line
0010 opt_new <calldata!mid:new, argc:0, ARGS_SIMPLE>, 17
For comparison, the non-opt_new path (Foo.bar, or Foo.new under
specialized_instruction: false) emits the line once, on opt_getconstant_path:
The [Li] on opt_getconstant_path in the opt_new path appears to be redundant:
the putnil before it already carries the statement's line event.
Impact¶
This surfaced as a failure of test/prism/newline_test.rb in make test-all on
ruby-head in this Prism PR.
That test asserts that prism's newline flags match RubyVM's :line trace points exactly.
Two fixtures hit the pattern:
test/prism/ruby/parameters_signature_test.rb:95—object = Object.newafter
return unless comparetest/prism/locals_test.rb:101—yield ISeq.new(opnd)afternext unless ...
It is not observed when running the same test in the prism repository, because there
require "test/unit" (the test-unit gem) sets specialized_instruction: false
globally, which suppresses opt_new and therefore the duplicate event.
Updated by Eregon (Benoit Daloze) about 21 hours ago
I bisected it to https://github.com/ruby/ruby/commit/f4813a34c274a764b99d6e706dd016514b66cf45 / https://github.com/ruby/ruby/pull/18538, which added a skip for ruby/parameters_signature_test.rb.
cc @byroot (Jean Boussier)
Updated by byroot (Jean Boussier) about 8 hours ago
- Status changed from Open to Closed
Applied in changeset git|9a2b620825060d80c1bbcd42ce360d39af6ef02b.
Fix the peephole optimizer to avoid duplicated line events
[Bug #22299]
Followup: https://github.com/ruby/ruby/pull/18538
Updated by Eregon (Benoit Daloze) about 7 hours ago
- Assignee set to byroot (Jean Boussier)
- Target version set to 4.1
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: DONTNEED, 3.4: DONTNEED, 4.0: DONTNEED