Skip to content

Exception for unhandled effect now carries the effect#11419

Closed
kayceesrk wants to merge 14 commits intoocaml:trunkfrom
kayceesrk:unhandled_effect2
Closed

Exception for unhandled effect now carries the effect#11419
kayceesrk wants to merge 14 commits intoocaml:trunkfrom
kayceesrk:unhandled_effect2

Conversation

@kayceesrk
Copy link
Copy Markdown
Contributor

@kayceesrk kayceesrk commented Jul 9, 2022

On trunk, when a program performs an effect that is not handled, we get the Unhandled effect.

utop # open Effect;;
utop # type _ t += E : unit t;;
type _ Stdlib.Effect.t += E : unit t
utop # perform E;;
Exception: Unhandled.

The exception above may be confusing since the term "Unhandled" may be applied to exceptions as well as effects. It is also not clear which effect was unhandled.

This PR renames the exception to Unhandled_effect and also parameterizes it to carry the unhandled effect. With this PR,

utop # open Effect;;
utop # type _ t += E : unit t;;
type _ Stdlib.Effect.t += E : unit t
utop # perform E;;
Exception: Unhandled_effect E.

The change itself is small but I needed two bootstrap commits, one for adding the new exception and the second one for removing the old one. It is essential to keep both of these bootstrap commits separate so that forks of the compiler can merge the changes sensibly.

Fixes #11185.

Todo

  • Test on Arm64
  • Update API docs
  • Update manual pages

@kayceesrk kayceesrk force-pushed the unhandled_effect2 branch from 08dbde2 to 793117d Compare July 9, 2022 04:17
@kayceesrk
Copy link
Copy Markdown
Contributor Author

I've confirmed that the PR passes the testsuite on Arm64.

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 9, 2022

@kayceesrk Thank you---I learned a few things by checking what you did in amd64/arm64.S. I suppose we are not planning to support other architectures now? (I was very stuck on the assembly code for other architectures, and thus I am curious.)

@kayceesrk
Copy link
Copy Markdown
Contributor Author

At the moment, trunk supports native code compilation only on amd64 and arm64.

@favonia

This comment was marked as resolved.

@kayceesrk kayceesrk force-pushed the unhandled_effect2 branch from 4f39852 to 29ff235 Compare July 9, 2022 07:37
@kayceesrk
Copy link
Copy Markdown
Contributor Author

Thanks. Once this PR has been reviewed, I'll rebase the changes to look sensible.

@xavierleroy xavierleroy added this to the 5.0 milestone Jul 9, 2022
@xavierleroy
Copy link
Copy Markdown
Contributor

Pinging the whole of @ocaml/caml-devel again. This is a last-minute change to a stdlib API, something that we have botched in the past. Please review. 3 approvals will be required before this goes in 5.0.

@dbuenzli
Copy link
Copy Markdown
Contributor

dbuenzli commented Jul 9, 2022

I suppose there's a technical reason for why this was not defined in the Effect module (i.e. Effect.Unhandled).

Also I don't see in the PR where this is defined in the Stdlib. I would expect something to be defined around here. How come is the name actually visible to pattern match on in the tests ?

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 9, 2022

I suppose there's a technical reason for why this was not defined in the Effect module (i.e. Effect.Unhandled).

Without heavily refactoring the runtime system, caml_perform has to know the index of this constructor (as a numeral in the runtime representation) to construct a correct value of type exn so that the correct exception can be raised. Otherwise, the Effect module would have to somehow pass the constructor index to the primitive caml_perform (because we would not know which index would be assigned to the constructor Unhandled_effect), and I personally don't feel that would lead to better code. For the same reason Continuation_already_taken is built in.

Also I don't see in the PR where this is defined in the Stdlib. I would expect something to be defined around here. How come is the name actually visible to pattern match on in the tests ?

It is as visible as other built-in stuff like int and bool. The type int is also not defined in Stdlib. They are injected into the environment even before the standard library is loaded.

@dbuenzli
Copy link
Copy Markdown
Contributor

dbuenzli commented Jul 9, 2022

It is as visible as other built-in stuff like int and bool. The type int is also not defined in Stdlib. They are injected into the environment even before the standard library is loaded.

Right but it's likely a good idea to have them properly documented somewhere no ?

@kayceesrk
Copy link
Copy Markdown
Contributor Author

@dbuenzli

Right but it's likely a good idea to have them properly documented somewhere no ?

I think it makes sense to redefine the predefined exceptions as stdlib.ml(i) does.

@kayceesrk
Copy link
Copy Markdown
Contributor Author

@dbuenzli the exceptions are now documented. See

ocaml/stdlib/effect.mli

Lines 22 to 28 in b8774c9

