muscript: add exception handling
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
28b08998b1
commit
56cd5a4aab
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue