From 516fcea3a154f96f08d91bf5609cd0a3504c7d4b Mon Sep 17 00:00:00 2001 From: JFronny Date: Sat, 11 Mar 2023 17:03:17 +0100 Subject: [PATCH] muScript: better documentation --- muscript/README.md | 79 +------ .../muscript/test/ValidExampleTest.java | 58 +++++ muscript/src/test/resources/example.md | 218 ++++++++++++++++++ 3 files changed, 281 insertions(+), 74 deletions(-) create mode 100644 muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java create mode 100644 muscript/src/test/resources/example.md diff --git a/muscript/README.md b/muscript/README.md index faa585c..b50a7dd 100644 --- a/muscript/README.md +++ b/muscript/README.md @@ -2,83 +2,14 @@ μ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 +μScript supports outputting data using various types, not just strings or booleans. -## Value types - -This DSL supports numbers (double), booleans, strings, objects, lists and functions. -The topmost operator of a condition must always return a boolean for it to be valid in the context of respackopts. -The values of input data are according to the pack config. -String literals may be written with quotation marks as follows: `"some text"` or `'some text'` -Numbers may be written as follows: `103` or `10.15` -Booleans may be written either as `true` or `false` -Lists may be created with the `listOf` function (part of StandardLib): `listOf(1, 2, 3, 4)` -Functions may be created with the syntax explained below (--> Closures). -You may assign any of these to a variable, including closures. -Objects cannot be created manually but may be provided to the script as parameters or from functions defined in Java. - -Please ensure you use proper whitespace, as this might behave unexpectedly otherwise. - -## Operators - -Numbers support the following operators (x and y are numbers): - -- Addition: `x + y` -- Subtraction: `x - y` -- Multiplication: `x * y` -- Division: `x / y` -- Modulo: `x % y` -- Power: `x ^ y` -- Inversion: `-x` -- Greater than: `x > y` -- Greater than or equal: `x >= y` -- Less than: `x < y` -- Less than or equal: `x <= y` -- Equality: `x == y` -- Inequality: `x != y` - -Strings support the following operators (x and y are strings, a is any value): - -- Equality: `x == y` -- Inequality: `x != y` -- Concatenation: `x || a` or `a || x` - -Booleans support the following operators (x and y are booleans, a and b are values of the same type): - -- Conditional: `x ? a : b` -- Inversion: `!x` -- And: `x & y` -- Or: `x | y` -- Equality (=XNOR): `x == y` -- Inequality (=XOR): `x != y` - -Objects support the following operators (x is an object with an entry called `entry`): - -- Value access via `.`: `x.entry` -- Value access via square brackets: `x["entry"]` - -Lists support the following operators (x is a list, someFunc is a method taking variadic arguments): - -- Value access via square brackets: `x[0]` -- Expanding via `...` (only in calls): `someFunc(x...)` - -Parentheses (`()`) may be used to indicate order of operation. - -## Closures (functions) -One may define a closure as follows: `{list, of, arguments -> body}`. -The body of a closure may contain multiple expressions, which can be (but don't have to be) seperated by semicolons. -In that case, the return value of the last expression will be the return value of the closure. -The arrow (`->`) must always be present, even if the closure takes no arguments. -The amount of arguments is checked at runtime, so the amount used to call the function MUST be correct. -If you don't know the amount of arguments upfront, you may declare a variadic closure with `...`: `{some, arguments... -> body}` -where `arguments` would be the variadic argument, which is always a list of excess parameters. -In closures (or multi-expression scripts for that matter), you may assign variables as follows: `name = value`. -Please note that you cannot assign values to fields of objects. -You may also assign a closure to a variable to use it like a named function. -This could look as follows: `someFunction = {arg -> arg * arg}` -Please also be aware that μScript does NOT allow you to modify variables of outer scopes from inner scopes. +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). ## Embedding μScript diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java new file mode 100644 index 0000000..f06f6e5 --- /dev/null +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java @@ -0,0 +1,58 @@ +package io.gitlab.jfronny.muscript.test; + +import io.gitlab.jfronny.commons.StringFormatter; +import io.gitlab.jfronny.muscript.compiler.Parser; +import io.gitlab.jfronny.muscript.error.LocationalException; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs; +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); + 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("
", split[i++]); + assertEquals("Result", split[i++]); + assertEquals("
", split[i++]); + assertEquals("", split[i++]); + assertEquals("```", split[i]); + StringBuilder resultBuilder = new StringBuilder(); + while (!split[++i].equals("```")) { + resultBuilder.append('\n').append(split[i]); + } + i++; + assertEquals("
", split[i++]); + final String block = blockBuilder.substring(1); + final String expectedResult = resultBuilder.substring(1); + String result = null; + try { + result = Parser.parseScript(block).asExpr().asStringExpr().get(makeArgs()); + } catch (Throwable t) { + assertEquals(expectedResult, StringFormatter.toString(t, e -> + e instanceof LocationalException le + ? le.asPrintable(block).toString() + : e.toString() + )); + } + if (result != null) assertEquals(expectedResult, result); + } + } + } +} diff --git a/muscript/src/test/resources/example.md b/muscript/src/test/resources/example.md new file mode 100644 index 0000000..94a2ee9 --- /dev/null +++ b/muscript/src/test/resources/example.md @@ -0,0 +1,218 @@ +# μ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). + +## 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` + +For 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, 2) + floor(E) // round also accepts a second argument: precision, 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, 5.1400] +``` +
+ +## 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. + +```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: `toUpper`, `toLower`, `contains` and `replace` + +```mu +15 || "string one"::toUpper() || 'string Two'::toLower() || "example"::contains("thing") || "yxamply"::replace("y", "e") +``` +
+Result +
+ +``` +15STRING ONEstring twofalseexample +``` +
+ +## Objects + +You cannot create your own objects, but objects may be passed to your script from functions or as parameters. +You can read (but not write) their fields via `.` or `[]`. + +In the following example, the objects `object` and `object2` is passed to the script. + +The StdLib 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. + +```mu +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`, `isEmpty`, `concat`, `filter`, `map`, `flatMap`, `fold` and `forEach`. + +```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. + +```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] +``` +