muscript: add exception handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
28b08998b1
commit
56cd5a4aab
|
@ -1,12 +1,18 @@
|
||||||
package io.gitlab.jfronny.muscript;
|
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.Scope;
|
||||||
import io.gitlab.jfronny.muscript.data.dynamic.*;
|
import io.gitlab.jfronny.muscript.data.dynamic.*;
|
||||||
import io.gitlab.jfronny.muscript.data.dynamic.additional.*;
|
import io.gitlab.jfronny.muscript.data.dynamic.additional.*;
|
||||||
|
import io.gitlab.jfronny.muscript.error.LocationalException;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal.of;
|
import static io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal.of;
|
||||||
|
@ -72,7 +78,10 @@ public class StandardLib {
|
||||||
.set("callableObject", StandardLib::callableObject)
|
.set("callableObject", StandardLib::callableObject)
|
||||||
.set("enum", StandardLib::enum_)
|
.set("enum", StandardLib::enum_)
|
||||||
.set("keys", StandardLib::keys)
|
.set("keys", StandardLib::keys)
|
||||||
.set("values", StandardLib::values);
|
.set("values", StandardLib::values)
|
||||||
|
|
||||||
|
.set("fail", StandardLib::fail)
|
||||||
|
.set("try", StandardLib::try_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numbers
|
// 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());
|
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());
|
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) {
|
} catch (DynamicTypeConversionException e) {
|
||||||
throw e.locational(location);
|
throw e.locational(location);
|
||||||
} catch (RuntimeException e) {
|
} 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()
|
final Scope scope = StandardLib.createScope()
|
||||||
.set("throw", args -> {
|
.set("throw", args -> {
|
||||||
throw new IllegalArgumentException("No");
|
throw new IllegalArgumentException("Expected Exception");
|
||||||
});
|
});
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void stackTrace() {
|
void stackTrace() {
|
||||||
assertEquals("""
|
assertEquals("""
|
||||||
Error at '(' (character 8): Could not perform call successfully
|
Error at '(' (character 8): Expected Exception
|
||||||
1 | throw()
|
1 | throw()
|
||||||
^-- Here
|
^-- Here
|
||||||
at someInner (call: line 5)
|
at someInner (call: line 5)
|
||||||
|
@ -42,7 +42,7 @@ class StackTraceTest {
|
||||||
@Test
|
@Test
|
||||||
void stackTrace2() {
|
void stackTrace2() {
|
||||||
assertEquals("""
|
assertEquals("""
|
||||||
Error at '(' (character 8): Could not perform call successfully
|
Error at '(' (character 8): Expected Exception
|
||||||
1 | throw()
|
1 | throw()
|
||||||
^-- Here
|
^-- Here
|
||||||
at someInner (call: line 5 in some/file.mu)
|
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]
|
[2, 'some expression(s)', 10, 7, 7, 2, 1, 1, 12, 24]
|
||||||
```
|
```
|
||||||
</details>
|
</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
Block a user