chore(muscript): copy tests and documentation over to new implementation

This commit is contained in:
Johannes Frohnmeyer 2024-04-07 17:18:48 +02:00
parent ab6c3b5b2d
commit beaba5cb02
Signed by: Johannes
GPG Key ID: E76429612C2929F4
30 changed files with 1230 additions and 403 deletions

View File

@ -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);
}
}
```

View File

@ -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.

View File

@ -11,6 +11,8 @@ dependencies {
api(projects.muscriptDataAdditional)
testImplementation(libs.junit.jupiter.api)
testImplementation(projects.muscriptParser)
testImplementation(projects.muscriptSerialize)
testRuntimeOnly(libs.junit.jupiter.engine)
}

View File

@ -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}()"));
}
}

View File

@ -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()));
}
}

View File

@ -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"));
}
}

View File

@ -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')"));
}
}

View File

@ -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());
}
}

View File

@ -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"));
}
}

View File

@ -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()));
}
}

View File

@ -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)));
}
}

View File

@ -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());
}
}

View File

@ -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'");
}
}

View File

@ -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)"));
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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"));
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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}"));
}
}

View File

@ -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'"));
}
}

View File

@ -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());
}
}

View File

@ -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<Executable> 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);
}
}
}

View File

@ -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)...)"));
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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`
<details>
<summary>Example</summary>
<br>
```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]
```
</details>
## 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.
<details>
<summary>Example</summary>
<br>
```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]
```
</details>
## 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`
<details>
<summary>Example</summary>
<br>
```mu
15 || "string one"::toUpper() || 'string Two'::toLower() || "example"::contains("thing") || "yxamply"::replace("y", "e")
```
Result:
```
15STRING ONEstring twofalseexample
```
</details>
## 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.
<details>
<summary>Example</summary>
<br>
```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]
```
</details>
## 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`.
<details>
<summary>Example</summary>
<br>
```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]]
```
</details>
## 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.
<details>
<summary>Example</summary>
<br>
```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]
```
</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>

View File

@ -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).

View File

@ -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.
This document was moved [here](../muscript-runtime/StandardLib.md).

View File

@ -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`
<details>
<summary>Example</summary>
<br>
```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]
```
</details>
## 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.
<details>
<summary>Example</summary>
<br>
```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]
```
</details>
## 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`
<details>
<summary>Example</summary>
<br>
```mu
15 || "string one"::toUpper() || 'string Two'::toLower() || "example"::contains("thing") || "yxamply"::replace("y", "e")
```
Result:
```
15STRING ONEstring twofalseexample
```
</details>
## 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.
<details>
<summary>Example</summary>
<br>
```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]
```
</details>
## 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`.
<details>
<summary>Example</summary>
<br>
```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]]
```
</details>
## 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.
<details>
<summary>Example</summary>
<br>
```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]
```
</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>
This document was moved [here](../../../../muscript-runtime/src/test/resources/example.md).