From 3302f72a74785b6a8302f4a293e48727ad4c5edf Mon Sep 17 00:00:00 2001 From: Piotr Polesiuk Date: Fri, 22 Nov 2024 21:46:30 +0100 Subject: [PATCH 1/6] Assertions and irrecoverable runtime errors 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. --- lib/Base/Assert.fram | 28 ++++++++++++++++++++++++++++ lib/Base/Int.fram | 13 +++++++------ lib/Base/Int64.fram | 19 +++++++++++-------- lib/Base/String.fram | 8 ++++---- lib/Prelude.fram | 2 ++ src/Eval/External.ml | 6 +++++- 6 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 lib/Base/Assert.fram diff --git a/lib/Base/Assert.fram b/lib/Base/Assert.fram new file mode 100644 index 0000000..d4c8b27 --- /dev/null +++ b/lib/Base/Assert.fram @@ -0,0 +1,28 @@ +(* This file is part of DBL, released under MIT license. + * See LICENSE for details. + *) + +import open Types + +(** Abort the program with given message. + + Note that this function is pure. In correct programs, this function should + be never called at runtime, but it can be used to appease the type-checker. + Use this function only in impossible match clauses or in case of gross + violation of function preconditions (e.g., division by zero). *) +pub let runtimeError {type T} = + (extern dbl_runtimeError : String ->[] T) + +(** Explicitly assert, that this case is impossible *) +pub let assertImpossible {?msg : String} () = + runtimeError + match msg with + | None => "Assertion failed" + | Some msg => msg + end + +(** Assert, that given condition should always hold. *) +pub let assert {?msg} b = + if b then () + else + assertImpossible {?msg} () diff --git a/lib/Base/Int.fram b/lib/Base/Int.fram index c638094..179df53 100644 --- a/lib/Base/Int.fram +++ b/lib/Base/Int.fram @@ -4,6 +4,7 @@ import open Types import open Operators +import open Assert pub method toString = (extern dbl_intToString : Int -> String) self pub method toInt64 = (extern dbl_int64ToInt : Int -> Int64) self @@ -19,13 +20,13 @@ pub method add = (extern dbl_addInt : Int -> Int -> Int) self pub method sub = (extern dbl_subInt : Int -> Int -> Int) self pub method mul = (extern dbl_mulInt : Int -> Int -> Int) self -pub method div {~re : {type X} -> Unit ->[|_] X} (n : Int) = - if n.equal 0 then ~re () - else (extern dbl_divInt : Int -> Int -> Int) self n +pub method div (n : Int) = + assert {msg="Division by zero"} (n.neq 0); + (extern dbl_divInt : Int -> Int -> Int) self n -pub method mod {~re : {type X} -> Unit ->[|_] X} (n : Int) = - if n.equal 0 then ~re () - else (extern dbl_modInt : Int -> Int -> Int) self n +pub method mod (n : Int) = + assert {msg="Division by zero"} (n.neq 0); + (extern dbl_modInt : Int -> Int -> Int) self n pub method neg {self : Int} = 0 - self diff --git a/lib/Base/Int64.fram b/lib/Base/Int64.fram index 47e2c61..23d1ca5 100644 --- a/lib/Base/Int64.fram +++ b/lib/Base/Int64.fram @@ -4,6 +4,7 @@ import open Types import open Operators +import open Assert pub method equal {self : Int64} = (extern dbl_eqInt64 : Int64 -> Int64 -> Bool) self @@ -26,12 +27,12 @@ pub method sub {self : Int64} = (extern dbl_subInt64 : Int64 -> Int64 -> Int64) self pub method mul {self : Int64} = (extern dbl_mulInt64 : Int64 -> Int64 -> Int64) self -pub method div {~re : {type X} -> Unit ->[|_] X} (n : Int64) = - if n.equal 0L then ~re () - else (extern dbl_divInt64 : Int64 -> Int64 -> Int64) self n -pub method mod {~re : {type X} -> Unit ->[|_] X} (n : Int64) = - if n.equal 0L then ~re () - else (extern dbl_modInt64 : Int64 -> Int64 -> Int64) self n +pub method div (n : Int64) = + assert {msg="Division by zero"} (n.neq 0L); + (extern dbl_divInt64 : Int64 -> Int64 -> Int64) self n +pub method mod (n : Int64) = + assert {msg="Division by zero"} (n.neq 0L); + (extern dbl_modInt64 : Int64 -> Int64 -> Int64) self n pub method land = (extern dbl_andInt64 : Int64 -> Int64 -> Int64) self pub method lor = (extern dbl_orInt64 : Int64 -> Int64 -> Int64) self @@ -45,14 +46,16 @@ pub method pred {self : Int64} = self - 1L pub method toString {self : Int64} = (extern dbl_int64ToString : Int64 -> String) self -pub method toInt {~re : {type X} -> Unit ->[|_] X, self : Int64} = +pub method toIntErr { ~onError, self : Int64 } = if self <= 0x3fffffffffffffffL && self >= 0xc000000000000000L then (extern dbl_intToInt64 : Int64 -> Int) self - else ~re () + else ~onError () pub method toIntOpt {self : Int64} = if self <= 0x3fffffffffffffffL && self >= 0x7fffffffffffffffL then Some ((extern dbl_intToInt64 : Int64 -> Int) self) else None +pub method toInt {self : Int64} = + self.toIntErr { ~onError = fn () => runtimeError "Conversion overflow" } pub method abs {self : Int64} = if self < 0L then self.neg else self diff --git a/lib/Base/String.fram b/lib/Base/String.fram index af4cebf..f6705f2 100644 --- a/lib/Base/String.fram +++ b/lib/Base/String.fram @@ -4,6 +4,7 @@ import open /Base/Types import open /Base/Operators +import open /Base/Assert import /Base/Int pub method add = (extern dbl_strCat : String -> String -> String) self @@ -16,10 +17,9 @@ pub method ge = (extern dbl_geStr : String -> String -> Bool) self pub method le = (extern dbl_leStr : String -> String -> Bool) self pub method length = (extern dbl_strLen : String -> Int) self -pub method get {~re : {type X} -> Unit ->[|_] X, self : String} (n : Int) = - if n >= 0 && n < self.length then - (extern dbl_strGet : String -> Int -> Char) self n - else ~re () +pub method get {self : String} (n : Int) = + assert {msg="Out of bounds"} (n >= 0 && n < self.length); + (extern dbl_strGet : String -> Int -> Char) self n pub method toList {self : String} = let getChar = extern dbl_strGet : String -> Int -> Char in diff --git a/lib/Prelude.fram b/lib/Prelude.fram index 383ba95..a3d299a 100644 --- a/lib/Prelude.fram +++ b/lib/Prelude.fram @@ -4,6 +4,7 @@ import /Base/Types import /Base/Operators +import /Base/Assert import /Base/Bool import /Base/Int import /Base/Int64 @@ -12,6 +13,7 @@ import /Base/String pub open Types pub open Operators +pub open Assert pub let id x = x diff --git a/src/Eval/External.ml b/src/Eval/External.ml index fdabfb2..e51dcc9 100644 --- a/src/Eval/External.ml +++ b/src/Eval/External.ml @@ -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) + let extern_map = - [ "dbl_negInt", int_unop ( ~- ); + [ "dbl_runtimeError", str_fun runtime_error; + "dbl_negInt", int_unop ( ~- ); "dbl_addInt", int_binop ( + ); "dbl_subInt", int_binop ( - ); "dbl_mulInt", int_binop ( * ); From 46453dcf94a118c319b85d8eb156cf80c328cf01 Mon Sep 17 00:00:00 2001 From: Piotr Polesiuk Date: Mon, 25 Nov 2024 11:30:10 +0100 Subject: [PATCH 2/6] Runtime error as a special exception --- lib/Base/Assert.fram | 4 ++-- src/Eval/Eval.ml | 4 +++- src/Eval/Eval.mli | 2 ++ src/Eval/External.ml | 17 ++++++++++------- src/dbl.ml | 1 + 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/Base/Assert.fram b/lib/Base/Assert.fram index d4c8b27..e2ce2ca 100644 --- a/lib/Base/Assert.fram +++ b/lib/Base/Assert.fram @@ -14,7 +14,7 @@ pub let runtimeError {type T} = (extern dbl_runtimeError : String ->[] T) (** Explicitly assert, that this case is impossible *) -pub let assertImpossible {?msg : String} () = +pub let impossible {?msg : String} () = runtimeError match msg with | None => "Assertion failed" @@ -25,4 +25,4 @@ pub let assertImpossible {?msg : String} () = pub let assert {?msg} b = if b then () else - assertImpossible {?msg} () + impossible {?msg} () diff --git a/src/Eval/Eval.ml b/src/Eval/Eval.ml index baa84d8..8bfee5b 100644 --- a/src/Eval/Eval.ml +++ b/src/Eval/Eval.ml @@ -4,6 +4,8 @@ open Value +exception Runtime_error = External.Runtime_error + (** Evaluator *) (* ========================================================================= *) @@ -90,7 +92,7 @@ let rec eval_expr env (e : Lang.Untyped.expr) cont = end | ERepl func -> eval_repl env func cont | EReplExpr(e1, tp, e2) -> - Printf.printf ": %s\n" tp; + Printf.printf ": %s\n%!" tp; eval_expr env e1 (fun v1 -> Printf.printf "= %s\n" (to_string v1); eval_expr env e2 cont) diff --git a/src/Eval/Eval.mli b/src/Eval/Eval.mli index cdd60de..352db6f 100644 --- a/src/Eval/Eval.mli +++ b/src/Eval/Eval.mli @@ -4,4 +4,6 @@ (** Evaluator *) +exception Runtime_error + val eval_program : Lang.Untyped.program -> unit diff --git a/src/Eval/External.ml b/src/Eval/External.ml index e51dcc9..4614441 100644 --- a/src/Eval/External.ml +++ b/src/Eval/External.ml @@ -4,26 +4,32 @@ open Value +exception Runtime_error + (** External functions *) (* ========================================================================= *) +let runtime_error msg = + Printf.eprintf "Runtime error: %s\n%!" msg; + raise Runtime_error + let unit_fun f = VFn (fun v cont -> cont (f ())) let int_fun f = VFn (fun v cont -> match v with | VNum n -> cont (f n) - | _ -> failwith "Runtime error!") + | _ -> runtime_error "Not an integer") let str_fun f = VFn (fun v cont -> match v with | VStr s -> cont (f s) - | _ -> failwith "Runtime error!") + | _ -> runtime_error "Not a string") let list_chr_fun f = VFn (fun v cont -> let rec parse_list = function | VCtor(0, []) -> [] | VCtor(1, [VNum x; xs]) -> Char.chr x :: parse_list xs - | _ -> failwith "Runtime error!" in + | _ -> runtime_error "Not a list" in cont (f @@ parse_list v)) let v_unit = VCtor(0, []) @@ -44,7 +50,7 @@ let int_cmpop op = int_fun2 (fun x y -> of_bool (op x y)) let int64_fun f = VFn (fun v cont -> match v with | VNum64 n -> cont (f n) - | _ -> failwith "Runtime error!") + | _ -> failwith "Not a 64-bit integer") let int64_fun2 f = int64_fun (fun x -> int64_fun (f x)) @@ -55,9 +61,6 @@ 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) - let extern_map = [ "dbl_runtimeError", str_fun runtime_error; "dbl_negInt", int_unop ( ~- ); diff --git a/src/dbl.ml b/src/dbl.ml index 7f35e00..2745850 100644 --- a/src/dbl.ml +++ b/src/dbl.ml @@ -79,3 +79,4 @@ let _ = | Some fname -> Pipeline.run_file fname with | InterpLib.Error.Fatal_error -> exit 1 + | Eval.Runtime_error -> exit 2 From 18b03245781c19ce2f29e2f416c89ae28f9062ee Mon Sep 17 00:00:00 2001 From: Patrycja Balik Date: Thu, 12 Dec 2024 16:54:08 +0100 Subject: [PATCH 3/6] Use new comment syntax in Assert --- lib/Base/Assert.fram | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/Base/Assert.fram b/lib/Base/Assert.fram index e2ce2ca..938b25e 100644 --- a/lib/Base/Assert.fram +++ b/lib/Base/Assert.fram @@ -1,19 +1,21 @@ -(* This file is part of DBL, released under MIT license. - * See LICENSE for details. - *) +{# This file is part of DBL, released under MIT license. + # See LICENSE for details. + #} import open Types -(** Abort the program with given message. +{## Abort the program with given message. Note that this function is pure. In correct programs, this function should - be never called at runtime, but it can be used to appease the type-checker. + never be called at runtime, but it can be used to appease the type-checker. Use this function only in impossible match clauses or in case of gross - violation of function preconditions (e.g., division by zero). *) + violation of function preconditions (e.g., division by zero). + #} pub let runtimeError {type T} = (extern dbl_runtimeError : String ->[] T) -(** Explicitly assert, that this case is impossible *) +{## Explicitly assert that this case is impossible. + #} pub let impossible {?msg : String} () = runtimeError match msg with @@ -21,7 +23,8 @@ pub let impossible {?msg : String} () = | Some msg => msg end -(** Assert, that given condition should always hold. *) +{## Assert that given condition should always hold. + #} pub let assert {?msg} b = if b then () else From 01ac2d7ea6af24238aaaf1b5409e7f63efbf54b6 Mon Sep 17 00:00:00 2001 From: Patrycja Balik Date: Fri, 13 Dec 2024 13:56:48 +0100 Subject: [PATCH 4/6] Add `unwrap*` methods for `Option` and some tweaks --- lib/Base/Int.fram | 4 ++-- lib/Base/Int64.fram | 6 ++---- lib/Base/Option.fram | 25 +++++++++++++++++++++++++ lib/Prelude.fram | 1 + src/Eval/External.ml | 2 +- 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 lib/Base/Option.fram diff --git a/lib/Base/Int.fram b/lib/Base/Int.fram index 27afad2..a9a176c 100644 --- a/lib/Base/Int.fram +++ b/lib/Base/Int.fram @@ -21,11 +21,11 @@ pub method sub = (extern dbl_subInt : Int -> Int -> Int) self pub method mul = (extern dbl_mulInt : Int -> Int -> Int) self pub method div (n : Int) = - assert {msg="Division by zero"} (n.neq 0); + assert {msg="Division by zero"} (n != 0); (extern dbl_divInt : Int -> Int -> Int) self n pub method mod (n : Int) = - assert {msg="Division by zero"} (n.neq 0); + assert {msg="Division by zero"} (n != 0); (extern dbl_modInt : Int -> Int -> Int) self n pub method neg {self : Int} = 0 - self diff --git a/lib/Base/Int64.fram b/lib/Base/Int64.fram index 621ef03..b83fd95 100644 --- a/lib/Base/Int64.fram +++ b/lib/Base/Int64.fram @@ -48,14 +48,12 @@ pub method toString {self : Int64} = (extern dbl_int64ToString : Int64 -> String) self pub method toIntErr { ~onError, self : Int64 } = if self <= 0x3fffffffffffffffL && self >= 0xc000000000000000L then - (extern dbl_intToInt64 : Int64 -> Int) self + (extern dbl_int64ToInt : Int64 -> Int) self else ~onError () -pub method toIntOpt {self : Int64} = +pub method toInt {self : Int64} = if self <= 0x3fffffffffffffffL && self >= 0x7fffffffffffffffL then Some ((extern dbl_int64ToInt : Int64 -> Int) self) else None -pub method toInt {self : Int64} = - self.toIntErr { ~onError = fn () => runtimeError "Conversion overflow" } pub method abs {self : Int64} = if self < 0L then self.neg else self diff --git a/lib/Base/Option.fram b/lib/Base/Option.fram new file mode 100644 index 0000000..abd4de3 --- /dev/null +++ b/lib/Base/Option.fram @@ -0,0 +1,25 @@ +{# This file is part of DBL, released under MIT license. + # See LICENSE for details. + #} + +import open /Base/Types +import open /Base/Assert + +{## For `Some x` returns `x`, otherwise the provided argument is returned. + #} +pub method unwrapOr default = + match self with + | None => default + | Some x => x + end + +{## This method should only be called on `Some x` values, in which case it + returns `x`. When applied to `None` the entire program crashes irrecoverably + with a runtime error. The resulting error message can be optionally specified + with `?msg`. + #} +pub method unwrap {?msg} = + match self with + | None => runtimeError (msg.unwrapOr "Called `unwrap` on `None`") + | Some x => x + end diff --git a/lib/Prelude.fram b/lib/Prelude.fram index eb17828..77618c8 100644 --- a/lib/Prelude.fram +++ b/lib/Prelude.fram @@ -6,6 +6,7 @@ import /Base/Types import /Base/Operators import /Base/Assert import /Base/Bool +import /Base/Option import /Base/Int import /Base/Int64 import /Base/Char diff --git a/src/Eval/External.ml b/src/Eval/External.ml index 4614441..bf3fbe2 100644 --- a/src/Eval/External.ml +++ b/src/Eval/External.ml @@ -50,7 +50,7 @@ let int_cmpop op = int_fun2 (fun x y -> of_bool (op x y)) let int64_fun f = VFn (fun v cont -> match v with | VNum64 n -> cont (f n) - | _ -> failwith "Not a 64-bit integer") + | _ -> runtime_error "Not a 64-bit integer") let int64_fun2 f = int64_fun (fun x -> int64_fun (f x)) From 7aacb27bd0cc4f3210bea02e44d6bff578aeeec8 Mon Sep 17 00:00:00 2001 From: Patrycja Balik Date: Sun, 15 Dec 2024 21:25:56 +0100 Subject: [PATCH 5/6] Add `unwrapErr` method to `Option` --- lib/Base/Assert.fram | 9 +++------ lib/Base/Option.fram | 14 ++++++++++---- test/stdlib/stdlib0000_Option.fram | 7 +++++++ 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 test/stdlib/stdlib0000_Option.fram diff --git a/lib/Base/Assert.fram b/lib/Base/Assert.fram index 938b25e..302e651 100644 --- a/lib/Base/Assert.fram +++ b/lib/Base/Assert.fram @@ -9,13 +9,11 @@ import open Types Note that this function is pure. In correct programs, this function should never be called at runtime, but it can be used to appease the type-checker. Use this function only in impossible match clauses or in case of gross - violation of function preconditions (e.g., division by zero). - #} + violation of function preconditions (e.g., division by zero). ##} pub let runtimeError {type T} = (extern dbl_runtimeError : String ->[] T) -{## Explicitly assert that this case is impossible. - #} +{## Explicitly assert that this case is impossible. ##} pub let impossible {?msg : String} () = runtimeError match msg with @@ -23,8 +21,7 @@ pub let impossible {?msg : String} () = | Some msg => msg end -{## Assert that given condition should always hold. - #} +{## Assert that given condition should always hold. ##} pub let assert {?msg} b = if b then () else diff --git a/lib/Base/Option.fram b/lib/Base/Option.fram index abd4de3..6555fe9 100644 --- a/lib/Base/Option.fram +++ b/lib/Base/Option.fram @@ -5,19 +5,25 @@ import open /Base/Types import open /Base/Assert -{## For `Some x` returns `x`, otherwise the provided argument is returned. - #} +{## For `Some x` returns `x`, otherwise the provided argument is returned. ##} pub method unwrapOr default = match self with | None => default | Some x => x end +{## For `Some x` returns `x`, otherwise the function `~onError` is called and + its result returned. ##} +pub method unwrapErr {~onError} = + match self with + | None => ~onError () + | Some x => x + end + {## This method should only be called on `Some x` values, in which case it returns `x`. When applied to `None` the entire program crashes irrecoverably with a runtime error. The resulting error message can be optionally specified - with `?msg`. - #} + with `?msg`. ##} pub method unwrap {?msg} = match self with | None => runtimeError (msg.unwrapOr "Called `unwrap` on `None`") diff --git a/test/stdlib/stdlib0000_Option.fram b/test/stdlib/stdlib0000_Option.fram new file mode 100644 index 0000000..a503050 --- /dev/null +++ b/test/stdlib/stdlib0000_Option.fram @@ -0,0 +1,7 @@ +let _ = + assert ((None).unwrapOr 0 == 0); + assert ((Some 42).unwrapOr 0 == 42); + assert ((Some 42).unwrap == 42); + let ~onError () = 0 in + assert ((None).unwrapErr == 0); + assert ((Some 42).unwrapErr == 42) From ff8cb6aaa9492fe26f6aa20671a04a8cdbe3cbb5 Mon Sep 17 00:00:00 2001 From: Patrycja Balik Date: Sun, 15 Dec 2024 21:35:33 +0100 Subject: [PATCH 6/6] Rename test --- test/stdlib/{stdlib0000_Option.fram => stdlib0001_Option.fram} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/stdlib/{stdlib0000_Option.fram => stdlib0001_Option.fram} (100%) diff --git a/test/stdlib/stdlib0000_Option.fram b/test/stdlib/stdlib0001_Option.fram similarity index 100% rename from test/stdlib/stdlib0000_Option.fram rename to test/stdlib/stdlib0001_Option.fram