Bug #22235
closednode_id/location for constant assignment failing while reading
Description
namespace = Module.new
begin
namespace::NotDefined += 1
rescue => e
pp e.backtrace_locations[0]
pp e.backtrace_locations[0].source_range
pp e.backtrace_locations[0].syntax_tree
end
On CRuby master it gives:
$ ruby -v const_assignment_fail_while_reading.rb
ruby 4.1.0dev (2026-08-08T08:04:35Z master e56f452e46) +PRISM [x86_64-linux]
"const_assignment_fail_while_reading.rb:3:in '<main>'"
#<Ruby::SourceRange /.../const_assignment_fail_while_reading.rb:(3,2)-(3,28)>
@ ConstantPathOperatorWriteNode (location: (3,2)-(3,28))
├── flags: newline
├── target:
│ @ ConstantPathNode (location: (3,2)-(3,23))
│ ├── flags: ∅
│ ├── parent:
│ │ @ LocalVariableReadNode (location: (3,2)-(3,11))
│ │ ├── flags: ∅
│ │ ├── name: :namespace
│ │ └── depth: 0
│ ├── name: :NotDefined
│ ├── delimiter_loc: (3,11)-(3,13) = "::"
│ └── name_loc: (3,13)-(3,23) = "NotDefined"
├── binary_operator_loc: (3,24)-(3,26) = "+="
├── value:
│ @ IntegerNode (location: (3,27)-(3,28))
│ ├── flags: static_literal, decimal
│ └── value: 1
└── binary_operator: :+
So even though it's the read failing, it returns the ConstantPathOperatorWriteNode and the section is namespace::NotDefined += 1.
I'm implementing source_range & syntax_tree on TruffleRuby and there the result is:
"const_assignment_fail_while_reading.rb:3:in 'Module#const_missing'"
#<Ruby::SourceRange /.../const_assignment_fail_while_reading.rb:(3,2)-(3,23)>
@ ConstantPathNode (location: (3,2)-(3,23))
...
So this returns the ConstantPathNode and the section is namespace::NotDefined.
I think TruffleRuby is correct here, especially if we want to differentiate failing while reading or writing.
On CRuby it is currently impossible to differentiate because the same node is returned in both cases.
(UPDATE: it's possible to differentiate via the exception, but that seems quite brittle. The point is syntax_tree&source_range should point to the relevant reading node)
More information:
irb(main):001> namespace = Module.new
irb(main):002> namespace::NotDefined += 1
irb(main):003> e=_
irb(main):005> l=e.backtrace_locations[0]
irb(main):008> puts RubyVM::InstructionSequence.of(l).disasm
== disasm: #<ISeq:<compiled>@(irb):2 (2,0)-(2,26)>
0000 getlocal_WC_1 namespace@0 ( 2)[Li]
0002 dup
0003 putobject true
0005 getconstant :NotDefined
0007 putobject_INT2FIX_1_
0008 opt_plus <calldata!mid:+, argc:1, FCALL|ARGS_SIMPLE>[CcCr]
0010 swap
0011 topn 1
0013 swap
0014 setconstant :NotDefined
0016 leave
irb(main):011> RubyVM::InstructionSequence.of(l).to_a
=>
["YARVInstructionSequence/SimpleDataFormat",
4,
1,
1,
{arg_size: 0,
local_size: 0,
stack_max: 3,
node_id: 6,
source_hash: 614720579064667961,
code_location: [2, 0, 2, 26],
node_ids: [2, 5, 5, 5, 4, 5, 5, 5, 5, 5, 6],
parser: :prism},
"<compiled>",
"(irb)",
nil,
2,
:eval,
[],
{},
[],
[2,
:RUBY_EVENT_LINE,
[:getlocal_WC_1, 3],
[:dup],
[:putobject, true],
[:getconstant, :NotDefined],
[:putobject_INT2FIX_1_],
[:opt_plus, {mid: :+, flag: 20, orig_argc: 1}],
[:swap],
[:topn, 1],
[:swap],
[:setconstant, :NotDefined],
[:leave]]]
irb(main):013> l.syntax_tree
=>
@ ConstantPathOperatorWriteNode (location: (1,0)-(1,26))
...
The failing instruction must be getconstant (BTW, do we have any API to get the insn/insn index for an exception?).
That's given node_id 5 which is the ConstantPathOperatorWriteNode.
OK to fix the node_id of getconstant in such a case to point to the ConstantPathNode?