From 0894dc551c4aad678e39e9de29919bb17a0ea23e Mon Sep 17 00:00:00 2001 From: Spotandjake <40705786+spotandjake@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:37:32 -0400 Subject: [PATCH] feat(stdlib): Add `Exception.toString` (#2143) --- compiler/test/stdlib/exception.test.gr | 22 +++ compiler/test/suites/basic_functionality.re | 2 +- compiler/test/suites/stdlib.re | 1 + stdlib/exception.gr | 20 +-- stdlib/exception.md | 25 ++++ stdlib/pervasives.gr | 4 +- stdlib/runtime/exception.gr | 142 +++++++++----------- stdlib/runtime/exception.md | 71 ++++++++-- stdlib/runtime/gc.gr | 12 +- stdlib/runtime/malloc.gr | 14 +- stdlib/runtime/unsafe/panic.gr | 28 ++++ stdlib/runtime/unsafe/panic.md | 14 ++ 12 files changed, 237 insertions(+), 118 deletions(-) create mode 100644 compiler/test/stdlib/exception.test.gr create mode 100644 stdlib/runtime/unsafe/panic.gr create mode 100644 stdlib/runtime/unsafe/panic.md diff --git a/compiler/test/stdlib/exception.test.gr b/compiler/test/stdlib/exception.test.gr new file mode 100644 index 0000000000..24bb90cd0c --- /dev/null +++ b/compiler/test/stdlib/exception.test.gr @@ -0,0 +1,22 @@ +module ExceptionTest + +from "exception" include Exception + +exception Test1 +exception Test2(String) +// Exception.toString +assert Exception.toString(Failure("Test")) == "Failure: Test" +assert Exception.toString(Test1) == "Test1" +assert Exception.toString(Test2("Test")) == "Test2(\"Test\")" + +// Exception.registerPrinter +let printer = e => { + match (e) { + Test1 => Some("Test1: This is a test"), + Test2(s) => Some("Test2"), + _ => None, + } +} +Exception.registerPrinter(printer) +assert Exception.toString(Test1) == "Test1: This is a test" +assert Exception.toString(Test2("Test")) == "Test2" diff --git a/compiler/test/suites/basic_functionality.re b/compiler/test/suites/basic_functionality.re index 4607d39675..dd75d71186 100644 --- a/compiler/test/suites/basic_functionality.re +++ b/compiler/test/suites/basic_functionality.re @@ -377,6 +377,6 @@ describe("basic functionality", ({test, testSkip}) => { ~config_fn=smallestFileConfig, "smallest_grain_program", "", - 5165, + 4750, ); }); diff --git a/compiler/test/suites/stdlib.re b/compiler/test/suites/stdlib.re index 5a0b14e318..88b0f2e6d1 100644 --- a/compiler/test/suites/stdlib.re +++ b/compiler/test/suites/stdlib.re @@ -77,6 +77,7 @@ describe("stdlib", ({test, testSkip}) => { assertStdlib("bytes.test"); assertStdlib("buffer.test"); assertStdlib("char.test"); + assertStdlib("exception.test"); assertStdlib("float32.test"); assertStdlib("float64.test"); assertStdlib("hash.test"); diff --git a/stdlib/exception.gr b/stdlib/exception.gr index d4c784f02c..bdd9a4958b 100644 --- a/stdlib/exception.gr +++ b/stdlib/exception.gr @@ -39,13 +39,15 @@ from "runtime/exception" include Exception * * @since v0.3.0 */ -@disableGC -provide let rec registerPrinter = (printer: Exception => Option) => { - // This function _must_ be @disableGC because the printer list uses - // unsafe types. Not really a memory leak as this list is never collected +provide let registerPrinter = Exception.registerPrinter - // no need to increment refcount on f; we just don't decRef it at the end of the function - Exception.printers = WasmI32.fromGrain((printer, Exception.printers)) - Memory.decRef(WasmI32.fromGrain(registerPrinter)) - void -} +/** + * Gets the string representation of the given exception. + * + * @param e: The exception to stringify + * + * @returns The string representation of the exception + * + * @since v0.7.0 + */ +provide let toString = Exception.toString diff --git a/stdlib/exception.md b/stdlib/exception.md index fed3e78b7e..5cefb31623 100644 --- a/stdlib/exception.md +++ b/stdlib/exception.md @@ -65,3 +65,28 @@ Exception.registerPrinter(e => { throw ExampleError(1) // Error found on line: 1 ``` +### Exception.**toString** + +
+Added in next +No other changes yet. +
+ +```grain +toString : (e: Exception) => String +``` + +Gets the string representation of the given exception. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`e`|`Exception`|The exception to stringify| + +Returns: + +|type|description| +|----|-----------| +|`String`|The string representation of the exception| + diff --git a/stdlib/pervasives.gr b/stdlib/pervasives.gr index 04a8e1b0da..5ab250843c 100644 --- a/stdlib/pervasives.gr +++ b/stdlib/pervasives.gr @@ -236,7 +236,7 @@ provide primitive unbox = "@unbox" primitive elideTypeInfo = "@meta.elide_type_info" @unsafe let setupExceptions = () => { - Exception.dangerouslyRegisterPrinter(e => { + Exception.registerPrinter(e => { match (e) { Failure(msg) => Some("Failure: " ++ msg), InvalidArgument(msg) => Some("Invalid argument: " ++ msg), @@ -247,7 +247,7 @@ let setupExceptions = () => { // If type information is elided, remove dependency on toString as // it will have no effect on exceptions if (!elideTypeInfo) { - Exception.dangerouslyRegisterBasePrinter(e => Some(toString(e))) + Exception.registerBasePrinter(e => toString(e)) } } diff --git a/stdlib/runtime/exception.gr b/stdlib/runtime/exception.gr index e0a35e7fb6..483d957bab 100644 --- a/stdlib/runtime/exception.gr +++ b/stdlib/runtime/exception.gr @@ -1,96 +1,74 @@ -@runtimeMode +@noPervasives module Exception -from "runtime/unsafe/wasmi32" include WasmI32 -use WasmI32.{ (==), (+), (-) } - -foreign wasm fd_write: - (WasmI32, WasmI32, WasmI32, WasmI32) => WasmI32 from "wasi_snapshot_preview1" - -primitive unreachable = "@unreachable" - -provide let mut printers = 0n - -// These functions are dangerous because they leak runtime memory and perform -// no GC operations. As such, they should only be called by this module and/or -// modules that understand these restrictions, namely Pervasives. - -provide let dangerouslyRegisterBasePrinter = f => { - let mut current = printers - while (true) { - // There will be at least one printer registered by the time this is called - let (_, next) = WasmI32.toGrain(current): - (Exception => Option, WasmI32) - if (next == 0n) { - // Using a tuple in runtime mode is typically disallowed as there is no way - // to reclaim the memory, but this function is only called once - let newBase = (WasmI32.fromGrain(f), 0n) - WasmI32.store(current, WasmI32.fromGrain(newBase), 12n) - break - } - current = next - } - // We don't decRef the closure or arguments here to avoid a cyclic dep. on Memory. - // This is fine, as this function should only be called once. - void -} - -provide let dangerouslyRegisterPrinter = f => { - printers = WasmI32.fromGrain((f, printers)) - // We don't decRef the closure or arguments here to avoid a cyclic dep. on Memory. - // This is fine, as this function is only called seldomly. - void -} - -// avoid cirular dependency on gc -let incRef = v => { - let ptr = WasmI32.fromGrain(v) - 8n - WasmI32.store(ptr, WasmI32.load(ptr, 0n) + 1n, 0n) - v -} +from "runtime/unsafe/panic" include Panic let _GENERIC_EXCEPTION_NAME = "GrainException" - -let exceptionToString = (e: Exception) => { - let mut result = _GENERIC_EXCEPTION_NAME - let mut current = printers - while (true) { - if (current == 0n) return result - let (printer, next) = WasmI32.toGrain(current): - (Exception => Option, WasmI32) - // as GC is not available, manually increment the references - match (incRef(printer)(incRef(e))) { - Some(str) => return str, - None => { - current = next +let mut basePrinter = None +let mut printers = [] + +/** + * Registers a base exception printer. If no other exception printers are + * registered, the base printer is used to convert an exception to a string. + * + * @param printer: The base exception printer to register + * + * @since v0.7.0 + */ +provide let registerBasePrinter = (printer: Exception => String) => + basePrinter = Some(printer) + +/** + * Registers an exception printer. When an exception is thrown, all registered + * printers are called in order from the most recently registered printer to + * the least recently registered printer. The first `Some` value returned is + * used as the exception's string value. + * + * @param printer: The exception printer to register + * + * @since v0.7.0 + */ +provide let registerPrinter = (printer: Exception => Option) => + printers = [printer, ...printers] + +/** + * Gets the string representation of the given exception. + * + * @param e: The exception to stringify + * + * @returns The string representation of the exception + * + * @since v0.7.0 + */ +provide let toString = (e: Exception) => { + let rec exceptionToString = (e, printers) => { + match (printers) { + [] => match (basePrinter) { + Some(f) => f(e), + None => _GENERIC_EXCEPTION_NAME, + }, + [printer, ...rest] => { + match (printer(e)) { + Some(s) => s, + None => exceptionToString(e, rest), + } }, } } - return result -} - -// HACK: Allocate static buffer for printing (40 bytes) -// Would be nice to have a better way to allocate a static block from -// the runtime heap, but this is the only module that needs to do it -let iov = WasmI32.fromGrain([> 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]) - -provide let panic = (msg: String) => { - let ptr = WasmI32.fromGrain(msg) - let written = iov + 32n - let lf = iov + 36n - WasmI32.store(iov, ptr + 8n, 0n) - WasmI32.store(iov, WasmI32.load(ptr, 4n), 4n) - WasmI32.store8(lf, 10n, 0n) - WasmI32.store(iov, lf, 8n) - WasmI32.store(iov, 1n, 12n) - fd_write(2n, iov, 2n, written) - unreachable() + exceptionToString(e, printers) } +/** + * Throws an uncatchable exception and traps. + * + * @param e: The exception to throw + */ provide let panicWithException = (e: Exception) => { - panic(exceptionToString(e)) + Panic.panic(toString(e)) } +// Runtime exceptions + provide exception DivisionByZero provide exception ModuloByZero provide exception Overflow @@ -126,4 +104,4 @@ let runtimeErrorPrinter = e => { } } -dangerouslyRegisterPrinter(runtimeErrorPrinter) +registerPrinter(runtimeErrorPrinter) diff --git a/stdlib/runtime/exception.md b/stdlib/runtime/exception.md index 44a1447620..6214bd283c 100644 --- a/stdlib/runtime/exception.md +++ b/stdlib/runtime/exception.md @@ -6,33 +6,84 @@ title: Exception Functions and constants included in the Exception module. -### Exception.**printers** +### Exception.**registerBasePrinter** + +
+Added in next +No other changes yet. +
```grain -printers : WasmI32 +registerBasePrinter : (printer: (Exception => String)) => Void ``` -### Exception.**dangerouslyRegisterBasePrinter** +Registers a base exception printer. If no other exception printers are +registered, the base printer is used to convert an exception to a string. -```grain -dangerouslyRegisterBasePrinter : (f: a) => Void -``` +Parameters: + +|param|type|description| +|-----|----|-----------| +|`printer`|`Exception => String`|The base exception printer to register| + +### Exception.**registerPrinter** -### Exception.**dangerouslyRegisterPrinter** +
+Added in next +No other changes yet. +
```grain -dangerouslyRegisterPrinter : (f: a) => Void +registerPrinter : (printer: (Exception => Option)) => Void ``` -### Exception.**panic** +Registers an exception printer. When an exception is thrown, all registered +printers are called in order from the most recently registered printer to +the least recently registered printer. The first `Some` value returned is +used as the exception's string value. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`printer`|`Exception => Option`|The exception printer to register| + +### Exception.**toString** + +
+Added in next +No other changes yet. +
```grain -panic : (msg: String) => a +toString : (e: Exception) => String ``` +Gets the string representation of the given exception. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`e`|`Exception`|The exception to stringify| + +Returns: + +|type|description| +|----|-----------| +|`String`|The string representation of the exception| + ### Exception.**panicWithException** ```grain panicWithException : (e: Exception) => a ``` +Throws an uncatchable exception and traps. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`e`|`Exception`|The exception to throw| + diff --git a/stdlib/runtime/gc.gr b/stdlib/runtime/gc.gr index a9c764ef01..34d82473aa 100644 --- a/stdlib/runtime/gc.gr +++ b/stdlib/runtime/gc.gr @@ -21,21 +21,19 @@ module GC from "runtime/malloc" include Malloc from "runtime/unsafe/tags" include Tags +from "runtime/unsafe/panic" include Panic from "runtime/unsafe/wasmi32" include WasmI32 use WasmI32.{ (+), (-), (*), (&), (==), (!=) } -// Using foreigns directly here to avoid cyclic dependency -foreign wasm fd_write: - (WasmI32, WasmI32, WasmI32, WasmI32) => WasmI32 from "wasi_snapshot_preview1" - primitive (&&) = "@and" primitive (||) = "@or" -primitive throw = "@throw" primitive ignore = "@ignore" primitive box = "@box" primitive unbox = "@unbox" -exception DecRefError +let throwDecRefError = () => { + Panic.panic("DecRefError: Reference count of zero") +} let mut _DEBUG = false @@ -89,7 +87,7 @@ let rec decRef = (userPtr: WasmI32, ignoreZeros: Bool) => { if (ignoreZeros) { userPtr } else { - throw DecRefError + throwDecRefError() } } else { let refCount = refCount - 1n diff --git a/stdlib/runtime/malloc.gr b/stdlib/runtime/malloc.gr index 1a83d90a68..8bbbb94dd1 100644 --- a/stdlib/runtime/malloc.gr +++ b/stdlib/runtime/malloc.gr @@ -21,7 +21,7 @@ use WasmI32.{ (&), (^), } -from "runtime/exception" include Exception +from "runtime/unsafe/panic" include Panic primitive memorySize = "@wasm.memory_size" primitive memoryGrow = "@wasm.memory_grow" @@ -38,12 +38,12 @@ primitive heapStart = "@heap.start" * lists are maintained, one for small blocks of 64 bytes, and one for larger * blocks of multiples of 64 bytes. Each block has an 8-byte header and 8-byte * footer to keep track of block sizes and maintain the free list. - * + * * Most allocations in programs are small, so the separate free lists allow us * to implement `malloc` and `free` in O(1) for small allocations and O(n) * `malloc` and O(1) `free` for large allocations, where `n` is the size of the * free list for large blocks. - * + * * The small blocks are able to service: * - Numbers (with the exception of large BigInts/Rationals) * - Tuples/Arrays up to 8 elements @@ -51,7 +51,7 @@ primitive heapStart = "@heap.start" * - Variants up to 5 elements * - Closures up to 6 elements * - Bytes/Strings up to length 32 - * + * * Blocks in memory look like this: * * 8 bytes 8 bytes 64n - 16 bytes 8 bytes 8 bytes @@ -71,11 +71,11 @@ primitive heapStart = "@heap.start" * * The size is kept in the header and footer to allow us to quickly combine * free blocks when blocks are freed. - * + * * Pointers to the previous/next free blocks give us doubly-linked free lists, * which makes it possible to remove blocks from the free list in constant * time. - * + * * A block is considered in use when the previous/next pointers are both zero. */ @@ -329,7 +329,7 @@ let morecore = (nunits: WasmI32) => { // If there was an error, fail if (cp == -1n) { - Exception.panic("OutOfMemory: Maximum memory size exceeded") + Panic.panic("OutOfMemory: Maximum memory size exceeded") } else { // Set up the block. We'll add dummy headers/footers before and after the // block to avoid unnecessary bounds checks elsewhere in the code. diff --git a/stdlib/runtime/unsafe/panic.gr b/stdlib/runtime/unsafe/panic.gr new file mode 100644 index 0000000000..8c64400b5c --- /dev/null +++ b/stdlib/runtime/unsafe/panic.gr @@ -0,0 +1,28 @@ +@runtimeMode +module Panic + +from "runtime/unsafe/wasmi32" include WasmI32 +use WasmI32.{ (+) } + +foreign wasm fd_write: + (WasmI32, WasmI32, WasmI32, WasmI32) => WasmI32 from "wasi_snapshot_preview1" + +primitive unreachable = "@unreachable" + +// HACK: Allocate static buffer for printing (40 bytes) +// Would be nice to have a better way to allocate a static block from +// the runtime heap, but this is the only module that needs to do it +let iov = WasmI32.fromGrain([> 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]) + +provide let panic = (msg: String) => { + let ptr = WasmI32.fromGrain(msg) + let written = iov + 32n + let lf = iov + 36n + WasmI32.store(iov, ptr + 8n, 0n) + WasmI32.store(iov, WasmI32.load(ptr, 4n), 4n) + WasmI32.store8(lf, 10n, 0n) + WasmI32.store(iov, lf, 8n) + WasmI32.store(iov, 1n, 12n) + fd_write(2n, iov, 2n, written) + unreachable() +} diff --git a/stdlib/runtime/unsafe/panic.md b/stdlib/runtime/unsafe/panic.md new file mode 100644 index 0000000000..883a1906ec --- /dev/null +++ b/stdlib/runtime/unsafe/panic.md @@ -0,0 +1,14 @@ +--- +title: Panic +--- + +## Values + +Functions and constants included in the Panic module. + +### Panic.**panic** + +```grain +panic : (msg: String) => a +``` +