exception Unhandled_effect : 'a eff -> exn
(** Exception raised when an effect is performed and there is no matching
handler for that effect. The argument is the unhandled effect. *)
exception Continuation_already_taken
(** Exception raised when a continuation is resumed with a [continue] or
[discontinue] but the continuation has already been resumed. *)
.

@kayceesrk
Copy link
Copy Markdown
Contributor Author

3 approvals will be required before this goes in 5.0.

@Octachron what else would be needed to get to an approval? I'm feeling a bit guilty to ping @gasche, but he usually has a good taste for API design. Perhaps @sadiqj also as he understands the runtime system bits well.

@xavierleroy
Copy link
Copy Markdown
Contributor

I'm pretty sure the implementation is fine. What I'm worried about is the design. This is what needs to be eyeballed, preferably by people who 1) use effects already in experimental libraries, or 2) have good taste in library design in general.

The questions that pop in my mind at the moment:

  • You say that the type of effects must now be predefined. An alternative would be to move the exception to the Effect module. so that we have type 'a Effect.t and exception Effect.Unhandled : 'a Effect.t -> exn. It requires a different implementation than what you have currently, something similar to what we do for Unix.Error in the Unix library. But my point is that the design should dictate the implementation, not the other way around.
  • What are the pros and cons of having a predefined type 'a eff for effects? Why 'a eff and not 'a effect? (I know we have 'a ref and not 'a reference, but we also have 'a array, not 'a arr.)


\begin{caml_example}{verbatim}
try bar () with Unhandled -> "Saw unhandled exception"
try bar () with Unhandled_effect F -> "Saw unhandled exception"
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.

The message "Saw unhandled exception" is misleading. It should be "Observed an Unhandled_effect exception".

@fpottier
Copy link
Copy Markdown
Contributor

If possible, I would suggest avoiding the existence of two names for the same type or exception, such as 'a eff versus 'a Effect.t.

@kayceesrk
Copy link
Copy Markdown
Contributor Author

@xavierleroy Thanks for the reference to Unix.Error. I wasn't aware of Callback.register_exception, which may simplify the implementation dramatically. Let me have a go at this now.

@xavierleroy
Copy link
Copy Markdown
Contributor

Let me have a go at this now.

I was hoping we would have the design discussion before implementing...

@kayceesrk
Copy link
Copy Markdown
Contributor Author

I was hoping we would have the design discussion before implementing...

Sure. Currently, on trunk, both Unhandled and Continuation_already_taken exceptions are pre-defined so that the assembler stubs may pick up those exceptions from the caml_global_data. This PR proposes to essentially replace exception Unhandled with exception Unhandled : 'a eff -> exn, leaving the exception as pre-defined and additionally having to additionally pre-define the 'a eff type.

With the Callback.register_exception facility, both Unhandled_effect and Continuation_already_taken exceptions may be defined in the Effect module (as they should have been in the first place). The assembler stubs and the interpreter will now call new C functions that raise the corresponding exceptions. This would obviate the need for any pre-defined exceptions. At that point, there is also no need to address #11422 since the exceptions are no longer pre-defined.

I do not see any downsides to implementation. The fast paths, where no exceptions are raised, remain just as fast. The rare slow paths are marginally slower on native code due to the need to switch from OCaml to C stack to call the C functions that raise the corresponding exception.

@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 11, 2022

I am one of the early adopters of OCaml's algebraic effects, though I am not familiar with the OCaml internals. What I can provide is the perspectives of a user. I really like @xavierleroy's questions and here's my current assessment:

Exception Registration (Question 1)

From the end-user point of view, I could not see any differences between all these possible designs other than potential efficiency implications. Moreover, once the promised effect system arrives, none of these would matter because the type checker should prevent unhandled effects. I wanted Unhandled_effect (or Unhandled) because I wanted to to suggest the users of our libraries using some function to handle a particular effect (e.g., You should use ThisLibrary.run to handle effect X). I suppose domainslib is more popular and in fact some main developers are right here, so let me provide an example from that library instead. Its Task module has

val run : pool -> 'a task -> 'a
(** [...omitted by me, favonia...]

    This function should be used at the top level to enclose the calls to other
    functions that may await on promises. This includes {!await},
    {!parallel_for} and its variants. Otherwise, those functions will raise
    [Unhandled] exception. *)

The current type system would not guarantee that this function is "used at the top level to enclose the calls to other functions". I wanted to provide good error messages to direct our users when they fail to follow the instructions. Once we embed the effects, I can use Printexc.register_printer to achieve my goal.

Therefore, from my point of view, how exactly the effect is embedded does not matter, as long as it does not slow down the successful performs. I do not except the users of our library to handle Unhandled_effect at all---I am willing to do extra work so that they would not even see it! When I advocated for the current design (that @kayceesrk implemented here), I was mainly to avoid any runtime overhead in the critical primitive caml_perform. However, as @kayceesrk pointed out, the fast path (for successful performs) is probably still the same and thus the impact is probably minimum.

