Skip to content

Implement exceptions for effect handlers using Callback.register_exception#11423

Merged
Octachron merged 14 commits intoocaml:trunkfrom
kayceesrk:effect_exceptions
Sep 26, 2022
Merged

Implement exceptions for effect handlers using Callback.register_exception#11423
Octachron merged 14 commits intoocaml:trunkfrom
kayceesrk:effect_exceptions

Conversation

@kayceesrk
Copy link
Copy Markdown
Contributor

This PR removes the pre-defined exceptions Unhandled and Continuation_already_taken used by effect handlers. Instead, we now declare these effects in the Effect module, and register them using Callback.register_exception to enable the runtime to raise these exceptions.

This PR also replaces the exception Unhandled with Unhandled_effect : 'a Effect.t -> exn, which carries the unhandled effect.

The PR supersedes #11419 and fixes #11185.

@dbuenzli
Copy link
Copy Markdown
Contributor

Personally I find Effect.Unhandled_effect a bit long winded. I would go for Effect.Unhandled.

(code_t)raise_unhandled_effect_code;
Closinfo_val(raise_unhandled_effect_closure) = Make_closinfo(0, 2);
raise_unhandled_effect = raise_unhandled_effect_closure;
caml_register_generational_global_root(&raise_unhandled_effect);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain to a newcomer why this black magic around raise_unhandled_effect is necessary? (The explanation could be added as a comment in the code.)

I'm not sure what parts of the bytecode are allowed to directly raise exceptions (apparently caml_raise_continuation_already_taken() below is fine, and there are existing examples with caml_raise_stack_overflow for example), and which are not / why. Here what's going on seems very complex compared to the semantics we want, or even to the handling of unhandled exceptions in

ocaml/runtime/interp.c

Lines 972 to 977 in 37df934

if (Stack_parent(domain_state->current_stack) == NULL) {
domain_state->external_raise = initial_external_raise;
domain_state->trap_sp_off = initial_trap_sp_off;
domain_state->current_stack->sp =
Stack_high(domain_state->current_stack) - initial_stack_words ;
return Make_exception_result(accu);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially, raise_unhandled_effect is necessary when we do not find a matching effect handler in our stack of handlers. The search had proceeded by checking one effect handler on the stack at a time. When we reach the bottom of the stack of effect handlers and find that there is no parent stack, we have to get the control back to the point of perform (do_resume) and then raise the exception. raise_unhandled_effect is the closure that raises the exception immediately after the control returns to the point of perform.

I'll write a clarifying comment in the code.

@dra27
Copy link
Copy Markdown
Member

dra27 commented Jul 11, 2022

I'm not sure that an example can actually be concocted which would trigger this, but it reads slightly strangely that the Effect module initialises a runtime structure (in this case the two exceptions in the named values table). In other words, is it possible to have a program which could lead to caml_raise_unhandled_effect where the exception has not been registered, thinking of full stdlib replacements, etc., or linking without -linkall. Is the registration something which ought to be in CamlinternalEffect and explicitly called as part of the startup code on the OCaml side?

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 11, 2022

Typo in the Git commit message of 37df934: "Undefined"

@xavierleroy
Copy link
Copy Markdown
Contributor

I won't review the implementation until we all agree on the interface and the documentation. +1 for @dbuenzli's suggestions, especially the suggestion to shorten Effect.Unhandled_effect to Effect.Unhandled.

!4]
!5]
Unhandled
Stdlib.Effect.Unhandled_effect(_)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (_) is unfortunate and reduces the usefulness of including the effect in the exception. It would be 100% better to also print the name of the effect ! For this we'll need some ad-hoc code in the exception printer(s), I'm afraid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I see that there is similar code in printexc.ml.

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 11, 2022

As the person who originally requested the renaming of Unhandled (unscoped) to Unhandled_effect, I am satisfied with Effect.Unhandled. 😃

@kayceesrk
Copy link
Copy Markdown
Contributor Author

Is the registration something which ought to be in CamlinternalEffect and explicitly called as part of the startup code on the OCaml side?

