Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Use Grain exceptions instead of JS exceptions #565

Merged
merged 1 commit into from
Mar 24, 2021
Merged

Conversation

ospencer
Copy link
Member

@ospencer ospencer commented Mar 16, 2021

feat: Add ability to throw exceptions
feat: Add ability to register custom exception printers
feat: Add Exception stdlib with Exception.registerPrinter
fix: Use Is instead of Eq for match variant comparison
fix: export * with exceptions
chore: Only emit MatchFailure exception for partial matches

Closes #173, closes #301, closes #302.

Overview

This PR adds the ability to throw exceptions in Grain. (Previously, you could create exceptions, pattern match on them, use them with Result, etc., but you could not throw them.)

throw InvalidArgument("Your argument is no good!")

The toString value of the exception is printed when an error is thrown, but thanks to a new standard library included in this PR, the message can be overridden:

import Exception from "exception"

exception ErrorCode(Number)

Exception.registerPrinter(e => match (e) {
  ErrorCode(1) => Some("Custom message 1"),
  ErrorCode(2) => Some("Custom message 2"),
  ErrorCode(_) => Some("Unknown error code!"),
  _ => None // Allow other printers to handle exceptions we don't know about!
})

Details

Exceptions are managed by stdlib/runtime/exception (not to be confused with stdlib/exception). This module maintains a pointer to the list of available error printers, defines some exceptions to be used by the runtime, and defines a printer for those exceptions. As toString is not available yet, it exports a method that allows Pervasives to register the "base" exception printer. If Pervasives is not used (such as via the --no-pervasives flag), exceptions are printed as the generic "GrainException".

If you want to know why the base set of exceptions are defined in that module and not built directly into the compiler, we can have a virtual drink and I'll tell you the tale of how I went down that rabbit hole.

There were a handful of bugs that I needed to fix to get this together, but I'll point them out with some comments.

If this is approved by the core team, there's a few issues that I'll want to open:

  • There's currently no way to re-export an exception. We've talked in the past about reworking how values vs types are imported/exported, and I believe we'll need to revisit that conversation. It's not a huge deal right now, as runtime functions can just import the exception and throw it. It will, however, be problematic in the future when WebAssembly exception handling lands and users will want to catch those errors—they should not be importing anything from stdlib/runtime.
  • MatchFailure wasn't actually being thrown on a match failure—we have a custom node in the match compiler that handles that case. That was a bigger refactor than what I thought should go in this PR, so I left it alone for now.
  • We should refactor most stdlib code to throw real errors instead of just failing.

@ospencer ospencer requested a review from a team March 16, 2021 02:22
@ospencer ospencer self-assigned this Mar 16, 2021
@@ -1969,7 +1973,7 @@ let allocate_array_n = (wasm_mod, env, num_elts, elt) => {
compiled_num_elts(),
Expression.const(wasm_mod, encoded_const_int32(0)),
),
InvalidArgument,
IndexOutOfBounds,
Copy link
Member Author

Choose a reason for hiding this comment

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

For these two, it didn't seem worth it to me to set up calling an exception with arguments in the compiler, when this is just going to be rewritten into Grain soon anyway. I feel that IndexOutOfBounds is also an appropriate error here until this is moved over. If anyone really cares, I can do it sooner.

switch (partial) {
| Partial => [
{
instr_desc: MError(Runtime_errors.MatchFailure, [compiled_arg]),
Copy link
Member Author

Choose a reason for hiding this comment

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

Like I mentioned in the description, this isn't actually the error that's happening on a match failure. I did have to do this (sensible) change though to allow the exception handler to use match and not depend on itself.

@@ -375,13 +375,7 @@ module MatchTreeCompiler = {
);
| Fail =>
/* FIXME: We need a "throw error" node in ANF */
Copy link
Member Author

Choose a reason for hiding this comment

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

Even though I made this trap instead of moving on silently, I left this note here because that's what needs to happen to throw MatchFailure instead of just unreachable.

@@ -502,7 +496,7 @@ module MatchTreeCompiler = {
cmp_id_name,
Comp.prim2(
~allocation_type=StackAllocated(WasmI32),
Eq,
Is,
Copy link
Member Author

Choose a reason for hiding this comment

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

Calling Pervasives.(==) was unnecessary here (and prevented pattern matching on variants to happen in runtime mode). This actually will speed up pattern matching by a non-trivial amount.

@@ -2182,5 +2204,4 @@ let tests =
@ export_tests
@ comment_tests
@ number_tests
@ exception_tests
@ export_tests;
Copy link
Member Author

Choose a reason for hiding this comment

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

export_tests was in the list twice! Sneaky sneaky.

@@ -1,7 +1,5 @@
import { managedMemory, grainModule } from "../runtime";
Copy link
Member Author

Choose a reason for hiding this comment

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

This file isn't used, but I kept it in because it can be useful for FFI.


export let mut printers = 0n

export let registerBasePrinter = (f) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Arguably this should be dangerouslyRegisterBasePrinter.


exception SystemError(Number)

let stringOfSystemError = (code) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is honestly one the biggest things that I'm excited for.

Copy link
Member

Choose a reason for hiding this comment

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

🔥

@ospencer ospencer force-pushed the refactor-runtime-setup branch 2 times, most recently from 77ecea7 to 8ae6706 Compare March 23, 2021 03:00
Base automatically changed from refactor-runtime-setup to main March 23, 2021 22:48
Copy link
Member

@phated phated left a comment

Choose a reason for hiding this comment

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

This PR is so spicy! 🌶️ Love it!

@@ -177,7 +177,7 @@ rule token = parse
| "fail" { FAIL }
| "exception" { EXCEPTION }
| "try" { TRY }
| "raise" { RAISE }
| "throw" { THROW }
Copy link
Member

Choose a reason for hiding this comment

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

Does this lexer change make this a breaking? (I forget if this PR includes a breaking note)

Copy link
Member Author

Choose a reason for hiding this comment

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

Mmm, yeah it does (if you used throw as an identifier).

compiler/src/typed/builtin_types.re Show resolved Hide resolved
runtime/src/core/closures.js Show resolved Hide resolved
stdlib/runtime/exception.gr Show resolved Hide resolved
stdlib/runtime/exception.gr Show resolved Hide resolved
stdlib/string.gr Outdated Show resolved Hide resolved
stdlib/string.gr Outdated Show resolved Hide resolved
stdlib/runtime/gc.gr Show resolved Hide resolved

export *

exception MalformedUtf8
Copy link
Member

Choose a reason for hiding this comment

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

Should this have a printer?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought the default printer was fine, but I suppose I could see a printer for it.

stdlib/runtime/exception.gr Outdated Show resolved Hide resolved
@phated phated changed the title feat: Use Grain exceptions instead of JS exceptions feat!: Use Grain exceptions instead of JS exceptions Mar 24, 2021
feat: Add ability to `throw` exceptions
feat: Add ability to register custom exception printers
feat: Add Exception stdlib with Exception.registerPrinter
fix: Use Is instead of Eq for match variant comparison
fix: `export *` with exceptions
chore: Only emit MatchFailure exception for partial matches
Copy link
Member

@phated phated left a comment

Choose a reason for hiding this comment

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

Looks like the tests are passing now, so take this ⚔️ (it's dangerous to go alone)!

@ospencer ospencer merged commit 1f1cd4a into main Mar 24, 2021
@ospencer ospencer deleted the grain-exception branch March 24, 2021 05:13
@github-actions github-actions bot mentioned this pull request Apr 20, 2021
@github-actions github-actions bot mentioned this pull request May 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants