Skip to content

Commit

Permalink
feat(stdlib): Add Exception.toString (#2143)
Browse files Browse the repository at this point in the history
  • Loading branch information
spotandjake authored Sep 22, 2024
1 parent f97c011 commit 0894dc5
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 118 deletions.
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

0 comments on commit 0894dc5

Please sign in to comment.