It would be odd to not find the exception when using caml_named_value. Currently, there is an assertion to check that the exception exists.

ocaml/runtime/fiber.c

Lines 669 to 674 in 37df934

exn = atomic_load(&caml_continuation_already_taken_exn);
if (exn == NULL) {
exn = caml_named_value("Effect.Continuation_already_taken");
CAMLassert (exn);
atomic_store(&caml_continuation_already_taken_exn, exn);
}

If we think that there is a possibility of these exceptions not being registered (with stdlib replacements), then it may be easiest to replace this assertion with a check that exn is not null and fail with a caml_fatal_error if it is null.

@Octachron
Copy link
Copy Markdown
Member

@dra27: If I am not mistaken, the exceptions can only be raised by OCaml code that links Effect, thus it seems fine to do the initialization only when linking the Effect module. If a stdlib replacement exposes the runtime effect primitive functions by itself without linking the Effect module, it should also take care to properly register the exceptions itself.

@lthls
Copy link
Copy Markdown
Contributor

lthls commented Jul 12, 2022

@dra27: If I am not mistaken, the exceptions can only be raised by OCaml code that links Effect, thus it seems fine to do the initialization only when linking the Effect module.

That's unfortunately not the case. Defining a new effect only needs to reference the interface, not the implementation, and the same applies for performing an effect (Effect.perform is an external declaration). So you can have code performing effects that has does not link with the Effect module.
Writing effect handlers currently needs to go through the implementation of Effect, but even that might change if we introduce support in the language itself.

Overall, I consider that if there is any amount of initialisation code in a module, it should be compiled with -linkall. This is annoying for Stdlib modules as that means it's almost impossible to not link them even if they're not used, so I'd advise one of the two following alternatives:

  • Switch back to builtin exceptions (you can still rebind a builtin exception in Effect, using type exn += Unhandled = Unhandled_effect for instance)
  • Make the Effect module an independent library

@kayceesrk
Copy link
Copy Markdown
Contributor Author

kayceesrk commented Jul 12, 2022

Defining a new effect only needs to reference the interface, not the implementation, and the same applies for performing an effect (Effect.perform is an external declaration). So you can have code performing effects that has does not link with the Effect module.

This seems like an adversarial case. Is it possible to write programs which will perform effects without linking to the Effect module? In other words, is this problem encountered by programmers who are not actively trying to use effects without linking to the effect module. We should certainly address this issue if the programmer is not trying to do something clever but encounters this issue.

so I'd advise one of the two following alternatives:

  • Switch back to builtin exceptions (you can still rebind a builtin exception in Effect, using type exn += Unhandled = Unhandled_effect for instance)
  • Make the Effect module an independent library

What do you think about the following options?

  1. We raise Failure "Effect module not linked" if the runtime is not able to find the exceptions.
  2. Separating the initialisation code to camlinternalEffect?

@xavierleroy
Copy link
Copy Markdown
Contributor

Again we're discussing implementation before we've agreed on the API...

We raise Invalid_argument "Effect module not linked" if the runtime is not able to find the exceptions.

That's what otherlibs/unix does for the Unix_error exception. A fatal error is also possible, see

ocaml/runtime/fail_nat.c

Lines 204 to 218 in ef376dd

void caml_array_bound_error(void)
{
static atomic_uintnat exn_cache = ATOMIC_UINTNAT_INIT(0);
const value* exn = (const value*)atomic_load_acq(&exn_cache);
if (!exn) {
exn = caml_named_value("Pervasives.array_bound_error");
if (!exn) {
fprintf(stderr, "Fatal error: exception "
"Invalid_argument(\"index out of bounds\")\n");
exit(2);
}
atomic_store_rel(&exn_cache, (uintnat)exn);
}
caml_raise(*exn);
}

Please forget about the implementation and discuss the API (incl. documentation comments).

@kayceesrk
Copy link
Copy Markdown
Contributor Author

kayceesrk commented Jul 13, 2022

