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(stdlib): Add Exception.toString #2143

Merged
merged 7 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions compiler/test/stdlib/exception.test.gr
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion compiler/test/suites/basic_functionality.re
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,6 @@ describe("basic functionality", ({test, testSkip}) => {
~config_fn=smallestFileConfig,
"smallest_grain_program",
"",
5165,
4750,
);
});
1 change: 1 addition & 0 deletions compiler/test/suites/stdlib.re
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
20 changes: 11 additions & 9 deletions stdlib/exception.gr
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ from "runtime/exception" include Exception
*
* @since v0.3.0
*/
@disableGC
provide let rec registerPrinter = (printer: Exception => Option<String>) => {
// 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
25 changes: 25 additions & 0 deletions stdlib/exception.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,28 @@ Exception.registerPrinter(e => {
throw ExampleError(1) // Error found on line: 1
```

### Exception.**toString**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```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|

4 changes: 2 additions & 2 deletions stdlib/pervasives.gr
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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))
}
}

Expand Down
142 changes: 60 additions & 82 deletions stdlib/runtime/exception.gr
Original file line number Diff line number Diff line change
@@ -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<String>, 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<String>, 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<String>) =>
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
Expand Down Expand Up @@ -126,4 +104,4 @@ let runtimeErrorPrinter = e => {
}
}

dangerouslyRegisterPrinter(runtimeErrorPrinter)
registerPrinter(runtimeErrorPrinter)
71 changes: 61 additions & 10 deletions stdlib/runtime/exception.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,84 @@ title: Exception

Functions and constants included in the Exception module.

### Exception.**printers**
### Exception.**registerBasePrinter**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```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**
<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
dangerouslyRegisterPrinter : (f: a) => Void
registerPrinter : (printer: (Exception => Option<String>)) => 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<String>`|The exception printer to register|

### Exception.**toString**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```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|

Loading
Loading