eff (Question 2)

I don't think it matters to me whether eff/effect is built in or not. As for the names (eff versus effect), I prefer effect over eff for clarity and consistency, and this would probably make my own teaching of OCaml easier. Personally, I can easily internalize it (as exn for extensible/exceptions) but some of my students probably would struggle and complain about these abbreviations. On the other hand, they could just copy the pattern type 'a eff += .... So, I like effect more, and it goes well with Effect.t and Unhandled_effect, but I don't have a strong preference.

@kayceesrk
Copy link
Copy Markdown
Contributor Author

kayceesrk commented Jul 11, 2022

What are the pros and cons of having a predefined type 'a eff for effects? Why 'a eff and not 'a effect? (I know we have 'a ref and not 'a reference, but we also have 'a array, not 'a arr.)

If we don't have any predefined exceptions for supporting effect handlers (as I propose to do), then this question does not apply. We will only have an 'a Effect.t type.

@kayceesrk kayceesrk closed this Jul 11, 2022
@kayceesrk kayceesrk reopened this Jul 11, 2022
@favonia
Copy link
Copy Markdown
Contributor

favonia commented Jul 11, 2022

Out of my curiosity, I suppose the current registration mechanism requires a concrete value of exn, which means we have to artificially add a constructor Something to eff/effect so that we can register Unhandled_effect Something for once (which sounds strange but okay). Is that accurate? What we need is only the constructor index associated to Unhandled_effect, and it would be great if we can just do that without creating a concrete value of exn. It was not a concern before because we could always use empty strings, zeros, or whatnot to fill in the unused fields, but now we have to do much more. Just to clarify, the registration protocol itself would not matter to a pure OCaml user like me (who has not played with C bindings---yet) but I am curious.

@kayceesrk
Copy link
Copy Markdown
Contributor Author

Out of my curiosity, I suppose the current registration mechanism requires a concrete value of exn, which means we have to artificially add a constructor Something to eff/effect so that we can have register Unhandled_effect Something for once (which sounds strange but okay).

Yes. We'd need to declare an effect in the effect.ml module which is only used for constructing a Unhandled_effect value. This effect will not be exposed by the Effect module and hence there is no risk of users observing this effect.

@gasche
Copy link
Copy Markdown
Member

gasche commented Jul 11, 2022

As for the names (eff versus effect), I prefer effect over eff for clarity and consistency, and this would probably make my own teaching of OCaml easier.

In some English-speaking communities "eff" is also used as a euphemised swear word ("eff off!").

@kayceesrk
Copy link
Copy Markdown
Contributor Author

I've opened #11423 which uses Callback.register_exception to share the exceptions from the Effect module with the runtime.


exception Unhandled_effect : 'a eff -> exn
(** Exception raised when an effect is performed and there is no matching
handler for that effect. The argument is the 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 could be improved by doing the usual trick of writing an expression for the item and describe what it means/how it should be read. In this case:

(** [Unhandled e] is raised when effect [e] is performed and there is no handler for it. *)

(** [perform e] performs an effect [e].

@raise Unhandled if there is no active handler. *)
@raise Unhandled_effect if there is no active handler. *)
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.

Is the concept of active handler defined somewhere (e.g. in supporting docs, sorry I hadn't the time to sift through them yet) ?

If not I don't like the term, it refers to something you can deactivate but AFAIK we are rather looking a syntactic property of programs (presence of a surrounding handler). Maybe simply:

@raise Unhandled e if there is no handler for [e].

handler for that effect. The argument is the unhandled effect. *)

exception Continuation_already_taken
(** Exception raised when a continuation is resumed with a [continue] or
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.

Please link to the functions, i.e. {!continue} or {!discontinue}.

Copy link
Copy Markdown
Contributor Author

@kayceesrk kayceesrk Jul 11, 2022

Choose a reason for hiding this comment

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

There is a Deep.continue and Shallow.continue and similarly two discontinues. I wasn't sure how to word it succinctly but also link to the right ones. Do you have a suggestion?

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.

I see. I'd first say then let's not have these two continue and discontinue bare naked. It's confusing, these names do not exist in this reading scope.

Also apparently there is even much more variants than what you mention. Since all these properly document that they @raise Continuation_already_taken, I don't think it's worth mentioning the names here.

What about one of these ?

(** Exception raised when a continuation is resumed for a second time. *)
(** Exception raised when a continuation is resumed twice. *)
(** Exception raised when a continuation is continued or discontinued a second time. *)
(** Exception raised when a continuation is continued or discontinued twice. *)

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.

or … more than once..

@dbuenzli
Copy link
Copy Markdown
Contributor

Gngngn, sorry these comments where meant to be made on the other PR.

@xavierleroy
Copy link
Copy Markdown
Contributor

Superceded by #11423. To avoid confusion, let me close this PR.

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

8 participants