Again we're discussing implementation before we've agreed on the API...

Do we agree that changing the exception Unhandled to Effect.Unhandled : 'a Effect.t -> exn is a good idea? Are there other API-related questions that we are missing?

There are a few (potentially non-controversial) tasks left for improving the PR:

@Octachron
Copy link
Copy Markdown
Member

I think that renaming the exception to Effect.Unhandled is definitively a good idea.

Thinking a bit about the Effect module, I was wondering about the long term expectation for this module. As far as I understand, we expect to add support in the language itself for most of the primitives exposed by the module. What will happen to this module at this point? Will it contain only some utility functions? Or might it be completely removed at some point?

If the later option is a possibility, it might make sense to expose the effect module as a library. This has the advantage of making it clearer that the module is an experimental API that does not come with the backward-compatibility guarantee of the standard library. Moreover, integrating a library in the standard library is both simpler and faster than removing a module from the standard library.

@dbuenzli
Copy link
Copy Markdown
Contributor

This has the advantage of making it clearer that the module is an experimental API that does not come with the backward-compatibility guarantee of the standard library. Moreover, integrating a library in the standard library is both simpler and faster than removing a module from the standard library.

It also entails quite a bit of bureaucracy. In general the whole process around the stdlib remains a bit unrealistic in my opinion, especially for something like the 5.0 transition.

You can think as much as you want about design, design errors still occur and most of the time you only get to know about them once it's in the wild and the stuff gets used in anger.

The current process entails that if a design error gets made it is cast in stone for the next 20 years which is not so great. I think the stdlib should have a notion of @experimental when new APIs or even functions are introduced which would allow for corrections for as long as the tag applies. As an end user, you have been warned.

Personally I would even flag all of the entry points introduced specifically for 5.0 as such (e.g. for the little I have looked into I'm already not really happy about the Domain interface).

Copy link
Copy Markdown
Contributor

@xavierleroy xavierleroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to agree on the API, shall we?

@kayceesrk
Copy link
Copy Markdown
Contributor Author

kayceesrk commented Jul 21, 2022

Thanks @jonludlam for the help. With the latest commits, the outstanding tasks mentioned in #11423 (comment) are complete.

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 21, 2022

It seems there are a few additional details of the API that were implicitly agreed upon: (Or, at very least, I did not see any objections.)

  1. It should be possible to use effects without the standard library. (Suggested by @dra27 but not documented.)
  2. There should not be any top-level aliases of the exception Effect.Unhandled.

The reason I brought up (2) is that it seems to be the only reason to favor exception registration over built-in exceptions. As for (1), if the current PR is tracking this API design, perhaps Continuation_already_taken and the registration of Unhandled and Continuation_already_taken should all be moved to the module CamlinternalEffect?

Personally, I also like @dbuenzli's suggestion to mark new things experimental. Is this part of the API discussion? If so, I support marking both Effect and Domain (especially Domain) experimental in the sense that backward compatibility is not guaranteed.

@xavierleroy
Copy link
Copy Markdown
Contributor

It should be possible to use effects without the standard library. (Suggested by @dra27 but not documented.)

Just to clarify: I do not consider this necessary at all. First, anyone can build on top of the standard library, there is seldom a need to replace it entirely. Second, a stdlib replacement can register exceptions itself, like the stdlib would do.

Built-in exceptions and Camlinternal* modules are best avoided if we can: built-in exceptions because they pollute the scope and complicate the bootstrap, and Camlinternal* modules because they complicate the structure of the stdlib. Here, we can avoid both. Let's do that.

Copy link
Copy Markdown
Contributor

@xavierleroy xavierleroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read stdlib/effect.mli again, and the API and doc strings look just fine to me. Thanks! @favonia @dbuenzli @fpottier and others: if you have reservations about the current API and documentation, now is the time to tell.

I started looking at the implementation and I have a number of suggestions to make, probably tomorrow.

Copy link
Copy Markdown
Contributor

@dbuenzli dbuenzli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs and names look good. Thank @kayceesrk.

| [] -> None in
conv (Atomic.get printers)

let walk_sum_type x =
Copy link
Copy Markdown
Contributor

@favonia favonia Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is about the implementation.) I personally prefer the name extensible_variant over sum_type because the printer here does not work for all variant types (and in OCaml documentation the term "sum types" is not used). This comment also applies to the function string_of_sum_type a few lines below.

