muscript: add exception handling
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Johannes Frohnmeyer 2023-06-09 14:12:21 +02:00
parent 28b08998b1
commit 56cd5a4aab
Signed by: Johannes
GPG Key ID: E76429612C2929F4
5 changed files with 121 additions and 5 deletions

View File

@ -1,12 +1,18 @@
package io.gitlab.jfronny.muscript;
import io.gitlab.jfronny.muscript.ast.Expr;
import io.gitlab.jfronny.muscript.ast.dynamic.Call;
import io.gitlab.jfronny.muscript.ast.dynamic.Variable;
import io.gitlab.jfronny.muscript.compiler.CodeLocation;
import io.gitlab.jfronny.muscript.data.Scope;
import io.gitlab.jfronny.muscript.data.dynamic.*;
import io.gitlab.jfronny.muscript.data.dynamic.additional.*;
import io.gitlab.jfronny.muscript.error.LocationalException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal.of;
@ -72,7 +78,10 @@ public class StandardLib {
.set("callableObject", StandardLib::callableObject)
.set("enum", StandardLib::enum_)
.set("keys", StandardLib::keys)
.set("values", StandardLib::values);
.set("values", StandardLib::values)
.set("fail", StandardLib::fail)
.set("try", StandardLib::try_);
}
// Numbers
@ -239,4 +248,43 @@ public class StandardLib {
if (args.size() != 1) throw new IllegalArgumentException("Invalid number of arguments for values: expected 1 but got " + args.size());
return of(args.get(0).asObject().getValue().values().stream().toList());
}
public static DObject try_(DList args) {
if (args.size() == 0) throw new IllegalArgumentException("Invalid number of arguments for try: expected 1 or more but got " + args.size());
var callable = args.get(0).asCallable();
var l = args.getValue();
var innerArgs = of(l.subList(1, l.size()));
Supplier<Expr<?>> serializedCatch = () ->
new Call(CodeLocation.NONE,
new Variable(CodeLocation.NONE, "call"),
args.getValue().stream().map(a -> new Call.Arg(a.toExpr().asDynamicExpr(), false)).toList()
);
try {
var result = callable.call(innerArgs);
return of(Map.of(
"result", result,
"catch", of("catch", param -> {
if (param.size() != 1) throw new IllegalArgumentException("Invalid number of arguments for catch: expected 1 but got " + param.size());
param.get(0).asCallable();
return of(Map.of("result", result));
}, serializedCatch)
));
} catch (LocationalException le) {
return of(Map.of(
"result", new DNull(),
"catch", of("catch", param -> {
if (param.size() != 1) throw new IllegalArgumentException("Invalid number of arguments for catch: expected 1 but got " + param.size());
var result = param.get(0).asCallable().call(of(Map.of(
"message", of(le.getMessage())
)));
return of(Map.of("result", result));
}, serializedCatch)
));
}
}
public static DNull fail(DList args) {
if (args.size() > 1) throw new IllegalArgumentException("Invalid number of arguments for fail: expected 0 or 1 but got " + args.size());
throw new RuntimeException(args.size() == 0 ? "Failed" : args.get(0).asString().getValue());
}
}

View File

@ -46,7 +46,7 @@ public class Call extends DynamicExpr {
} catch (DynamicTypeConversionException e) {
throw e.locational(location);
} catch (RuntimeException e) {
throw new LocationalException(location, "Could not perform call successfully", e);
throw new LocationalException(location, e.getMessage(), e);
}
}

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.muscript.test;
import io.gitlab.jfronny.muscript.compiler.Parser;
import io.gitlab.jfronny.muscript.error.LocationalException;
import org.junit.jupiter.api.Test;
import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs;
import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.string;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ExceptionHandlingTest {
@Test
void catchStdlib() {
assertEquals("Invalid number of arguments for isEmpty: expected 1 but got 0", assertThrows(LocationalException.class, () -> string("isEmpty()")).getMessage());
assertEquals("Invalid number of arguments for isEmpty: expected 1 but got 0", string("try({->isEmpty()}).catch({e->e.message}).result"));
}
@Test
void fail() {
assertEquals("Failed", assertThrows(LocationalException.class, () -> string("fail()")).getMessage());
assertEquals("Joe", assertThrows(LocationalException.class, () -> string("fail('Joe')")).getMessage());
}
@Test
void catchFail() {
assertEquals("Cought Joe", string("try({->fail('Joe')}).catch({e->'Cought ' || e.message}).result"));
}
@Test
void catchInner() {
assertEquals("Got Invalid number of arguments for isEmpty: expected 1 but got 0", assertThrows(LocationalException.class, () -> Parser.parseScript("""
inner = {-> isEmpty()}
outer = {-> inner()}
outer2 = {-> try({a->a()}, outer).catch({e -> fail('Got ' || e.message)}).result}
outer2()
""").run(makeArgs())).getMessage());
}
}

View File

@ -24,13 +24,13 @@ class StackTraceTest {
final Scope scope = StandardLib.createScope()
.set("throw", args -> {
throw new IllegalArgumentException("No");
throw new IllegalArgumentException("Expected Exception");
});
@Test
void stackTrace() {
assertEquals("""
Error at '(' (character 8): Could not perform call successfully
Error at '(' (character 8): Expected Exception
1 | throw()
^-- Here
at someInner (call: line 5)
@ -42,7 +42,7 @@ class StackTraceTest {
@Test
void stackTrace2() {
assertEquals("""
Error at '(' (character 8): Could not perform call successfully
Error at '(' (character 8): Expected Exception
1 | throw()
^-- Here
at someInner (call: line 5 in some/file.mu)

View File

@ -231,3 +231,32 @@ Result:
[2, 'some expression(s)', 10, 7, 7, 2, 1, 1, 12, 24]
```
</details>
## Exception Handling
μScript supports basic exception handling.
This means you can throw and catch exceptions, both those you throw in μScript and those in your Java code.
The function `fail` throws an exception with the supplied message (optional) or "Failed" if it is omitted.
You may use the function `try` to catch exceptions with `catch`.
<details>
<summary>Example</summary>
<br>
```mu
someFunction = { -> fail("You did something wrong") } // This is an example. Usually, you'd do this after some checks.
listOf(
try(someFunction).result, // You can access the result of your method (or null if it failed) directly like this
try({->"Yay"}).result, // if it succeeds, you get its result
try(someFunction).catch({e->"Cought!"}).result, // if a catch is present and it fails, the result of catch will be used instead
try(someFunction).catch({e->e.message}).result, // the parameter to catch represents the cought exception
try({a-> "ABC" || a}, "DEF").result // try also supports adding parameters to your first arg
)
// Note: You may not catch twice. Doing so will lead to an exception
```
Result:
```
[null, 'Yay', 'Cought!', 'You did something wrong', 'ABCDEF']
```
</details>