Project

General

Profile

Actions

Bug #22299

closed

opt_new emits a duplicate :line trace point for Const.new following a branch

Bug #22299: opt_new emits a duplicate :line trace point for Const.new following a branch

Added by Eregon (Benoit Daloze) about 21 hours ago. Updated about 7 hours ago.

Status:
Closed
Target version:
[ruby-core:126596]

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 — not foo.new),
  • the method being new with opt_new applicable (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] being true
    (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:

0007 opt_getconstant_path <Foo>(   2)[Li]
0009 opt_send_without_block <calldata!mid:bar, ...>

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:95object = Object.new after
    return unless compare
  • test/prism/locals_test.rb:101yield ISeq.new(opnd) after next 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 byroot (Jean Boussier) about 8 hours ago Actions #2

  • 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 Actions #3

  • 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
Actions

Also available in: PDF Atom