From beaba5cb024365efd39fe0b1c01cbbda014fbf42 Mon Sep 17 00:00:00 2001 From: JFronny Date: Sun, 7 Apr 2024 17:18:48 +0200 Subject: [PATCH] chore(muscript): copy tests and documentation over to new implementation --- muscript-runtime/README.md | 67 +++++ muscript-runtime/StandardLib.md | 77 +++++ muscript-runtime/build.gradle.kts | 2 + .../jfronny/muscript/test/AssignTest.java | 37 +++ .../jfronny/muscript/test/BindTest.java | 42 +++ .../jfronny/muscript/test/BooleanTest.java | 26 ++ .../jfronny/muscript/test/CallableTest.java | 19 ++ .../jfronny/muscript/test/ClosureTest.java | 48 ++++ .../muscript/test/CombinationTest.java | 17 ++ .../jfronny/muscript/test/CommentTest.java | 45 +++ .../jfronny/muscript/test/DecompileTest.java | 35 +++ .../muscript/test/ExceptionHandlingTest.java | 38 +++ .../jfronny/muscript/test/GlobalTest.java | 40 +++ .../jfronny/muscript/test/ListTest.java | 24 ++ .../muscript/test/LocationalErrorTest.java | 28 ++ .../muscript/test/MultiScriptTest.java | 41 +++ .../jfronny/muscript/test/NumberTest.java | 38 +++ .../jfronny/muscript/test/ObjectTest.java | 39 +++ .../jfronny/muscript/test/StackTraceTest.java | 56 ++++ .../muscript/test/StarScriptIngesterTest.java | 15 + .../jfronny/muscript/test/StringTest.java | 24 ++ .../jfronny/muscript/test/TernaryTest.java | 21 ++ .../muscript/test/ValidExampleTest.java | 71 +++++ .../jfronny/muscript/test/VaragsTest.java | 28 ++ .../muscript/test/util/MuTestUtil.java | 74 +++++ .../muscript/test/util/UnforkableScope.java | 10 + .../src/test/resources/example.md | 265 ++++++++++++++++++ muscript/README.md | 66 +---- muscript/StandardLib.md | 76 +---- muscript/src/test/resources/example.md | 264 +---------------- 30 files changed, 1230 insertions(+), 403 deletions(-) create mode 100644 muscript-runtime/README.md create mode 100644 muscript-runtime/StandardLib.md create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/AssignTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BindTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BooleanTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CallableTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ClosureTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CombinationTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/DecompileTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ExceptionHandlingTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/GlobalTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ListTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/MultiScriptTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/NumberTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ObjectTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StarScriptIngesterTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/TernaryTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/VaragsTest.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java create mode 100644 muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/UnforkableScope.java create mode 100644 muscript-runtime/src/test/resources/example.md diff --git a/muscript-runtime/README.md b/muscript-runtime/README.md new file mode 100644 index 0000000..99e5408 --- /dev/null +++ b/muscript-runtime/README.md @@ -0,0 +1,67 @@ +# μScript + +μScript was created to allow respackopts pack authors to specify conditions for loading resources +in a more human-friendly manner than the previous json tree-based system. + +It is intended to be vaguely like java in its syntax, though it deviates in some aspects. + +The language is parsed into an AST representation which is executed in-place, no compilation is performed. +μScript supports outputting data using various types, not just strings or booleans. + +The purpose of this document is to be a guide on embedding μScript in your application or library. + +For details on how to use the language, look [here](src/test/resources/example.md). + +For details on the standard library provided by μScript, look [here](StandardLib.md). + +## Embedding μScript + +μScript is available as a [maven package](https://maven.frohnmeyer-wds.de/#/artifacts/io/gitlab/jfronny/muscript) which you can add to your +project. +To use it, first parse an expression via `Parser.parse(String script)` and convert the returned generic expression to a typed +one by calling `as(Bool|String|Number|Dynamic)Expr`. +This process may throw a ParseException. +You may also use `Parser.parseScript(String script)` for multi-expression scripts. +You can call `get(Dynamic dataRoot)` on the result to execute the script on the provided data, which should be a +`Scope` which you created with `StandardLib.createScope()` to add standard methods. +This is also where you can add custom data to be accessed by your script. +The execution of a script can throw a LocationalException which may be converted to a LocationalError for printing +using the source of the expression if available. +You may also call `StarScriptIngester.starScriptToMu()` to generate μScript code from StarScript code. + +A full example could look as follows: + +```java +public class Example { + public static void main(String[] args) { + String source = String.join(" ", args); + Expr parsed; + try { + parsed = Parser.parse(source); // or Parser.parse(StarScriptIngester.starScriptToMu(source)) + } catch (Parser.ParseException e) { // Could not parse + System.err.println(e.error); + return; + } + BoolExpr typed; + try { + typed = parsed.asBoolExpr(); + } catch (LocationalException e) { + System.err.println(e.asPrintable(source)); + return; + } + Scope scope = StandardLib.createScope() + .set("someValue", 15) + .set("someOther", Map.of( + "subValue", DFinal.of(true) + )); + boolean result; + try { + result = typed.get(scope); + } catch (LocationalException e) { + System.err.println(e.asPrintable(source)); + return; + } + System.out.println("Result: " + result); + } +} +``` diff --git a/muscript-runtime/StandardLib.md b/muscript-runtime/StandardLib.md new file mode 100644 index 0000000..15777b0 --- /dev/null +++ b/muscript-runtime/StandardLib.md @@ -0,0 +1,77 @@ +# μScript Standard Library + +The purpose of this document is to be a guide to the standard library provided by μScript. +It is not intended to explain the syntax of the language or provide information on its history, implementation, or how to embed it in programs. + +For information on that, look at the [language guide](src/test/resources/example.md) and the [developer info](README.md). + +## Types +| name | description | +|----------|-----------------------------------------------------------------------------------------------------| +| number | Equivalent to a java double, the only number type in muScript | +| bool | A boolean. Either true or false | +| null | Equivalent to null in java or Unit in kotlin | +| string | A sequence of characters | +| list | An ordered sequence of items of any type | +| object | A mapping between keys (string) and values (any) | +| callable | A value that can be invoked via `()` | +| date | Represents a calendar date. Can be represented as string, number (days since 01-01-1970) and object | +| time | Represents a time of day. Can be represented as a string, number (second of day) and object | +| enum | Details below | +| try | Details below | + +### Enum +A map between names and values of any type. Can also be represented as a list of names. +One entry may be selected, its index will be accessible as the number representation and its name as the string representation of the enum. + +### Try +An `object` returned by the `try` function. Contains the following entries: +- `result`: The result of executing the closure, the result of `catch` if it failed and `catch` was called, or `null` +- `catch(handler: {string -> any})`: If the execution of the closure failed, + `handler` will be executed with the error message and its result will be provided as `result`. + No more than one `catch` may exist per `try`. + +## Functions +The standard library also provides a number of default utility functions. +Below, these are listed alongside their arguments. +The syntax used for signatures in this list is not used elsewhere and not part of the language. + +### Signature syntax +- If a parameter is suffixed with `?`, it can be omitted. If omission is equivalent to a default value, it will be listed with the default behind an equals sign +- If a parameter is suffixed with `...`, it will be variadic (support 0 or more arguments) +- If multiple types are possible, they are listed as `T1 / T2`, where T1 is preferred over T2 +- If an object supports multiple representations, it will be denoted as `T1 & T2` +- Callables are represented like closures but with types instead of names + +### Supported functions +| signature | description | +|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `round(value: number, decimalPlaces?: number = 0): number` | Rounds `value` while retaining `decimalPlaces` places after the comma | +| `floor(value: number): number` | Returns the largest integer lower than or equal to `value` | +| `ceil(value: number): number` | Returns the smallest integer larger than or equal to `value` | +| `abs(value: number): number` | Returns the absolute value of `value` (always positive or zero) | +| `random(min?: number = 0, max?: number = 1): number` | Returns a random number between `min` and `max`. If `min` is provided, `max` cannot be omitted. | +| `toUpper(value: string): string` | Converts all characters in `value` to upper case | +| `toLower(value: string): string` | Converts all characters in `value` to lower case | +| `contains(in: list / object / string, find: any): bool` | Checks whether `find` is an element of the list, a key of the object or a substring of the string provided as `in` | +| `replace(source: string, target: string, replacement: string): string` | Replaces all instances of `target` in `source` with `replacement` and returns the result | +| `listOf(elements: any...): list` | Creates a list with the provided elements | +| `len(toCheck: string / object / list): number` | Gets the amount of characters in the string or entries in the object or list provided as `toCheck` | +| `isEmpty(toCheck: object / list): bool` | Checks whether the object or list provided as `toCheck` contain any entries | +| `concat(lists: list...): list` | Returns a list containing all elements of all provided lists in their original order | +| `filter(in: list, with: {any -> bool}): list` | Returns a list of all elements `el` of `in` for which `with(el)` returned true | +| `allMatch(in: list, with: {any -> bool}): bool` | Whether `with(el)` is true for all elements `el` of `in` | +| `anyMatch(in: list, with: {any -> bool}): bool` | Whether `with(el)` is true for any element `el` of `in` | +| `map(in: list, with: {any -> any}): list` | Returns a list of the results of applying `with` to all elements of `in` | +| `flatMap(in: list, with: {any -> list}): list` | Returns the concatenated list of the results of applying `with` to all elements of `in` | +| `fold(in: list, identity: any, operator: {any, any -> any}): any` | Runs `operator(previous, current)` for every element in `in` where `previous` is the previous result of `operator`, starting with `identity` and `current` is the current element | +| `forEach(in: list, with: {any -> any}): any` | Runs `with` for each element in `in`, returning the last result or `null` for empty lists | +| `toObject(from: list, keyMapper: {any -> string}, valueMapper: {any -> any}): object` | Creates an object mapping the results of `keyMapper` to the results of `valueMapper` for every element in `from` | +| `callableObject(source: object, action: callable): object & callable` | Returns an object equivalent to `source` that can be called, resulting in the invocation of `action` | +| `enum(values: object, selected?: string): enum` | Creates an enum with the values provided as `values`, optionally marking the one with the key `selected` as such | +| `keys(ob: object): list` | Returns the list of names of entries of the object `ob` | +| `values(ob: object): list` | Returns the list of values of entries of the object `ob` | +| `try(closure: {any... -> any}, args: any...): try` | Attempts to execute the closure with the provided args and returns a `try`. For details on what to do with that, look above. | + +Other project-specific functions (like [μScript-gson](../muscript-gson/README.md)) will likely also be available to your scripts. +Please consult the documentation for the project using μScript for more information on what is available or use `this::keys()` to view everything. \ No newline at end of file diff --git a/muscript-runtime/build.gradle.kts b/muscript-runtime/build.gradle.kts index 2726d9f..1f7742d 100644 --- a/muscript-runtime/build.gradle.kts +++ b/muscript-runtime/build.gradle.kts @@ -11,6 +11,8 @@ dependencies { api(projects.muscriptDataAdditional) testImplementation(libs.junit.jupiter.api) + testImplementation(projects.muscriptParser) + testImplementation(projects.muscriptSerialize) testRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/AssignTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/AssignTest.java new file mode 100644 index 0000000..a5b2b24 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/AssignTest.java @@ -0,0 +1,37 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.ast.StringExpr; +import io.gitlab.jfronny.muscript.ast.dynamic.DynamicAssign; +import io.gitlab.jfronny.muscript.ast.string.StringLiteral; +import io.gitlab.jfronny.muscript.core.CodeLocation; +import io.gitlab.jfronny.muscript.data.additional.context.Scope; +import io.gitlab.jfronny.muscript.serialize.Decompiler; +import io.gitlab.jfronny.muscript.test.util.UnforkableScope; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.asDynamic; +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.asString; +import static io.gitlab.jfronny.muscript.runtime.Runtime.evaluate; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.number; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parse; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AssignTest { + @Test + void testAssignSimple() { + String source = "someval = 'test'"; + StringExpr expr = asString(parse(source)); + assertEquals(asString(new DynamicAssign(new CodeLocation(0, 6, source, null), "someval", asDynamic(new StringLiteral(new CodeLocation(10, 15, source, null), "test")))), expr); + assertEquals("someval = 'test'", Decompiler.decompile(expr)); + Scope scope = new UnforkableScope(); + assertEquals("test", evaluate(expr, scope)); + assertEquals("test", scope.getValue().get("someval").asString().getValue()); + } + + @Test + void testAssignInner() { + assertEquals(2, number("{->some = other = 2; other}()")); + assertEquals(2, number("{->some = other = 2; some}()")); + assertEquals(2, number("{->some = 2 + 4 * (other = 4 / 2); other}()")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BindTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BindTest.java new file mode 100644 index 0000000..57477f2 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BindTest.java @@ -0,0 +1,42 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.asNumber; +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.asString; +import static io.gitlab.jfronny.muscript.runtime.Runtime.evaluate; +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BindTest { + @Test + void simpleBind() { + assertEquals(30, run(parseScript( """ + fn = {n, b -> n*b*2} + 5::fn(3) + """), makeArgs()).asNumber().getValue()); + } + + @Test + void lists() { + assertEquals("[1, 9, 25, 49, 81]", evaluate(asString(parseScript(""" + numbers::map({n->n::function(2)})::filter({n->n%2!=0}) + """).content()), makeArgs())); + } + + @Test + void complexBind() { + // fn = {b -> 2 * b} + assertEquals(6, evaluate(asNumber(parseScript(""" + fn = 2::({a, b -> a * b}) + fn(3) + """).content()), makeArgs())); + // {a, b, c -> a(b(c))}(fn, {a -> a + 5}, 3) + assertEquals(16, evaluate(asNumber(parseScript(""" + fn = 2::({a, b -> a * b}) + fn::({a, b, c -> a(b(c))})({a -> a + 5}, 3) + """).content()), makeArgs())); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BooleanTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BooleanTest.java new file mode 100644 index 0000000..1e9273c --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/BooleanTest.java @@ -0,0 +1,26 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.bool; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.number; +import static org.junit.jupiter.api.Assertions.*; + +class BooleanTest { + @Test + void simpleLogic() { + assertTrue(bool("false | true")); + assertTrue(bool("false != true")); + assertTrue(bool("!false != false")); + assertFalse(bool("!false & false")); + assertTrue(bool("!false & true")); + assertTrue(bool("true == true")); + assertTrue(bool("false != true")); + } + + @Test + void conditional() { + assertEquals(3, number("true ? 3 : 4")); + assertEquals(4, number("false ? 3 : 4")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CallableTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CallableTest.java new file mode 100644 index 0000000..eae5292 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CallableTest.java @@ -0,0 +1,19 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CallableTest { + @Test + void basicFunctionTest() { + assertEquals(3, number("object.subfunc(0, 1, 2)")); + assertEquals(18, number("object.subfunc(0, object.subfunc(1, 2, 3), 4)")); + assertTrue(bool("repeatArgs().repeatArgs().boolean")); + assertEquals(32, number("function(object.subfunc(0, 1), 5)")); + assertEquals("<=1.16.5\"", string("object2.stringfunc('<=1.16.5\"')")); + assertEquals("minecraft", string("object2.stringfunc('minecraft', '<=1.16.5')")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ClosureTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ClosureTest.java new file mode 100644 index 0000000..2dae0bf --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ClosureTest.java @@ -0,0 +1,48 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.data.additional.DFinal; +import io.gitlab.jfronny.muscript.data.additional.context.Scope; +import io.gitlab.jfronny.muscript.parser.Parser; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.asDynamic; +import static io.gitlab.jfronny.muscript.runtime.Runtime.evaluate; +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ClosureTest { + @Test + void testScript() { + assertEquals(8, run(parseScript("function(2, 1); function(2, 2); function(2, 3)"), makeArgs()) + .asNumber() + .getValue()); + } + + @Test + void testClosure() { + assertEquals(2, number("{->2}()")); + assertEquals(2, number("{n->n}(2)")); + assertEquals(2, number("{n->n()}({->2})")); + assertThrows(Parser.ParseException.class, () -> number("{->num = 2 num = num * 2 num = num - 2}()")); + assertEquals(2, number("{->num = 2; num = num * 2; num = num - 2}()")); + } + + @Test + void fizzbuzzInμScriptByEmbeddingADomainSpecificLanguage() { + var fn = evaluate(asDynamic(parse(""" + { n -> + test = { b, s, o -> n % b == 0 ? { _ -> s || o('')} : o } + fizz = { o -> test(3, 'Fizz', o) } + buzz = { o -> test(5, 'Buzz', o) } + fizz(buzz({n -> n}))(n) + } + """)), new Scope()) + .asCallable(); + assertEquals("8", fn.call(DFinal.of(8)).asString().getValue()); + assertEquals("Buzz", fn.call(DFinal.of(10)).asString().getValue()); + assertEquals("Fizz", fn.call(DFinal.of(12)).asString().getValue()); + assertEquals("FizzBuzz", fn.call(DFinal.of(15)).asString().getValue()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CombinationTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CombinationTest.java new file mode 100644 index 0000000..0b548ed --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CombinationTest.java @@ -0,0 +1,17 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.bool; +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.assertTrue; + +class CombinationTest { + @Test + void simpleTest() { + assertTrue(bool("1 + 5 < 12 & (true != false)")); + assertEquals("59", string("15 / 3 || 2 + 7")); + assertEquals("7yes5", string("2 + 5 || (true ? 'yes' : 'no') || 15 / 3")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java new file mode 100644 index 0000000..2f7371f --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java @@ -0,0 +1,45 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CommentTest { + @Test + void singleLine() { + assertEquals(2, run(parseScript(""" + n = 5 + n = 2 + // n = 3 + n + """), makeArgs()).asNumber().getValue()); + } + + @Test + void multiLine() { + assertEquals(2, run(parseScript(""" + n = 2 + /* n = 3 */ + /* + n = 3 + n = 4 + n = 5 + */ + n + """), makeArgs()).asNumber().getValue()); + } + + @Test + void commentWithStuff() { + assertDoesNotThrow(() -> run(parseScript(""" + ob = { + k1 = 'Yes', + `1` = "One" + } + """), makeArgs())); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/DecompileTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/DecompileTest.java new file mode 100644 index 0000000..649433c --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/DecompileTest.java @@ -0,0 +1,35 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.serialize.Decompiler; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DecompileTest { + private static final String script = """ + clientVersion = challenge({ -> + mod('better-whitelist').version; + }); + assert(mod('better-whitelist').version == clientVersion, 'You have the wrong mod version'); + assert(challenge({ arg -> + allMatch(arg, { v -> + anyMatch(mods, { m -> + v.name == m.name & v.version == m.version; + }); + }); + }, map(filter(mods, { v -> + v.environment != 'server'; + }), { v -> + { + name = v.name, + version = v.version + }; + }))); + """; + + @Test + void testDecompile() { + assertEquals(script, Decompiler.decompile(parseScript(script))); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ExceptionHandlingTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ExceptionHandlingTest.java new file mode 100644 index 0000000..16d3f56 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ExceptionHandlingTest.java @@ -0,0 +1,38 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.core.LocationalException; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ExceptionHandlingTest { + @Test + void catchStdlib() { + assertEquals("Signature mismatch for isEmpty: expected <(collection: string | [any] | {any}) -> bool> but got <() -> any>", assertThrows(LocationalException.class, () -> string("isEmpty()")).getMessage()); + assertEquals("Signature mismatch for isEmpty: expected <(collection: string | [any] | {any}) -> bool> but got <() -> any>", 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 Signature mismatch for isEmpty: expected <(collection: string | [any] | {any}) -> bool> but got <() -> any>", assertThrows(LocationalException.class, () -> run(parseScript(""" + inner = {-> isEmpty()} + outer = {-> inner()} + outer2 = {-> try({a->a()}, outer).catch({e -> fail('Got ' || e.message)}).result} + outer2() + """), makeArgs())).getMessage()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/GlobalTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/GlobalTest.java new file mode 100644 index 0000000..d3ba215 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/GlobalTest.java @@ -0,0 +1,40 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.core.LocationalException; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GlobalTest { + @Test + void notAll() { + assertEquals(12, run(parseScript(""" + v = 12 + {->v = 5}() + v + """), makeArgs()).asNumber().getValue()); + } + + @Test + void global() { + assertEquals(5, run(parseScript(""" + $v = 12 + {->$v = 5}() + $v + """), makeArgs()).asNumber().getValue()); + } + + @Test + void innermost() { + assertThrows(LocationalException.class, + () -> run(parseScript(""" + {->$v = 12}() + $v + """), makeArgs()).asNumber().getValue(), + "This object doesn't contain '$v'"); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ListTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ListTest.java new file mode 100644 index 0000000..cd9c7c8 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ListTest.java @@ -0,0 +1,24 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.bool; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ListTest { + @Test + void listAccess() { + assertTrue(bool("list[0]")); + assertTrue(bool("list[1] < 3")); + assertTrue(bool("list[2] || list[0] == '3true'")); + } + + @Test + void contains() { + assertFalse(bool("numbers::contains(',')")); + assertFalse(bool("numbers::contains('1,')")); + assertFalse(bool("numbers::contains('1')")); + assertTrue(bool("numbers::contains(1)")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java new file mode 100644 index 0000000..0b21096 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java @@ -0,0 +1,28 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.parser.Parser; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class LocationalErrorTest { + @Test + void invalidCode() { + assertEquals(""" + Error at 't' (character 6): Expected number expression but got boolean + 1 | 15 + true + ^--^-- Here""", + assertThrows(Parser.ParseException.class, () -> parse("15 + true")).error.toString()); + } + + @Test + void invalidCode2() { + assertEquals(""" + Error at ''' (character 6): Expected number expression but got string + 1 | 15 + 'yes' + ^---^-- Here""", + assertThrows(Parser.ParseException.class, () -> parse("15 + 'yes'")).error.toString()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/MultiScriptTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/MultiScriptTest.java new file mode 100644 index 0000000..e9f8bce --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/MultiScriptTest.java @@ -0,0 +1,41 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.core.MuScriptVersion; +import io.gitlab.jfronny.muscript.core.SourceFS; +import io.gitlab.jfronny.muscript.data.additional.context.Scope; +import io.gitlab.jfronny.muscript.data.additional.libs.StandardLib; +import io.gitlab.jfronny.muscript.parser.Parser; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MultiScriptTest { + final SourceFS FS = new SourceFS.Immutable(Map.of( + "main.mu", """ + #include import1.mu + func(4)""", + "import1.mu", """ + #include import2.mu + func = {c->fn2(c-1, c)}""", + "import2.mu", """ + #include import3.mu + fn = {a->a}""", + "import3.mu", """ + #include import2.mu + fn = {a->a*2} + fn2 = {a, b -> fn(a)+b}""" + )); + + final Scope scope = StandardLib.createScope(MuScriptVersion.DEFAULT) + .set("throw", args -> { + throw new IllegalArgumentException("No"); + }); + + @Test + void simpleMultifile() { + assertEquals(7, run(Parser.parseMultiScript(MuScriptVersion.DEFAULT, "main.mu", FS), scope).asNumber().getValue()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/NumberTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/NumberTest.java new file mode 100644 index 0000000..84097e2 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/NumberTest.java @@ -0,0 +1,38 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.bool; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.number; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NumberTest { + @Test + void simpleMath() { + assertEquals(12, number("7 + 5")); + assertEquals(12, number("14 - 2")); + assertEquals(12, number("4* 3")); + assertEquals(12, number("24 /2")); + assertEquals(12, number("92 % 20")); + assertEquals(12, number("2^2*3")); + assertEquals(-12, number("-12")); + assertEquals(12, number("-1 * -12 + 2 + (-2)")); + } + + @Test + void compare() { + assertTrue(bool("12 < 10 * 2")); + assertTrue(bool("12 > 14 / 2")); + assertTrue(bool("12 == 10 + 2")); + assertTrue(bool("12 >= 10 + 2")); + assertTrue(bool("10 <= 10 + 2")); + assertTrue(bool("12 != 10 * 2")); + } + + @Test + void orderOfOperations() { + assertEquals(12, number("2 + 5 * 2")); + assertEquals(12, number("3*2^2")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ObjectTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ObjectTest.java new file mode 100644 index 0000000..be075f5 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ObjectTest.java @@ -0,0 +1,39 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ObjectTest { + @Test + void valueAccess() { + assertTrue(bool("boolean")); + assertTrue(bool("object.subvalue > 1000")); + assertTrue(bool("object.subfunc(2, 4, 8) == 12")); + } + + @Test + void arrayAccess() { + assertTrue(bool("object[1] == 'One'")); + assertTrue(bool("object['subvalue'] == 1024")); + assertTrue(bool("object[object2.valuename] == 1024")); + assertTrue(bool("object2['sub'].val == 10")); + assertTrue(bool("object2.sub['val'] == 10")); + } + + @Test + void objectLiteral() { + assertEquals(12, run(parseScript(""" + ob = {} + ob = {test = 2, test2 = 3} + t = ob.test + ob = {test2 = 3, test = 2} + t = t * ob.test + ob = {test = 3} + t * ob.test + """), makeArgs()).asNumber().getValue()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java new file mode 100644 index 0000000..ec11d86 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java @@ -0,0 +1,56 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.core.LocationalException; +import io.gitlab.jfronny.muscript.core.MuScriptVersion; +import io.gitlab.jfronny.muscript.data.additional.context.Scope; +import io.gitlab.jfronny.muscript.data.additional.libs.IntentionalException; +import io.gitlab.jfronny.muscript.data.additional.libs.StandardLib; +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class StackTraceTest { + final String source = """ + someInner = { -> + throw() + } + + someOuter = { -> + someInner() + } + + someOuter() + """; + + final Scope scope = StandardLib.createScope(MuScriptVersion.DEFAULT) + .set("throw", args -> { + throw new IntentionalException("Expected Exception"); + }); + + @Test + void stackTrace() { + assertEquals(""" + Error at '(' (character 8): Expected Exception + 1 | throw() + ^-- Here + at someInner (call: line 5) + at someOuter (call: line 8)""", + assertThrows(LocationalException.class, () -> run(parseScript(source), scope)) + .asPrintable().toString()); + } + + @Test + void stackTrace2() { + assertEquals(""" + Error at '(' (character 8): Expected Exception + 1 | throw() + ^-- Here + at someInner (call: line 5 in some/file.mu) + at someOuter (call: line 8 in some/file.mu)""", + assertThrows(LocationalException.class, () -> run(parseScript(source, "some/file.mu"), scope)) + .asPrintable().toString()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StarScriptIngesterTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StarScriptIngesterTest.java new file mode 100644 index 0000000..9cb2c30 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StarScriptIngesterTest.java @@ -0,0 +1,15 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.muscript.parser.StarScriptIngester; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StarScriptIngesterTest { + @Test + void ingestionTest() { + assertEquals("'Hello ' || (name) || '!'", StarScriptIngester.starScriptToMu("Hello {name}!")); + assertEquals("('Only \"Content')", StarScriptIngester.starScriptToMu("{'Only \"Content'}")); + assertEquals("(a + c / 75) || ' equals ' || (b)", StarScriptIngester.starScriptToMu("{a + c / 75} equals {b}")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java new file mode 100644 index 0000000..7299114 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java @@ -0,0 +1,24 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.bool; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.string; +import static org.junit.jupiter.api.Assertions.*; + +class StringTest { + @Test + void operators() { + assertEquals("Hello, world!", string("'Hello, ' || 'world!'")); + assertEquals("Yes 15 hello", string("'Yes ' || 16 - 1 || ' hello'")); + assertEquals("Value", string("string")); + assertTrue(bool("string == 'Value'")); + assertFalse(bool("string == 'Something else'")); + } + + @Test + void concatComparison() { + assertEquals("Hellotrue", string("'Hello' || -12 < 5")); + assertEquals("trueHello", string("-12 < 5 || 'Hello'")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/TernaryTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/TernaryTest.java new file mode 100644 index 0000000..49d8edb --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/TernaryTest.java @@ -0,0 +1,21 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.runtime.Runtime.run; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TernaryTest { + @Test + void testRootTernary() { + assertEquals(2, run(parseScript(""" + value = 0; + value == 1 ? joe() : value = 1; + value == 1 ? value = 2 : joe(); + value == 1 ? value = 3 : value; + value; + """), makeArgs()).asNumber().getValue()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java new file mode 100644 index 0000000..006b6ad --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java @@ -0,0 +1,71 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.commons.StringFormatter; +import io.gitlab.jfronny.muscript.core.LocationalException; +import io.gitlab.jfronny.muscript.core.MuScriptVersion; +import io.gitlab.jfronny.muscript.data.additional.libs.StandardLib; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.asString; +import static io.gitlab.jfronny.muscript.runtime.Runtime.evaluate; +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.parseScript; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ValidExampleTest { + @Test + void assertValidExample() throws IOException { + final String source; + try (InputStream is = Objects.requireNonNull(ValidExampleTest.class.getClassLoader().getResourceAsStream("example.md"))) { + source = new String(is.readAllBytes()); + } + String[] split = source.lines().toArray(String[]::new); + List testCases = new LinkedList<>(); + for (int i = 0; i < split.length; i++) { + String s = split[i]; + if (s.equals("```mu")) { + StringBuilder blockBuilder = new StringBuilder(); + while (!split[++i].equals("```")) { + blockBuilder.append('\n').append(split[i]); + } + assertEquals("```", split[i++]); + assertEquals("Result:", split[i++]); + assertEquals("```", split[i]); + StringBuilder resultBuilder = new StringBuilder(); + while (!split[++i].equals("```")) { + resultBuilder.append('\n').append(split[i]); + } + i++; + final String block = blockBuilder.substring(1); + final String expectedResult = resultBuilder.substring(1); + testCases.add(new TestCase(block, expectedResult)); + } + } + + assertAll(testCases); + } + + record TestCase(String source, String expectedResult) implements Executable { + @Override + public void execute() { + String result = null; + try { + result = evaluate(asString(parseScript(source).content()), StandardLib.createScope(MuScriptVersion.DEFAULT)); + } catch (Throwable t) { + assertEquals(expectedResult, StringFormatter.toString(t, e -> + e instanceof LocationalException le + ? le.asPrintable().toString() + : e.toString() + )); + } + if (result != null) assertEquals(expectedResult, result); + } + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/VaragsTest.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/VaragsTest.java new file mode 100644 index 0000000..6517fda --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/VaragsTest.java @@ -0,0 +1,28 @@ +package io.gitlab.jfronny.muscript.test; + +import org.junit.jupiter.api.Test; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.number; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class VaragsTest { + @Test + void basicListOf() { + assertEquals(2, number("listOf(1, 2, 3, 4)[2] - 1")); + } + + @Test + void variadicFunction() { + assertEquals(2, number("{joe...->joe[2] - 1}(1, 2, 3, 4)")); + } + + @Test + void variadicParameter() { + assertEquals(2, number("{n...->n[2] - 1}(listOf(1, 2, 3, 4)...)")); + } + + @Test + void variadicFootgun() { + assertEquals(2, number("{a, b, c, d -> c - 1}(listOf(1, 2, 3, 4)...)")); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java new file mode 100644 index 0000000..e4db29f --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java @@ -0,0 +1,74 @@ +package io.gitlab.jfronny.muscript.test.util; + +import io.gitlab.jfronny.muscript.ast.Expr; +import io.gitlab.jfronny.muscript.ast.context.Script; +import io.gitlab.jfronny.muscript.core.MuScriptVersion; +import io.gitlab.jfronny.muscript.data.additional.context.Scope; +import io.gitlab.jfronny.muscript.data.additional.impl.ObjectGraphPrinter; +import io.gitlab.jfronny.muscript.data.additional.libs.StandardLib; +import io.gitlab.jfronny.muscript.parser.Parser; + +import java.util.Map; + +import static io.gitlab.jfronny.muscript.ast.context.ExprUtils.*; +import static io.gitlab.jfronny.muscript.data.additional.DFinal.of; +import static io.gitlab.jfronny.muscript.runtime.Runtime.evaluate; + +public class MuTestUtil { + public static double number(String source) { + return evaluate(asNumber(parse(source)), makeArgs()); + } + + public static boolean bool(String source) { + Expr tree = parse(source); + try { + return evaluate(asBool(tree), makeArgs()); + } catch (RuntimeException e) { + try { + System.out.println("Caught error with tree:\n" + ObjectGraphPrinter.printGraph(tree)); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + throw e; + } + } + + public static String string(String source) { + return evaluate(asString(parse(source)), makeArgs()); + } + + public static Expr parse(String source) { + return Parser.parse(MuScriptVersion.DEFAULT, source); + } + + public static Script parseScript(String source) { + return Parser.parseScript(MuScriptVersion.DEFAULT, source); + } + + public static Script parseScript(String source, String file) { + return Parser.parseScript(MuScriptVersion.DEFAULT, source, file); + } + + public static Scope makeArgs() { + return StandardLib.createScope(MuScriptVersion.DEFAULT) + .set("boolean", true) + .set("number", 15) + .set("string", "Value") + .set("object", Map.of( + "subvalue", of(1024), + "subfunc", of(v -> of(v.get(1).asNumber().getValue() * v.size()), "object.subfunc"), + "1", of("One") + )) + .set("object2", Map.of( + "valuename", of("subvalue"), + "sub", of(Map.of( + "val", of(10) + )), + "stringfunc", of(v -> of(v.get(0).asString().getValue()), "object2.stringfunc") + )) + .set("list", of(of(true), of(2), of("3"))) + .set("numbers", of(of(1), of(2), of(3), of(4), of(5), of(6), of(7), of(8), of(9), of(10))) + .set("function", v -> of(Math.pow(v.get(0).asNumber().getValue(), v.get(1).asNumber().getValue()))) + .set("repeatArgs", v -> makeArgs()); + } +} diff --git a/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/UnforkableScope.java b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/UnforkableScope.java new file mode 100644 index 0000000..48ae951 --- /dev/null +++ b/muscript-runtime/src/test/java/io/gitlab/jfronny/muscript/test/util/UnforkableScope.java @@ -0,0 +1,10 @@ +package io.gitlab.jfronny.muscript.test.util; + +import io.gitlab.jfronny.muscript.data.additional.context.Scope; + +public class UnforkableScope extends Scope { + @Override + public Scope fork() { + return this; + } +} diff --git a/muscript-runtime/src/test/resources/example.md b/muscript-runtime/src/test/resources/example.md new file mode 100644 index 0000000..0fef8f2 --- /dev/null +++ b/muscript-runtime/src/test/resources/example.md @@ -0,0 +1,265 @@ +# μScript + +The purpose of this document is to be a guide to using the μScript language. +It is not intended to explain any history of the language, implementation details or details on how to embed μScript in programs. + +For information on that, look [here](../../../README.md). + +For information on the standard library provided by μScript, look [here](../../../StandardLib.md) + +## Types +μScript supports a couple of basic types, namely: +- [Numbers](#numbers) (double-precision floating-point) +- [Booleans](#booleans) +- [Strings](#strings) +- [Objects](#objects) +- [Lists](#lists) +- [Functions](#functions) + +Numbers, booleans and strings can be created just like in java. + +## Numbers + +You can use the most common math operations on numbers. +That includes addition, subtraction, multiplication, division, modulo, pow, and your standard comparison operators. + +Order of operation is also supported. + +The StdLib comes with additional constants and functions, namely: `PI`, `E`, `round`, `floor`, `ceil`, `abs`, `random` + +
+Example +
+ +```mu +listOf( + 2 + 5, + 2 - 5, + 0.5 * 4, + 2 + 2 * 3, + (2 + 2) * 3, + 4 / 2, + 5 % 3, // this is modulo + 2 ^ 10, // two to the power of ten + 12 > 4, + 5 < 3, + 4 >= 4, + 5 != 2, + 8 == 8, + round(PI, floor(E)) // round also accepts a second argument: decimalPlaces, but it can be ommitted to round to a whole number +) +``` +Result: +``` +[7, -3, 2, 8, 12, 2, 2, 1024, true, false, true, true, true, 3.14] +``` +
+ +## Booleans + +Your standard logic operators are supported. +XOR/XNOR are just your normal `!=`/`==` operators. +Make sure not to use `||`, as that is the string concatenation operator, not OR. +You can also use ternary conditional operators as you would in java. + +
+Example +
+ +```mu +listOf( + 1 < 3 ? "this is correct" : "it is not", // if the condition is true, do the first thing. Else, do the second + !true, // not + true & false, // and + true & true, + true | false, // or + false | false +) +``` +Result: +``` +['this is correct', false, false, true, true, false] +``` +
+ +## Strings + +Strings only support concatenation via `||`. +You can also concatenate other things to a string. +Values of all other types can also be coerced into strings. + +Equality operations are supported. + +The StdLib comes with some additional functions, namely: `len`, `toUpper`, `toLower`, `contains` and `replace` + +
+Example +
+ +```mu +15 || "string one"::toUpper() || 'string Two'::toLower() || "example"::contains("thing") || "yxamply"::replace("y", "e") +``` +Result: +``` +15STRING ONEstring twofalseexample +``` +
+ +## Objects + +You can read (but not write) the fields of objects via `.` or `[]`. + +The StdLib comes with some additional functions, namely: `isEmpty`, `len`, `keys`, `values` and `contains` +It also contains two objects, namely `date` and `time` which allow reading the current date and time. +They also allow creating date/time objects and comparing them. + +
+Example +
+ +```mu +object2 = { + valuename = 'subvalue', + sub = { + val = 10 + }, + stringfunc = {text -> text} +} + +object = { + subvalue = 1024, + `1` = "One" // you can escape any identifier (not just object keys) with '`' +} + +listOf( + object2.valuename, // This is how you would normally do this + object2['valuename'], // You can also use [] + this['object2']['valuename'], // 'this' is always an object representing the current scope + object2['value' || 'name'], // [] can contain any expression + object[object2['sub'].val / 10], // [] even references to other field + object2.sub['val'] * 2, // You can do anything with the value of a field + object.subvalue / (object2.sub.val + 6), + object2.stringfunc("some parameter"), // Objects can also contain functions + date(2023, 5, 13), + time(23, 55, 10), + date(2020, 5, 10) > date.today +) +``` +Result: +``` +['subvalue', 'subvalue', 'subvalue', 'subvalue', 'One', 20, 64, 'some parameter', 2023-05-13, 23:55:10, false] +``` +
+ +## Lists + +Lists can be created with the stdlib function `listOf` (as seen in other examples). +You can access their entries with `[]`, just like you would access fields for objects. +In function calls, you can use the spread operator (`...`) to use all elements of the list as parameters. +The StdLib also comes with some additional functions, namely `len`, `contains`, `isEmpty`, `concat`, `filter`, `allMatch`, `anyMatch`, `map`, `flatMap`, `fold`, `forEach` and `toObject`. + +
+Example +
+ +```mu +listOf( + len(listOf(1, 2, 3)), + isEmpty(listOf()), + concat(listOf(1, 2), listOf(3, 4)), + listOf(1, 2)::concat(listOf(3, 4))::len(), // Don't forget using the bind operator for readability in longer chains + time(listOf(23, 55)..., 10), // You can use the spread operator in any call, not just variadic ones + listOf(1, 2, 3)[1], // Lists are 0-indexed + listOf(1, 2, 3, 4)::filter({n->n%2==0})::map({n->n/2}) // you can chain the additional functions for proper functional programming +) +``` +Result: +``` +[3, true, [1, 2, 3, 4], 4, 23:55:10, 2, [1, 2]] +``` +
+ +## Functions + +μScript is functional as in functions being treated like any other data type. +This means you can pass them to methods as arguments, store them in variables or do whatever else you feel like. +A closure consists of a list of arguments (of which the last may be variadic) and some instructions. + +
+Example +
+ +```mu +someFunction = {n -> n * 2} // By assigning a closure to a variable, you get an equivalent to a normal java function + +someVal = 1 +$someVal = 1 + +{->someVal = 12}() // If you try to change a variable from inside a closure, you create a copy of it. The outer scope doesn't change +{->$someVal = 12}() // The only exception to that are variables prefixed with a dollar sign + // These work like mutable variables in other languages +/* + Use `...` to mark the last argument of a closure as variadic + All arguments that don't fit in the previous parameters will be packed in it as a list + Note that the length of that list may be 0 +*/ +someFunction2 = { a, b... -> + isEmpty(b) ? a : a * someFunction2(b...) // you can use the spread operator on that list like any other +} + +listOf( + someFunction(1), // Closures are ran with parentheses + {->"some expression(s)"}(), // You don't have to store them to call them once... + {f1, f2 -> f1(f2(5))}(someFunction, {n->n}), // ... or to pass them to a higher order function + {-> + 12 + 3 + 10 + 2 + 5 + 2 // The last expression in a closure is its result. This is the same behavior as scripts (like the one this runs in) + }(), + {-> + 12 + 3; // You can (and probably should) use semicolons to seperate expressions + 10 + 2; + 5 + 2; + }(), + 1::someFunction(), // You can use the bind operator '::' to create a new closure using the value to the left as the first argument + // In this case, this new closure is executed immediately using the parentheses, but doing that isn't necessary + 1::({n->n})(), // Instead of a name, you can also use any expression as the right part of a bind operator (if it is in parentheses) + someVal, + $someVal, + someFunction2(1, 2, 3, 4) +) +``` +Result: +``` +[2, 'some expression(s)', 10, 7, 7, 2, 1, 1, 12, 24] +``` +
+ +## 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`. + +
+Example +
+ +```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'] +``` +
\ No newline at end of file diff --git a/muscript/README.md b/muscript/README.md index 99e5408..3825840 100644 --- a/muscript/README.md +++ b/muscript/README.md @@ -1,67 +1,3 @@ # μScript -μScript was created to allow respackopts pack authors to specify conditions for loading resources -in a more human-friendly manner than the previous json tree-based system. - -It is intended to be vaguely like java in its syntax, though it deviates in some aspects. - -The language is parsed into an AST representation which is executed in-place, no compilation is performed. -μScript supports outputting data using various types, not just strings or booleans. - -The purpose of this document is to be a guide on embedding μScript in your application or library. - -For details on how to use the language, look [here](src/test/resources/example.md). - -For details on the standard library provided by μScript, look [here](StandardLib.md). - -## Embedding μScript - -μScript is available as a [maven package](https://maven.frohnmeyer-wds.de/#/artifacts/io/gitlab/jfronny/muscript) which you can add to your -project. -To use it, first parse an expression via `Parser.parse(String script)` and convert the returned generic expression to a typed -one by calling `as(Bool|String|Number|Dynamic)Expr`. -This process may throw a ParseException. -You may also use `Parser.parseScript(String script)` for multi-expression scripts. -You can call `get(Dynamic dataRoot)` on the result to execute the script on the provided data, which should be a -`Scope` which you created with `StandardLib.createScope()` to add standard methods. -This is also where you can add custom data to be accessed by your script. -The execution of a script can throw a LocationalException which may be converted to a LocationalError for printing -using the source of the expression if available. -You may also call `StarScriptIngester.starScriptToMu()` to generate μScript code from StarScript code. - -A full example could look as follows: - -```java -public class Example { - public static void main(String[] args) { - String source = String.join(" ", args); - Expr parsed; - try { - parsed = Parser.parse(source); // or Parser.parse(StarScriptIngester.starScriptToMu(source)) - } catch (Parser.ParseException e) { // Could not parse - System.err.println(e.error); - return; - } - BoolExpr typed; - try { - typed = parsed.asBoolExpr(); - } catch (LocationalException e) { - System.err.println(e.asPrintable(source)); - return; - } - Scope scope = StandardLib.createScope() - .set("someValue", 15) - .set("someOther", Map.of( - "subValue", DFinal.of(true) - )); - boolean result; - try { - result = typed.get(scope); - } catch (LocationalException e) { - System.err.println(e.asPrintable(source)); - return; - } - System.out.println("Result: " + result); - } -} -``` +This document was moved [here](../muscript-runtime/README.md). \ No newline at end of file diff --git a/muscript/StandardLib.md b/muscript/StandardLib.md index 15777b0..4d7371b 100644 --- a/muscript/StandardLib.md +++ b/muscript/StandardLib.md @@ -1,77 +1,3 @@ # μScript Standard Library -The purpose of this document is to be a guide to the standard library provided by μScript. -It is not intended to explain the syntax of the language or provide information on its history, implementation, or how to embed it in programs. - -For information on that, look at the [language guide](src/test/resources/example.md) and the [developer info](README.md). - -## Types -| name | description | -|----------|-----------------------------------------------------------------------------------------------------| -| number | Equivalent to a java double, the only number type in muScript | -| bool | A boolean. Either true or false | -| null | Equivalent to null in java or Unit in kotlin | -| string | A sequence of characters | -| list | An ordered sequence of items of any type | -| object | A mapping between keys (string) and values (any) | -| callable | A value that can be invoked via `()` | -| date | Represents a calendar date. Can be represented as string, number (days since 01-01-1970) and object | -| time | Represents a time of day. Can be represented as a string, number (second of day) and object | -| enum | Details below | -| try | Details below | - -### Enum -A map between names and values of any type. Can also be represented as a list of names. -One entry may be selected, its index will be accessible as the number representation and its name as the string representation of the enum. - -### Try -An `object` returned by the `try` function. Contains the following entries: -- `result`: The result of executing the closure, the result of `catch` if it failed and `catch` was called, or `null` -- `catch(handler: {string -> any})`: If the execution of the closure failed, - `handler` will be executed with the error message and its result will be provided as `result`. - No more than one `catch` may exist per `try`. - -## Functions -The standard library also provides a number of default utility functions. -Below, these are listed alongside their arguments. -The syntax used for signatures in this list is not used elsewhere and not part of the language. - -### Signature syntax -- If a parameter is suffixed with `?`, it can be omitted. If omission is equivalent to a default value, it will be listed with the default behind an equals sign -- If a parameter is suffixed with `...`, it will be variadic (support 0 or more arguments) -- If multiple types are possible, they are listed as `T1 / T2`, where T1 is preferred over T2 -- If an object supports multiple representations, it will be denoted as `T1 & T2` -- Callables are represented like closures but with types instead of names - -### Supported functions -| signature | description | -|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `round(value: number, decimalPlaces?: number = 0): number` | Rounds `value` while retaining `decimalPlaces` places after the comma | -| `floor(value: number): number` | Returns the largest integer lower than or equal to `value` | -| `ceil(value: number): number` | Returns the smallest integer larger than or equal to `value` | -| `abs(value: number): number` | Returns the absolute value of `value` (always positive or zero) | -| `random(min?: number = 0, max?: number = 1): number` | Returns a random number between `min` and `max`. If `min` is provided, `max` cannot be omitted. | -| `toUpper(value: string): string` | Converts all characters in `value` to upper case | -| `toLower(value: string): string` | Converts all characters in `value` to lower case | -| `contains(in: list / object / string, find: any): bool` | Checks whether `find` is an element of the list, a key of the object or a substring of the string provided as `in` | -| `replace(source: string, target: string, replacement: string): string` | Replaces all instances of `target` in `source` with `replacement` and returns the result | -| `listOf(elements: any...): list` | Creates a list with the provided elements | -| `len(toCheck: string / object / list): number` | Gets the amount of characters in the string or entries in the object or list provided as `toCheck` | -| `isEmpty(toCheck: object / list): bool` | Checks whether the object or list provided as `toCheck` contain any entries | -| `concat(lists: list...): list` | Returns a list containing all elements of all provided lists in their original order | -| `filter(in: list, with: {any -> bool}): list` | Returns a list of all elements `el` of `in` for which `with(el)` returned true | -| `allMatch(in: list, with: {any -> bool}): bool` | Whether `with(el)` is true for all elements `el` of `in` | -| `anyMatch(in: list, with: {any -> bool}): bool` | Whether `with(el)` is true for any element `el` of `in` | -| `map(in: list, with: {any -> any}): list` | Returns a list of the results of applying `with` to all elements of `in` | -| `flatMap(in: list, with: {any -> list}): list` | Returns the concatenated list of the results of applying `with` to all elements of `in` | -| `fold(in: list, identity: any, operator: {any, any -> any}): any` | Runs `operator(previous, current)` for every element in `in` where `previous` is the previous result of `operator`, starting with `identity` and `current` is the current element | -| `forEach(in: list, with: {any -> any}): any` | Runs `with` for each element in `in`, returning the last result or `null` for empty lists | -| `toObject(from: list, keyMapper: {any -> string}, valueMapper: {any -> any}): object` | Creates an object mapping the results of `keyMapper` to the results of `valueMapper` for every element in `from` | -| `callableObject(source: object, action: callable): object & callable` | Returns an object equivalent to `source` that can be called, resulting in the invocation of `action` | -| `enum(values: object, selected?: string): enum` | Creates an enum with the values provided as `values`, optionally marking the one with the key `selected` as such | -| `keys(ob: object): list` | Returns the list of names of entries of the object `ob` | -| `values(ob: object): list` | Returns the list of values of entries of the object `ob` | -| `try(closure: {any... -> any}, args: any...): try` | Attempts to execute the closure with the provided args and returns a `try`. For details on what to do with that, look above. | - -Other project-specific functions (like [μScript-gson](../muscript-gson/README.md)) will likely also be available to your scripts. -Please consult the documentation for the project using μScript for more information on what is available or use `this::keys()` to view everything. \ No newline at end of file +This document was moved [here](../muscript-runtime/StandardLib.md). \ No newline at end of file diff --git a/muscript/src/test/resources/example.md b/muscript/src/test/resources/example.md index 0fef8f2..fb4aabc 100644 --- a/muscript/src/test/resources/example.md +++ b/muscript/src/test/resources/example.md @@ -1,265 +1,3 @@ # μScript -The purpose of this document is to be a guide to using the μScript language. -It is not intended to explain any history of the language, implementation details or details on how to embed μScript in programs. - -For information on that, look [here](../../../README.md). - -For information on the standard library provided by μScript, look [here](../../../StandardLib.md) - -## Types -μScript supports a couple of basic types, namely: -- [Numbers](#numbers) (double-precision floating-point) -- [Booleans](#booleans) -- [Strings](#strings) -- [Objects](#objects) -- [Lists](#lists) -- [Functions](#functions) - -Numbers, booleans and strings can be created just like in java. - -## Numbers - -You can use the most common math operations on numbers. -That includes addition, subtraction, multiplication, division, modulo, pow, and your standard comparison operators. - -Order of operation is also supported. - -The StdLib comes with additional constants and functions, namely: `PI`, `E`, `round`, `floor`, `ceil`, `abs`, `random` - -
-Example -
- -```mu -listOf( - 2 + 5, - 2 - 5, - 0.5 * 4, - 2 + 2 * 3, - (2 + 2) * 3, - 4 / 2, - 5 % 3, // this is modulo - 2 ^ 10, // two to the power of ten - 12 > 4, - 5 < 3, - 4 >= 4, - 5 != 2, - 8 == 8, - round(PI, floor(E)) // round also accepts a second argument: decimalPlaces, but it can be ommitted to round to a whole number -) -``` -Result: -``` -[7, -3, 2, 8, 12, 2, 2, 1024, true, false, true, true, true, 3.14] -``` -
- -## Booleans - -Your standard logic operators are supported. -XOR/XNOR are just your normal `!=`/`==` operators. -Make sure not to use `||`, as that is the string concatenation operator, not OR. -You can also use ternary conditional operators as you would in java. - -
-Example -
- -```mu -listOf( - 1 < 3 ? "this is correct" : "it is not", // if the condition is true, do the first thing. Else, do the second - !true, // not - true & false, // and - true & true, - true | false, // or - false | false -) -``` -Result: -``` -['this is correct', false, false, true, true, false] -``` -
- -## Strings - -Strings only support concatenation via `||`. -You can also concatenate other things to a string. -Values of all other types can also be coerced into strings. - -Equality operations are supported. - -The StdLib comes with some additional functions, namely: `len`, `toUpper`, `toLower`, `contains` and `replace` - -
-Example -
- -```mu -15 || "string one"::toUpper() || 'string Two'::toLower() || "example"::contains("thing") || "yxamply"::replace("y", "e") -``` -Result: -``` -15STRING ONEstring twofalseexample -``` -
- -## Objects - -You can read (but not write) the fields of objects via `.` or `[]`. - -The StdLib comes with some additional functions, namely: `isEmpty`, `len`, `keys`, `values` and `contains` -It also contains two objects, namely `date` and `time` which allow reading the current date and time. -They also allow creating date/time objects and comparing them. - -
-Example -
- -```mu -object2 = { - valuename = 'subvalue', - sub = { - val = 10 - }, - stringfunc = {text -> text} -} - -object = { - subvalue = 1024, - `1` = "One" // you can escape any identifier (not just object keys) with '`' -} - -listOf( - object2.valuename, // This is how you would normally do this - object2['valuename'], // You can also use [] - this['object2']['valuename'], // 'this' is always an object representing the current scope - object2['value' || 'name'], // [] can contain any expression - object[object2['sub'].val / 10], // [] even references to other field - object2.sub['val'] * 2, // You can do anything with the value of a field - object.subvalue / (object2.sub.val + 6), - object2.stringfunc("some parameter"), // Objects can also contain functions - date(2023, 5, 13), - time(23, 55, 10), - date(2020, 5, 10) > date.today -) -``` -Result: -``` -['subvalue', 'subvalue', 'subvalue', 'subvalue', 'One', 20, 64, 'some parameter', 2023-05-13, 23:55:10, false] -``` -
- -## Lists - -Lists can be created with the stdlib function `listOf` (as seen in other examples). -You can access their entries with `[]`, just like you would access fields for objects. -In function calls, you can use the spread operator (`...`) to use all elements of the list as parameters. -The StdLib also comes with some additional functions, namely `len`, `contains`, `isEmpty`, `concat`, `filter`, `allMatch`, `anyMatch`, `map`, `flatMap`, `fold`, `forEach` and `toObject`. - -
-Example -
- -```mu -listOf( - len(listOf(1, 2, 3)), - isEmpty(listOf()), - concat(listOf(1, 2), listOf(3, 4)), - listOf(1, 2)::concat(listOf(3, 4))::len(), // Don't forget using the bind operator for readability in longer chains - time(listOf(23, 55)..., 10), // You can use the spread operator in any call, not just variadic ones - listOf(1, 2, 3)[1], // Lists are 0-indexed - listOf(1, 2, 3, 4)::filter({n->n%2==0})::map({n->n/2}) // you can chain the additional functions for proper functional programming -) -``` -Result: -``` -[3, true, [1, 2, 3, 4], 4, 23:55:10, 2, [1, 2]] -``` -
- -## Functions - -μScript is functional as in functions being treated like any other data type. -This means you can pass them to methods as arguments, store them in variables or do whatever else you feel like. -A closure consists of a list of arguments (of which the last may be variadic) and some instructions. - -
-Example -
- -```mu -someFunction = {n -> n * 2} // By assigning a closure to a variable, you get an equivalent to a normal java function - -someVal = 1 -$someVal = 1 - -{->someVal = 12}() // If you try to change a variable from inside a closure, you create a copy of it. The outer scope doesn't change -{->$someVal = 12}() // The only exception to that are variables prefixed with a dollar sign - // These work like mutable variables in other languages -/* - Use `...` to mark the last argument of a closure as variadic - All arguments that don't fit in the previous parameters will be packed in it as a list - Note that the length of that list may be 0 -*/ -someFunction2 = { a, b... -> - isEmpty(b) ? a : a * someFunction2(b...) // you can use the spread operator on that list like any other -} - -listOf( - someFunction(1), // Closures are ran with parentheses - {->"some expression(s)"}(), // You don't have to store them to call them once... - {f1, f2 -> f1(f2(5))}(someFunction, {n->n}), // ... or to pass them to a higher order function - {-> - 12 + 3 - 10 + 2 - 5 + 2 // The last expression in a closure is its result. This is the same behavior as scripts (like the one this runs in) - }(), - {-> - 12 + 3; // You can (and probably should) use semicolons to seperate expressions - 10 + 2; - 5 + 2; - }(), - 1::someFunction(), // You can use the bind operator '::' to create a new closure using the value to the left as the first argument - // In this case, this new closure is executed immediately using the parentheses, but doing that isn't necessary - 1::({n->n})(), // Instead of a name, you can also use any expression as the right part of a bind operator (if it is in parentheses) - someVal, - $someVal, - someFunction2(1, 2, 3, 4) -) -``` -Result: -``` -[2, 'some expression(s)', 10, 7, 7, 2, 1, 1, 12, 24] -``` -
- -## 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`. - -
-Example -
- -```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'] -``` -
\ No newline at end of file +This document was moved [here](../../../../muscript-runtime/src/test/resources/example.md). \ No newline at end of file