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

Assertions and irrecoverable runtime errors #158

Merged
merged 9 commits into from
Dec 15, 2024

Conversation

ppolesiuk
Copy link
Member

This commit introduces three functions to the standard library:

  • runtimeError for raising fatal errors in impossible cases or in case of gross violation of function preconditions.
  • assertImpossible for indicating impossible branches
  • regular assert function

All these function are pure, so they should not be called at runtime of a correct program.

Resolves #157

This commit introduces three functions to the standard library:

- `runtimeError` for raising fatal errors in impossible cases or in
  case of gross violation of function preconditions.
- `assertImpossible` for indicating impossible branches
- regular `assert` function

All these function are pure, so they should not be called at runtime of
a correct program.
Copy link
Collaborator

@Foxinio Foxinio left a comment

Choose a reason for hiding this comment

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

Marked two minor changes i would prefer, but aside from that all seems good. Maybe if we decide to add reflection (like macros evaluating to filename, line number, etc) this module could be revisited.

@@ -55,8 +55,12 @@ let int64_cmpop op = int64_fun2 (fun x y -> of_bool (op x y))

let str_cmpop op = str_fun (fun s1 -> str_fun (fun s2 -> of_bool (op s1 s2)))

let runtime_error msg =
failwith ("Runtime error: " ^ msg)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this should be implemented using failwith. Triggering this function will result in output:

Fatal error: exception Failure("Runtime error: Assertion failed")

Personally i think it should be Just Runtime error: Assertion failed, so this code should be changed to eprintf + exit 1

Copy link
Contributor

Choose a reason for hiding this comment

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

I would add possibility to add custom error code/number on exit.

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 agree that this error should not be implemented with failwith. Maybe a custom exception that is handled in the main file would be better? The REPL implementation could catch this exception and recover with a prompt for an another command, instead of aborting the interpreter.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually that would probably be best

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know and possible I am wrong but this sounds like an error implementation which we would make in standard library.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean the issue #130? I think these two are two different things. In this PR we talk about errors that should never occur in correct programs (but unfortunately, most of programs are incorrect), so the effect of such error is pure ([]). In #130 the error has some effect visible in types. However, after merging this PR, we may rethink if we still want to implement #130.

Copy link
Member Author

Choose a reason for hiding this comment

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

However, I realized that catching runtime error in REPL is not as trivial as I thought. It is not just adding extra match clasue in Eval.ml, because the exception are catch before the evaluation. I think it is still possible to handle it correctly, because the evaluator is in CPS, but it would be a more complex change, and should be done in a separate issue.

lib/Base/Assert.fram Outdated Show resolved Hide resolved
@ppolesiuk ppolesiuk requested a review from Foxinio November 25, 2024 15:34
@Foxinio
Copy link
Collaborator

Foxinio commented Nov 27, 2024

I think we should have default ~onError in prelude that just calls runtimeError. After that it's all good to go.

Copy link
Collaborator

@Foxinio Foxinio left a comment

Choose a reason for hiding this comment

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

One thing to fix, and in my opinion PR is ready to merge

src/Eval/External.ml Outdated Show resolved Hide resolved
@forell
Copy link
Member

forell commented Dec 13, 2024

I brought this PR up to date with master and made some small changes based on the discussion we had.

  • I got rid of the program-ending version of Int64.toInt and made the Option variant the default. The failing version could be reintroduced as toIntUnsafe or something, but I think it's nicer to minimize the number of such "unsafe" functions, so...
  • I added Base.Option to the standard library, with two methods to unwrap optionals: unwrap (unsafe, uses runtimeError) and unwrapOr (takes a default value as an argument). I'm open to other names for these methods, I just went with what Rust uses. Also, unwrapOr is similar to the various implicit-based *Err functions that exist in the standard library, except the argument is not named and not a function. The proposed version is convenient for short expressions like n.toInt.unwrapOr 0, though.
  • The get method for strings remains unsafe. Littering code using indexing with unwraps or long method names would be too noisy when it's normally used in a context where the bounds have already been checked by the programmer. Safe variants that allow for recovery could be introduced.

@forell forell requested a review from Foxinio December 13, 2024 13:17
Copy link
Member Author

@ppolesiuk ppolesiuk left a comment

Choose a reason for hiding this comment

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

I agree with the changes, and generally, I approve this PR (this PR is created be me, so GitHub forbids me from approving it formally). I have some minor comments (below), that I would like to have discussed, but not necessarily implemented.

else (extern dbl_divInt : Int -> Int -> Int) self n
pub method div (n : Int) =
assert {msg="Division by zero"} (n != 0);
(extern dbl_divInt : Int -> Int -> Int) self n
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 have a philosophical question: is the type of this extern correct? Or should it be Int -> Int ->[] Int? The decision will to influence the overall type of this method (because it uses assert), but in the potential optimizing compiler can we assume that this function always terminates correctly?

The same question applies for some other function modified in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well the effect in this case doesn't come from the extern, but from assertion, so in my opinion it works both ways.

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 agree with these two methods, and proposed names are fine. I think, the unwrapErr function is missing. As a use case, consider that we have some polymorphic ~onError implicit in the context, so we can write a code optimistically using *Err functions without explicit error handling, but we still can recover from an error in some handler.

Moreover, other methods and functions for manipulating Option type, like map, fold, iter, etc. are missing, but the can be added as a separate issue.


{## For `Some x` returns `x`, otherwise the provided argument is returned.
#}
pub method unwrapOr default =
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 was wondering, if the default parameter should be named, because in almost all cases it will be provided. But I think, the proposed interface is good: the name of this method suggests what is the meaning of its first parameter, and writing code like x.unwrapOr {default = 42} would make some unnecessary noise.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I had the same dilemma here and came to the same conclusion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Alternative (although I don't know how good) would be for unwrap to take optional argument default.

Copy link
Member Author

Choose a reason for hiding this comment

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

Functions unwrap with optional parameter and unwrapOr have different effects. The latter is more pure (-> instead of ->[]).

Copy link
Contributor

@MinionJakub MinionJakub left a comment

Choose a reason for hiding this comment

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

It looks good.

Copy link
Collaborator

@Foxinio Foxinio left a comment

Choose a reason for hiding this comment

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

I agree that more Option functionality should be added, and can be done in separate PR. And beside that, all changes look good and should be merged.

@ppolesiuk ppolesiuk merged commit b88e893 into master Dec 15, 2024
2 checks passed
@ppolesiuk ppolesiuk deleted the 157-irrecoverable-errors branch December 15, 2024 20:42
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.

Irrecoverable errors
4 participants