Skip to content

Commit 64f508a

Browse files
Support cause: in Thread#raise and Fiber#raise. (ruby#13967)
* Add support for `cause:` argument to `Fiber#raise` and `Thread#raise`. The implementation behaviour is consistent with `Kernel#raise` and `Exception#initialize` methods, allowing the `cause:` argument to be passed to `Fiber#raise` and `Thread#raise`. This change ensures that the `cause:` argument is handled correctly, providing a more consistent and expected behavior when raising exceptions in fibers and threads. [Feature #21360] * Shared specs for Fiber/Thread/Kernel raise. --------- Co-authored-by: Samuel Williams <samuel.williams@shopify.com>
1 parent 2e0a782 commit 64f508a

12 files changed

Lines changed: 451 additions & 75 deletions

File tree

NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ Note: We're only listing outstanding class updates.
104104
* Update Unicode to Version 16.0.0 and Emoji Version 16.0.
105105
[[Feature #19908]][[Feature #20724]] (also applies to Regexp)
106106

107+
* Thread
108+
109+
* Introduce support for `Thread#raise(cause:)` argument similar to
110+
`Kernel#raise`. [[Feature #21360]]
111+
112+
* Fiber
113+
114+
* Introduce support for `Fiber#raise(cause:)` argument similar to
115+
`Kernel#raise`. [[Feature #21360]]
116+
107117
* Fiber::Scheduler
108118

109119
* Introduce `Fiber::Scheduler#fiber_interrupt` to interrupt a fiber with a
@@ -243,3 +253,4 @@ The following bundled gems are updated.
243253
[Feature #21262]: https://bugs.ruby-lang.org/issues/21262
244254
[Feature #21287]: https://bugs.ruby-lang.org/issues/21287
245255
[Feature #21347]: https://bugs.ruby-lang.org/issues/21347
256+
[Feature #21360]: https://bugs.ruby-lang.org/issues/21360

common.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4094,6 +4094,7 @@ cont.$(OBJEXT): $(top_srcdir)/internal/basic_operators.h
40944094
cont.$(OBJEXT): $(top_srcdir)/internal/compilers.h
40954095
cont.$(OBJEXT): $(top_srcdir)/internal/cont.h
40964096
cont.$(OBJEXT): $(top_srcdir)/internal/error.h
4097+
cont.$(OBJEXT): $(top_srcdir)/internal/eval.h
40974098
cont.$(OBJEXT): $(top_srcdir)/internal/gc.h
40984099
cont.$(OBJEXT): $(top_srcdir)/internal/imemo.h
40994100
cont.$(OBJEXT): $(top_srcdir)/internal/namespace.h
@@ -19292,6 +19293,7 @@ thread.$(OBJEXT): $(top_srcdir)/internal/class.h
1929219293
thread.$(OBJEXT): $(top_srcdir)/internal/compilers.h
1929319294
thread.$(OBJEXT): $(top_srcdir)/internal/cont.h
1929419295
thread.$(OBJEXT): $(top_srcdir)/internal/error.h
19296+
thread.$(OBJEXT): $(top_srcdir)/internal/eval.h
1929519297
thread.$(OBJEXT): $(top_srcdir)/internal/gc.h
1929619298
thread.$(OBJEXT): $(top_srcdir)/internal/hash.h
1929719299
thread.$(OBJEXT): $(top_srcdir)/internal/imemo.h

cont.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ extern int madvise(caddr_t, size_t, int);
3030
#include "internal/cont.h"
3131
#include "internal/thread.h"
3232
#include "internal/error.h"
33+
#include "internal/eval.h"
3334
#include "internal/gc.h"
3435
#include "internal/proc.h"
3536
#include "internal/sanitizers.h"
@@ -3218,9 +3219,9 @@ fiber_raise(rb_fiber_t *fiber, VALUE exception)
32183219
}
32193220

32203221
VALUE
3221-
rb_fiber_raise(VALUE fiber, int argc, const VALUE *argv)
3222+
rb_fiber_raise(VALUE fiber, int argc, VALUE *argv)
32223223
{
3223-
VALUE exception = rb_make_exception(argc, argv);
3224+
VALUE exception = rb_exception_setup(argc, argv);
32243225

32253226
return fiber_raise(fiber_ptr(fiber), exception);
32263227
}

eval.c

Lines changed: 117 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -703,49 +703,142 @@ rb_interrupt(void)
703703
rb_exc_raise(rb_exc_new(rb_eInterrupt, 0, 0));
704704
}
705705

706-
enum {raise_opt_cause, raise_max_opt}; /*< \private */
707-
708706
static int
709-
extract_raise_opts(int argc, VALUE *argv, VALUE *opts)
707+
extract_raise_options(int argc, VALUE *argv, VALUE *cause)
710708
{
711-
int i;
709+
// Keyword arguments:
710+
static ID keywords[1] = {0};
711+
if (!keywords[0]) {
712+
CONST_ID(keywords[0], "cause");
713+
}
714+
712715
if (argc > 0) {
713-
VALUE opt;
714-
argc = rb_scan_args(argc, argv, "*:", NULL, &opt);
715-
if (!NIL_P(opt)) {
716-
if (!RHASH_EMPTY_P(opt)) {
717-
ID keywords[1];
718-
CONST_ID(keywords[0], "cause");
719-
rb_get_kwargs(opt, keywords, 0, -1-raise_max_opt, opts);
720-
if (!RHASH_EMPTY_P(opt)) argv[argc++] = opt;
721-
return argc;
716+
VALUE options;
717+
argc = rb_scan_args(argc, argv, "*:", NULL, &options);
718+
719+
if (!NIL_P(options)) {
720+
if (!RHASH_EMPTY_P(options)) {
721+
// Extract optional cause keyword argument, leaving any other options alone:
722+
rb_get_kwargs(options, keywords, 0, -2, cause);
723+
724+
// If there were any other options, add them back to the arguments:
725+
if (!RHASH_EMPTY_P(options)) argv[argc++] = options;
722726
}
723727
}
724728
}
725-
for (i = 0; i < raise_max_opt; ++i) {
726-
opts[i] = Qundef;
727-
}
729+
728730
return argc;
729731
}
730732

733+
/**
734+
* Complete exception setup for cross-context raises (Thread#raise, Fiber#raise).
735+
* Handles keyword extraction, validation, exception creation, and cause assignment.
736+
*
737+
* @param[in] argc Number of arguments
738+
* @param[in] argv Argument array (will be modified for keyword extraction)
739+
* @return Prepared exception object with cause applied
740+
*/
741+
VALUE
742+
rb_exception_setup(int argc, VALUE *argv)
743+
{
744+
rb_execution_context_t *ec = GET_EC();
745+
746+
// Extract cause keyword argument:
747+
VALUE cause = Qundef;
748+
argc = extract_raise_options(argc, argv, &cause);
749+
750+
// Validate cause-only case:
751+
if (argc == 0 && !UNDEF_P(cause)) {
752+
rb_raise(rb_eArgError, "only cause is given with no arguments");
753+
}
754+
755+
// Create exception:
756+
VALUE exception;
757+
if (argc == 0) {
758+
exception = rb_exc_new(rb_eRuntimeError, 0, 0);
759+
}
760+
else {
761+
exception = rb_make_exception(argc, argv);
762+
}
763+
764+
VALUE resolved_cause = Qnil;
765+
766+
// Resolve cause with validation:
767+
if (UNDEF_P(cause)) {
768+
// No explicit cause - use automatic cause chaining from calling context:
769+
resolved_cause = rb_ec_get_errinfo(ec);
770+
771+
// Prevent self-referential cause (e.g. `raise $!`):
772+
if (resolved_cause == exception) {
773+
resolved_cause = Qnil;
774+
}
775+
}
776+
else if (NIL_P(cause)) {
777+
// Explicit nil cause - prevent chaining:
778+
resolved_cause = Qnil;
779+
}
780+
else {
781+
// Explicit cause - validate and assign:
782+
if (!rb_obj_is_kind_of(cause, rb_eException)) {
783+
rb_raise(rb_eTypeError, "exception object expected");
784+
}
785+
786+
if (cause == exception) {
787+
// Prevent self-referential cause (e.g. `raise error, cause: error`) - although I'm not sure this is good behaviour, it's inherited from `Kernel#raise`.
788+
resolved_cause = Qnil;
789+
}
790+
else {
791+
// Check for circular causes:
792+
VALUE current_cause = cause;
793+
while (!NIL_P(current_cause)) {
794+
// We guarantee that the cause chain is always terminated. Then, creating an exception with an existing cause is not circular as long as exception is not an existing cause of any other exception.
795+
if (current_cause == exception) {
796+
rb_raise(rb_eArgError, "circular causes");
797+
}
798+
if (THROW_DATA_P(current_cause)) {
799+
break;
800+
}
801+
current_cause = rb_attr_get(current_cause, id_cause);
802+
}
803+
resolved_cause = cause;
804+
}
805+
}
806+
807+
// Apply cause to exception object (duplicate if frozen):
808+
if (!UNDEF_P(resolved_cause)) {
809+
if (OBJ_FROZEN(exception)) {
810+
exception = rb_obj_dup(exception);
811+
}
812+
rb_ivar_set(exception, id_cause, resolved_cause);
813+
}
814+
815+
return exception;
816+
}
817+
731818
VALUE
732819
rb_f_raise(int argc, VALUE *argv)
733820
{
734-
VALUE err;
735-
VALUE opts[raise_max_opt], *const cause = &opts[raise_opt_cause];
821+
VALUE cause = Qundef;
822+
argc = extract_raise_options(argc, argv, &cause);
736823

737-
argc = extract_raise_opts(argc, argv, opts);
824+
VALUE exception;
825+
826+
// Bare re-raise case:
738827
if (argc == 0) {
739-
if (!UNDEF_P(*cause)) {
828+
// Cause was extracted, but no arguments were provided:
829+
if (!UNDEF_P(cause)) {
740830
rb_raise(rb_eArgError, "only cause is given with no arguments");
741831
}
742-
err = get_errinfo();
743-
if (!NIL_P(err)) {
832+
833+
// Otherwise, re-raise the current exception:
834+
exception = get_errinfo();
835+
if (!NIL_P(exception)) {
744836
argc = 1;
745-
argv = &err;
837+
argv = &exception;
746838
}
747839
}
748-
rb_raise_jump(rb_make_exception(argc, argv), *cause);
840+
841+
rb_raise_jump(rb_make_exception(argc, argv), cause);
749842

750843
UNREACHABLE_RETURN(Qnil);
751844
}

include/ruby/internal/intern/cont.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ VALUE rb_fiber_transfer_kw(VALUE fiber, int argc, const VALUE *argv, int kw_spla
275275
* @exception rb_eFiberError `fiber` is terminated etc.
276276
* @return (See rb_fiber_resume() for details)
277277
*/
278-
VALUE rb_fiber_raise(VALUE fiber, int argc, const VALUE *argv);
278+
VALUE rb_fiber_raise(VALUE fiber, int argc, VALUE *argv);
279279

280280
RBIMPL_SYMBOL_EXPORT_END()
281281

internal/eval.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extern ID ruby_static_id_status;
2626
VALUE rb_refinement_module_get_refined_class(VALUE module);
2727
void rb_class_modify_check(VALUE);
2828
NORETURN(VALUE rb_f_raise(int argc, VALUE *argv));
29+
VALUE rb_exception_setup(int argc, VALUE *argv);
2930
void rb_refinement_setup(struct rb_refinements_data *data, VALUE module, VALUE klass);
3031
void rb_vm_using_module(VALUE module);
3132
VALUE rb_top_main_class(const char *method);

spec/ruby/core/fiber/fixtures/classes.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
module FiberSpecs
22

33
class NewFiberToRaise
4-
def self.raise(*args)
5-
fiber = Fiber.new { Fiber.yield }
4+
def self.raise(*args, **kwargs, &block)
5+
fiber = Fiber.new do
6+
if block_given?
7+
block.call do
8+
Fiber.yield
9+
end
10+
else
11+
Fiber.yield
12+
end
13+
end
14+
615
fiber.resume
7-
fiber.raise(*args)
16+
17+
fiber.raise(*args, **kwargs)
818
end
919
end
1020

spec/ruby/core/fiber/raise_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
describe "Fiber#raise" do
66
it_behaves_like :kernel_raise, :raise, FiberSpecs::NewFiberToRaise
7+
it_behaves_like :kernel_raise_across_contexts, :raise, FiberSpecs::NewFiberToRaise
78
end
89

910
describe "Fiber#raise" do

spec/ruby/core/thread/fixtures/classes.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ def initialize(*args)
66
end
77
end
88

9+
class NewThreadToRaise
10+
def self.raise(*args, **kwargs, &block)
11+
thread = Thread.new do
12+
Thread.current.report_on_exception = false
13+
14+
if block_given?
15+
block.call do
16+
sleep
17+
end
18+
else
19+
sleep
20+
end
21+
end
22+
23+
Thread.pass until thread.stop?
24+
25+
thread.raise(*args, **kwargs)
26+
27+
thread.join
28+
ensure
29+
thread.kill if thread.alive?
30+
end
31+
end
32+
933
class Status
1034
attr_reader :thread, :inspect, :status, :to_s
1135
def initialize(thread)

spec/ruby/core/thread/raise_spec.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
require_relative '../../shared/kernel/raise'
44

55
describe "Thread#raise" do
6+
it_behaves_like :kernel_raise, :raise, ThreadSpecs::NewThreadToRaise
7+
it_behaves_like :kernel_raise_across_contexts, :raise, ThreadSpecs::NewThreadToRaise
8+
69
it "ignores dead threads and returns nil" do
710
t = Thread.new { :dead }
811
Thread.pass while t.alive?

0 commit comments

Comments
 (0)