Copy link
Copy Markdown
Contributor

@favonia favonia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that in effects.etex, Unhandled is qualified (as Effect.Unhandled) but Continuation_already_taken is not, even though both are defined at the same place now. Otherwise, everything else looks great. @kayceesrk Thank you. 🙂

@kayceesrk
Copy link
Copy Markdown
Contributor Author

@xavierleroy

Camlinternal* modules because they complicate the structure of the stdlib

I needed to introduce the CamlInternalEffect module to break the cyclic dependency between the Printexc and Effect modules. Agree that it would be good to avoid the introduction of a new CamlInternal* module if there is way around this.

(Apologies for the slow response on this thread. It is the first week of teaching in the new semester for me.)

kayceesrk and others added 11 commits September 21, 2022 10:34
This bootstrap removes the pre-defined exceptions Undefined and
Continuation_already_taken exceptions used by effect handlers.
It seemed sensible not to duplicate the logic to turn sum types into
strings that is currently in use for exceptions. However, since it's
not exposed in Printexc, and the exception being matched is in
effect.ml, which depends on Printexc, I've opted for matching the
string "Stdlib.Effect.Unhandled" and assuming the single argument is
the effect.
In this case, produce a somewhat clean fatal error if these exceptions
are raised.
The documentation was already using "resumed" rather than taken which is
a good sign that "Continuation_already_resumed" is clearer.
@Octachron
Copy link
Copy Markdown
Member

Octachron commented Sep 21, 2022

I have updated and rebased the PR and added @xavierleroy's commit and mine to the branch.
I have also renamed the Continuation_already_taken exception to Continuation_already_resumed to align the exception name with the documentation.

@favonia: After this PR neither the Continuation_already_taken exception nor Unhandled exception are built-in exceptions. It thus seems reasonable to me to keep them printed as regular exceptions.

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Sep 21, 2022

After this PR neither the Continuation_already_taken exception nor Unhandled exception are built-in exceptions. It thus seems reasonable to me to keep them printed as regular exceptions.

I think the difference between "a built-in exception" and "an exception defined in the standard library" is very small. It's only meaningful to people who build compilers or alternative standard libraries. Therefore, I still feel it is better to include all (public) exceptions defined in the standard library. That said, I do not have a strong opinion because the output will be clear either way. I see that there are some unaddressed comments about the documentation. Other than those, I am very satisfied with the current PR and hope it can be shipped with the next alpha or the beta! 😃

@xavierleroy xavierleroy added this to the 5.0 milestone Sep 21, 2022
Use "@raise Unhandled" rather than "@raise Unhanded [e]"
@kayceesrk
Copy link
Copy Markdown
Contributor Author

kayceesrk commented Sep 24, 2022

The PR as it stands looks good to me. The only point of contention is the choice between the strings "Unhandled effect: %s" and "Stdlib.Effect.Unhandled(%s)". This can be changed in a future version without breaking code.

I realised I can't approve the PR since I'd opened it! @Octachron feel free to merge when you are happy.

Copy link
Copy Markdown
Contributor

@xavierleroy xavierleroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on behalf of @kayceesrk and @Octachron , who both wrote the code and reviewed the other's code.

@xavierleroy
Copy link
Copy Markdown
Contributor

There's a bootstrap in the middle, so I'd rather let @Octachron handle the final merge.

@Octachron
Copy link
Copy Markdown
Member

Merged and cherry-picked by hand to 5.0 .

@Octachron
Copy link
Copy Markdown
Member

The reperform test appears to be failing on Windows-mingw64, I will investigate the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Embed the unhandled effect in the exception Unhandled

9 participants