summaryrefslogtreecommitdiff
path: root/eval.c
diff options
context:
space:
mode:
authorSamuel Williams <samuel.williams@oriontransfer.co.nz>2025-07-24 14:45:43 +1200
committerGitHub <noreply@github.com>2025-07-24 14:45:43 +1200
commit64f508ade8c8535b7d3ecdd217886aa52fddd43c (patch)
tree6acdef44dba44edc023070790190c2f2ebbafee3 /eval.c
parent2e0a782936608bee757c07b9578cfa6885009fa4 (diff)
Support `cause:` in `Thread#raise` and `Fiber#raise`. (#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>
Diffstat (limited to 'eval.c')
-rw-r--r--eval.c141
1 files changed, 117 insertions, 24 deletions
diff --git a/eval.c b/eval.c
index 74f31db5c0..019a2d19a2 100644
--- a/eval.c
+++ b/eval.c
@@ -703,49 +703,142 @@ rb_interrupt(void)
rb_exc_raise(rb_exc_new(rb_eInterrupt, 0, 0));
}
-enum {raise_opt_cause, raise_max_opt}; /*< \private */
-
static int
-extract_raise_opts(int argc, VALUE *argv, VALUE *opts)
+extract_raise_options(int argc, VALUE *argv, VALUE *cause)
{
- int i;
+ // Keyword arguments:
+ static ID keywords[1] = {0};
+ if (!keywords[0]) {
+ CONST_ID(keywords[0], "cause");
+ }
+
if (argc > 0) {
- VALUE opt;
- argc = rb_scan_args(argc, argv, "*:", NULL, &opt);
- if (!NIL_P(opt)) {
- if (!RHASH_EMPTY_P(opt)) {
- ID keywords[1];
- CONST_ID(keywords[0], "cause");
- rb_get_kwargs(opt, keywords, 0, -1-raise_max_opt, opts);
- if (!RHASH_EMPTY_P(opt)) argv[argc++] = opt;
- return argc;
+ VALUE options;
+ argc = rb_scan_args(argc, argv, "*:", NULL, &options);
+
+ if (!NIL_P(options)) {
+ if (!RHASH_EMPTY_P(options)) {
+ // Extract optional cause keyword argument, leaving any other options alone:
+ rb_get_kwargs(options, keywords, 0, -2, cause);
+
+ // If there were any other options, add them back to the arguments:
+ if (!RHASH_EMPTY_P(options)) argv[argc++] = options;
}
}
}
- for (i = 0; i < raise_max_opt; ++i) {
- opts[i] = Qundef;
- }
+
return argc;
}
+/**
+ * Complete exception setup for cross-context raises (Thread#raise, Fiber#raise).
+ * Handles keyword extraction, validation, exception creation, and cause assignment.
+ *
+ * @param[in] argc Number of arguments
+ * @param[in] argv Argument array (will be modified for keyword extraction)
+ * @return Prepared exception object with cause applied
+ */
+VALUE
+rb_exception_setup(int argc, VALUE *argv)
+{
+ rb_execution_context_t *ec = GET_EC();
+
+ // Extract cause keyword argument:
+ VALUE cause = Qundef;
+ argc = extract_raise_options(argc, argv, &cause);
+
+ // Validate cause-only case:
+ if (argc == 0 && !UNDEF_P(cause)) {
+ rb_raise(rb_eArgError, "only cause is given with no arguments");
+ }
+
+ // Create exception:
+ VALUE exception;
+ if (argc == 0) {
+ exception = rb_exc_new(rb_eRuntimeError, 0, 0);
+ }
+ else {
+ exception = rb_make_exception(argc, argv);
+ }
+
+ VALUE resolved_cause = Qnil;
+
+ // Resolve cause with validation:
+ if (UNDEF_P(cause)) {
+ // No explicit cause - use automatic cause chaining from calling context:
+ resolved_cause = rb_ec_get_errinfo(ec);
+
+ // Prevent self-referential cause (e.g. `raise $!`):
+ if (resolved_cause == exception) {
+ resolved_cause = Qnil;
+ }
+ }
+ else if (NIL_P(cause)) {
+ // Explicit nil cause - prevent chaining:
+ resolved_cause = Qnil;
+ }
+ else {
+ // Explicit cause - validate and assign:
+ if (!rb_obj_is_kind_of(cause, rb_eException)) {
+ rb_raise(rb_eTypeError, "exception object expected");
+ }
+
+ if (cause == exception) {
+ // 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`.
+ resolved_cause = Qnil;
+ }
+ else {
+ // Check for circular causes:
+ VALUE current_cause = cause;
+ while (!NIL_P(current_cause)) {
+ // 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.
+ if (current_cause == exception) {
+ rb_raise(rb_eArgError, "circular causes");
+ }
+ if (THROW_DATA_P(current_cause)) {
+ break;
+ }
+ current_cause = rb_attr_get(current_cause, id_cause);
+ }
+ resolved_cause = cause;
+ }
+ }
+
+ // Apply cause to exception object (duplicate if frozen):
+ if (!UNDEF_P(resolved_cause)) {
+ if (OBJ_FROZEN(exception)) {
+ exception = rb_obj_dup(exception);
+ }
+ rb_ivar_set(exception, id_cause, resolved_cause);
+ }
+
+ return exception;
+}
+
VALUE
rb_f_raise(int argc, VALUE *argv)
{
- VALUE err;
- VALUE opts[raise_max_opt], *const cause = &opts[raise_opt_cause];
+ VALUE cause = Qundef;
+ argc = extract_raise_options(argc, argv, &cause);
- argc = extract_raise_opts(argc, argv, opts);
+ VALUE exception;
+
+ // Bare re-raise case:
if (argc == 0) {
- if (!UNDEF_P(*cause)) {
+ // Cause was extracted, but no arguments were provided:
+ if (!UNDEF_P(cause)) {
rb_raise(rb_eArgError, "only cause is given with no arguments");
}
- err = get_errinfo();
- if (!NIL_P(err)) {
+
+ // Otherwise, re-raise the current exception:
+ exception = get_errinfo();
+ if (!NIL_P(exception)) {
argc = 1;
- argv = &err;
+ argv = &exception;
}
}
- rb_raise_jump(rb_make_exception(argc, argv), *cause);
+
+ rb_raise_jump(rb_make_exception(argc, argv), cause);
UNREACHABLE_RETURN(Qnil);
}