Repository navigation
An internal raise carries $! as its cause, through BOTH of its doors - #907
Conversation
language/send_spec.rb: pass=75 fail=3 -> pass=77 fail=1. The two examples
"raises NameError with $! as a cause" and "raises NoMethodError with $! as a
cause" now pass; the one left is the SyntaxError for a literal block plus a
block argument, which is the parser's.
## It was never a missing feature
autoCause, applyRaiseCause, curExc, @__cause__, an explicit `cause:` and a
cycle walk were all already here, and Kernel#raise linked causes correctly. An
INTERNAL raise did not, and there are TWO doors:
raiseWithIvars builds its object up front to carry @name/@receiver/@Args
-> stamped at the RAISE, which is where MRI reads it
raise() helper panics with a RubyError STRUCT carrying no object
-> stamped where exceptionObject builds one
My first attempt stamped only the second and changed NOTHING OBSERVABLE: the
dispatcher's NameError/NoMethodError come through raiseWithIvars, so
exceptionObject returned at its `e.Obj != nil` line and the new call was
unreachable. Localised by probing three shapes rather than by reasoning:
A Kernel#raise inside a rescue cause set <- already worked
B internal raise inside a rescue cause=nil
C internal raise via a method cause=nil
A passing proved autoCause and curExc were both fine, so the fault had to be
reachability, not mechanism.
## Bibliography
exc_setup_message (eval.c at v4.0.9) takes the cause from get_ec_errinfo(ec)
when none was given; exc_setup_cause declines a self-cause (`cause != exc`) and
terminates the chain; a bare re-raise gets nil; a cycle raises ArgumentError
"circular causes". autoCause already encoded the first three.
## Measured
cmd/oracle against /opt/homebrew/opt/ruby@4.0/bin/ruby (ruby 4.0.7, +PRISM),
nine rules, three engines: the treatment AGREES byte for byte, the control
differs on exactly three -- an internal raise inside a rescue, a two-link
chain, and an internal raise inside an ensure.
## ⛔ The negative rules are the point
Stamping a cause on EVERY internal raise is how this goes wrong, and no
positive row would notice. Measured and asserted:
no enclosing rescue cause=nil
after the rescue has finished cause=nil
a bare re-raise (same exception) cause=nil (no self-cause)
an explicit cause: wins over $!
an explicit cause: nil suppresses the automatic one
## ⛔ Both stamps are load-bearing, and the partition is exact
A second stamp that changed nothing would be a rule nobody authorised, and
"both work" cannot show that. Each was disabled in turn:
without raiseWithIvars' stamp the 4 object-carrying rows FAIL, the 2
objectless ones pass
without exceptionObject's exactly the other 2 fail
Disjoint and complementary.
11 table rows in internal/vm/internal_raise_cause_test.go. go test ./... : 14
packages ok, 0 failures.
Refs #799
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The corpus sweep, as promised — and the corpus had already named this defect
⛔ The honest figure is +3, not +4
So: +2 in ⭐
|
Closes the last two
language/send_spec.rbfailures from#799that are not the parser's:pass=75 fail=3→pass=77 fail=1.⛔ It was never a missing feature — it was two unstamped doors
autoCause,applyRaiseCause,curExc,@__cause__, an explicitcause:and a cycle walk were all already here, andKernel#raiselinked causes correctly. Only an internal raise did not, and internal raises take two different routes:raiseWithIvarsNoMethodError#name,#receiver,#argsraise()helperRubyErrorstruct, no objectexceptionObjectbuilds oneMy first attempt stamped only the second and changed nothing observable. The dispatcher's
NameError/NoMethodErrorcome throughraiseWithIvars, soexceptionObjectreturned at itse.Obj != nilline and the new call was unreachable — and the comment I had written there asserted the opposite.A three-shape probe localised it in one run, rather than reasoning about it:
Kernel#raiseinside a rescuenilnilA passing is what mattered: it proved
autoCauseandcurExcwere both fine, so the fault had to be reachability, not mechanism.Bibliography
exc_setup_messagetakes the cause fromget_ec_errinfo(ec)when none was given;exc_setup_causedeclines a self-cause (cause != exc) and terminates the chain; a bare re-raise getsnil; a cycle raisesArgumentError: circular causes.autoCausealready encoded the first three — another reason this is a routing fix and not a feature.Measured
cmd/oracle, nine rules, three engines — reference, control (mainatac87a757), treatment:The control differs on exactly three: an internal raise inside a rescue, a two-link chain, and an internal raise inside an
ensure.⛔ The negative rules are the point
Stamping a cause on every internal raise is how this change goes wrong, and no positive row would notice. Measured on 4.0.7 and asserted:
nilnilnil— no self-causecause:$!cause: nil⛔ Both stamps are load-bearing, and the partition is exact
A second stamp that changed nothing would be a rule nobody authorised, and "both work" cannot show that. Each was disabled in turn against the new test:
raiseWithIvars' stampexceptionObject's stampDisjoint and complementary.
⛔ The corpus sweep is running
Every internally-raised exception inside any rescue now carries a cause — corpus-wide, and far wider than the two examples this closes.
specdelta -dir core,language -move none -timeout 5magainstac87a757is running and will be posted here with itsKILLED on timeoutcount: non-zero means the clock decided a count and the run is void.11 table rows in
internal/vm/internal_raise_cause_test.go.go test ./...: 14 packages ok, 0 failures.Refs #799
🤖 Generated with Claude Code