diff options
| author | Samuel Williams <samuel.williams@oriontransfer.co.nz> | 2025-07-24 14:45:43 +1200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-24 14:45:43 +1200 |
| commit | 64f508ade8c8535b7d3ecdd217886aa52fddd43c (patch) | |
| tree | 6acdef44dba44edc023070790190c2f2ebbafee3 /eval.c | |
| parent | 2e0a782936608bee757c07b9578cfa6885009fa4 (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.c | 141 |
1 files changed, 117 insertions, 24 deletions
@@ -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); } |
