diff --git a/docusaurus.config.ts b/docusaurus.config.ts index dd0e8fd..ad5b83e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -63,6 +63,7 @@ const config: Config = { plugins: [ './plugins/maven-versions-plugin', + './plugins/mochafloats-version-plugin', [ '@docusaurus/plugin-content-docs', { @@ -81,6 +82,15 @@ const config: Config = { sidebarPath: './sidebars.ts', }, ], + [ + '@docusaurus/plugin-content-docs', + { + id: 'mochafloats', + path: "mochafloats", + routeBasePath: "mochafloats", + sidebarPath: './sidebars.ts', + }, + ], ], themeConfig: { @@ -115,6 +125,13 @@ const config: Config = { docsPluginId: "emotecraft", label: "Emotecraft Documentation", }, + { + type: "docSidebar", + sidebarId: "mainSidebar", + position: "left", + docsPluginId: "mochafloats", + label: "MochaFloats Documentation", + }, { href: "https://github.com/PlayerAnimationLibrary/Documentation", label: "GitHub", @@ -135,6 +152,10 @@ const config: Config = { { to: "/emotecraft/gettingstarted", label: "Emotecraft Documentation", + }, + { + to: "/mochafloats/intro", + label: "MochaFloats Documentation", } ], }, diff --git a/mochafloats/features/_category_.json b/mochafloats/features/_category_.json new file mode 100644 index 0000000..59287f7 --- /dev/null +++ b/mochafloats/features/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Features", + "position": 4 +} diff --git a/mochafloats/features/compiling.md b/mochafloats/features/compiling.md new file mode 100644 index 0000000..20559e6 --- /dev/null +++ b/mochafloats/features/compiling.md @@ -0,0 +1,116 @@ +--- +sidebar_position: 1 +description: "Compiling MoLang expressions to JVM bytecode with MolangCompiler: typed functions, Java calls and what the compiler supports." +--- + +# Compiling to bytecode + +The `runtime-compiler` module can turn an expression into a small JVM class. +Calling it is as fast as calling a hand-written Java method: there's no syntax tree to walk, and the JIT can inline it like any other code. + +Compile expressions that run very often and only use math and your own Java methods. +For everything else — and especially for MoLang written by users or artists — use the [interpreter](../gettingstarted/usage.md): it supports the whole language, while the compiler only supports [a subset](#what-the-compiler-supports). + +## Compiling an expression + +```java +MochaEngine engine = MochaEngine.createStandard(); + +MochaFunction function = engine.compiler().compile("math.sqrt(3 * 3 + 4 * 4)"); +float result = function.evaluate(); // 5.0 +``` + +Constant parts are evaluated while compiling, so the class generated for this example simply returns `5.0` — its whole method body is `ldc 5.0; freturn`. + +Compile once and keep the function: every `compile` call parses the text and defines a new class. +The class is a [hidden class](https://openjdk.org/jeps/371), so the JVM can unload it once you no longer reference the function. + +`MolangCompiler` can also be created directly — `new MolangCompiler(entity, scope)` — to compile against any scope, for example an interpreter's `scope()`. + +## Functions with parameters + +Declare an interface with a single method that extends `MochaCompiledFunction`, and the compiled class will implement it. +Parameters are available in the expression by name: + +```java +public interface Falloff extends MochaCompiledFunction { + float apply(@Named("distance") float distance, @Named("radius") float radius); +} + +Falloff falloff = engine.compiler().compile("math.clamp(1 - distance / radius, 0, 1)", Falloff.class); +falloff.apply(2, 8); // 0.75 +``` + +- The interface must have exactly one abstract method. Default and static methods are fine. +- Name every parameter with `@Named`, or compile your code with `javac -parameters` so the names are kept. +- Parameters and the return value can be `float`, `double`, `int`, `long` or `boolean`; values are converted as needed. A `boolean` result is `true` when the expression is non-zero. + +## Calling Java code + +Bindings made with [`@Binding`](../gettingstarted/bindings.md#java-classes-with-binding) are called directly from the generated code — no reflection, no boxing: + +```java +@Binding("game") +public final class GameBindings { + @Binding("gravity") + public static final float GRAVITY = 0.08F; + + @Binding("ticks_to_seconds") + public static float ticksToSeconds(float ticks) { + return ticks / 20F; + } +} + +engine.interpreter().bind(GameBindings.class); +engine.compiler().compile("game.ticks_to_seconds(40) * game.gravity").evaluate(); // 0.16 +``` + +- Static methods become `invokestatic`, methods of an instance bound with `bindInstance` become `invokevirtual` on that instance. +- `public static final` number fields are copied into the class as constants; other static fields are read every time. +- String literals can be passed to methods that take a `String`. +- A parameter annotated with `@Entity` receives the compiler's entity. + +## What the compiler supports + +| Supported | Not supported — use the interpreter | +|-------------------------------------------------------------|-------------------------------------------------------------| +| Numbers, `true`, `false` | `variable` / `v` | +| `+` `-` `*` `/`, unary `-` | Bindings made of lambdas or `ObjectValue`s, such as a `query` built from `Function`s | +| `<` `<=` `>` `>=` `==` `!=`, `&&` `\|\|` `!` | `loop`, `for_each`, `break`, `continue`, blocks `{ ... }` | +| `c ? a : b`, `c ? a`, `return`, several statements | `->`, `??` and `[]` | +| `temp` / `t` variables | Non-static `@Binding` fields | +| The interface's parameters | Methods that take an `ExecutionContext`, a `Lazy` or varargs | +| `math.*` and other `@Binding` static methods and fields | | +| Methods of instances bound with `bindInstance` | | + +An unsupported construct makes `compile` throw an `UnsupportedOperationException` that names it. +Parts of an expression that only use constants are computed at compile time, so `1 ?? 2` compiles fine; `t.a ?? 3` doesn't. + +Compiled code follows the interpreter's number rules: dividing by zero gives `0`, and `NaN` or infinities, from arithmetic or returned by a Java method, become `0`. +A method whose `@Binding` sets `skipChecking = true` is trusted to return finite values and isn't checked. + +## Debugging + +- `postCompile(bytes -> ...)` receives the bytecode of every generated class before it's loaded. Write it to a `.class` file and open it in your IDE or with `javap`. +- `handleParseExceptions(handler)` is called when the source doesn't parse. The compiler then compiles an empty body, which returns `0` (or `false`). + +```java +engine.compiler() + .postCompile(bytes -> { + try { + Files.write(Path.of("molang.class"), bytes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +``` + +## Constant folding + +By default the compiler runs every expression through an `ExpressionInliner` first, which evaluates parts made only of literals and `pure` functions. +Pass your own inliner as the third constructor argument, or `null` to compile expressions exactly as written: + +```java +MolangCompiler compiler = new MolangCompiler(null, engine.scope(), null); +``` diff --git a/mochafloats/features/index.md b/mochafloats/features/index.md new file mode 100644 index 0000000..fbf7418 --- /dev/null +++ b/mochafloats/features/index.md @@ -0,0 +1,10 @@ +--- +description: "What mochafloats does beyond interpreting MoLang: compiling it to bytecode and sending it over the network." +--- + +# Features + +Beyond interpreting MoLang, mochafloats can: + +- [Compile expressions to bytecode](./compiling.md) — turn hot expressions into JVM classes that run as fast as hand-written Java. +- [Send expressions over the network](./serialization.md) — write parsed expressions to a Netty `ByteBuf` and read them back without parsing the text again, or print them as MoLang. diff --git a/mochafloats/features/serialization.md b/mochafloats/features/serialization.md new file mode 100644 index 0000000..ff18f9a --- /dev/null +++ b/mochafloats/features/serialization.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +sidebar_label: "Network format" +description: "Writing parsed MoLang expressions to a Netty ByteBuf and reading them back, and printing them as text." +--- + +# Sending expressions over the network + +The `parser` module can write parsed expressions to a Netty `ByteBuf` and read them back. +The receiving side gets a ready syntax tree: it never parses text, so it can't hit a syntax error. + +Minecraft's `FriendlyByteBuf` is a `ByteBuf`, so you can pass the packet buffer directly. + +```java +List expressions = MolangParser.parseAll("math.sin(q.anim_time * 360) * 30"); + +// sender +ExprBytesUtils.writeExpressions(expressions, buf); + +// receiver +List received = ExprBytesUtils.readExpressions(buf); +float value = molang.eval(received); +``` + +`writeExpression` and `readExpression` do the same for a single expression. +Evaluating an expression doesn't change it, so one parsed list can be written to many players and shared between interpreters. + +PlayerAnimationLibrary's binary animation format, which is what it sends over the network, stores every keyframe value this way. + +## The format + +Every expression is one byte with its type, followed by its contents: + +| Id | Expression | Contents | +|----|------------------------------------|--------------------------------------------| +| 0 | Unary (`-x`, `!x`, `return x`) | operator, expression | +| 1 | Conditional (`c ? a : b`) | three expressions | +| 2 | String | string | +| 3 | `break` / `continue` | operator | +| 4 | Name (`math`, `v`) | string | +| 5 | Number | 4-byte float | +| 6 | Block (`{ ... }`) | list of expressions | +| 7 | Call | expression, list of arguments | +| 8 | Binary (`a + b`, `a = b`, ...) | operator, two expressions | +| 9 | Array index (`a[i]`) | two expressions | +| 10 | Property access (`a.b`) | expression, string | + +Strings are UTF-8 with a [VarInt](https://minecraft.wiki/w/Java_Edition_protocol/Data_types#VarInt_and_VarLong) length in front, lists are a VarInt count followed by the elements, and operators are a single byte. + +- The format has no version number, and operators are stored by their position in mochafloats' operator lists. Make sure both sides use the same mochafloats version. +- It saves parsing, not bandwidth: the result is usually a little longer than the source text. + +`VarIntUtils` and `ProtocolUtils` are public too, if you want the same VarInts, strings and lists in your own packets. + +## Printing expressions as text + +`toString()` on a parsed expression prints it in MoLang syntax, and `ExpressionListUtils.toString(list)` joins a whole script with `;`: + +```java +List expressions = MolangParser.parseAll("v.a = 2; return math.sin(q.anim_time * 50) * 45;"); +ExpressionListUtils.toString(expressions); // v.a=2;return (math.sin(q.anim_time*50)*45) +``` + +The printed text keeps the parentheses the expression needs — `(1 + 2) * 3` prints as `(1+2)*3` — so parsing it again gives the same expression. +It isn't the original text, though: spaces are gone, and parentheses that don't change the meaning may be added or left out. Keep the source if you want to show people what they wrote. diff --git a/mochafloats/gettingstarted/_category_.json b/mochafloats/gettingstarted/_category_.json new file mode 100644 index 0000000..c1998c5 --- /dev/null +++ b/mochafloats/gettingstarted/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Getting Started", + "position": 3 +} diff --git a/mochafloats/gettingstarted/bindings.md b/mochafloats/gettingstarted/bindings.md new file mode 100644 index 0000000..6c017cc --- /dev/null +++ b/mochafloats/gettingstarted/bindings.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 3 +description: "Exposing your own values, functions and Java classes to MoLang expressions." +--- + +# Bindings + +A binding is anything an expression can reach by name: `math`, `v`, your `query` object, a Java class. +All of them live in the interpreter's scope, which you get with `scope()`. + +## How names are resolved + +`q.health` looks up `q` in the scope, then asks that object for its `health` property. +`q.distance_to(2)` does the same, then calls the property as a function. + +- Names are **case-insensitive**: `Math.PI`, `math.pi` and `MATH.PI` are the same thing. mochafloats lowercases them with `Locale.ROOT`, so the player's language doesn't matter. +- Names are made of letters, digits and `_`, and can't start with a digit. A dot always means "property of", so `q.mymod.speed` is the `speed` property of the `mymod` property of `q` — use `q.mymod_speed` for a flat name. +- A name that doesn't exist evaluates to `0`, without an error. + +## Values + +Everything an expression can touch is a `Value`: + +| Type | Holds | +|------------------|--------------------------------------------------------------| +| `NumberValue` | A `float`. `NumberValue.of(1.5F)`, `Value.of(true)` (which is `1`). | +| `StringValue` | A string. MoLang strings use single quotes: `'minecraft:pig'`. | +| `ArrayValue` | An array of values, indexed with `[]`. | +| `ObjectValue` | Something with named properties, like `math` or `query`. | +| `Function` | Something you can call with `()`. | + +`Value.of(object)` converts plain Java values: numbers, strings, booleans and arrays. + +## Functions as lambdas + +The quickest way to add a function is a `MutableObjectBinding` holding `Function` lambdas: + +```java +MolangInterpreter molang = MolangInterpreter.standard(player); + +MutableObjectBinding query = new MutableObjectBinding(); +query.set("health", (Function) (ctx, args) -> NumberValue.of(ctx.entity().getHealth())); +query.set("distance_to", (Function) (ctx, args) -> { + float x = args.next().eval().getAsNumber(); + return NumberValue.of(Math.abs(ctx.entity().getX() - x)); +}); + +molang.scope().set("query", query); +molang.scope().set("q", query); + +molang.eval("q.health() / 2"); +molang.eval("q.distance_to(10)"); +``` + +- `ctx.entity()` is the interpreter's entity. +- `args.next()` returns the next argument; `eval()` evaluates it. Arguments are evaluated only when you ask, so a function decides whether and how often to evaluate them. `args.length()` tells you how many were passed. +- Return `Value.nil()` or `null` for "nothing"; both read as `0`. + +For plain number functions, `setFunction` saves the casting: + +```java +query.setFunction("half", x -> x / 2); +query.setFunction("random_angle", () -> (float) (Math.random() * 360)); +``` + +Once everything is registered, `query.block()` makes the object read-only: later `set` calls return `false` and change nothing. + +Lambdas are the fastest bindings for the interpreter. The [compiler](../features/compiling.md) can't call them, though — use [`@Binding` classes](#java-classes-with-binding) for compiled expressions. + +## Properties computed when read + +Bedrock writes most queries without parentheses: `q.health`, `q.is_sneaking`. +A `Function` stored in a `MutableObjectBinding` only runs when called with `()`, so for that style implement `ObjectValue` and compute the value in `getProperty`: + +```java +public final class PlayerQueries implements ObjectValue { + private final Player player; + + public PlayerQueries(Player player) { + this.player = player; + } + + @Override + public @Nullable ObjectProperty getProperty(@NotNull String name) { + return switch (name.toLowerCase(Locale.ROOT)) { + case "health" -> ObjectProperty.property(NumberValue.of(player.getHealth()), false); + case "is_sneaking" -> ObjectProperty.property(Value.of(player.isCrouching()), false); + default -> null; + }; + } +} + +molang.scope().set("q", new PlayerQueries(player)); +molang.eval("q.is_sneaking ? q.health : 0"); +``` + +:::warning +`getProperty` receives the name exactly as it was written in the expression — `Health` for `q.Health`. +Lowercase it with `Locale.ROOT` yourself, like above. The built-in bindings already do. +::: + +PlayerAnimationLibrary does the same in its `QueryBinding`: a `MutableObjectBinding` whose `getProperty` calls the stored `Function` instead of returning it. + +## Java classes with `@Binding` + +Annotate a class and its static members with `@Binding`, then bind the class: + +```java +@Binding("random") +public final class RandomBinding { + @Binding("select") + public static float select(float a, float b) { + return Math.random() < 0.5 ? a : b; + } + + @Binding("max_roll") + public static final float MAX_ROLL = 6F; +} + +molang.bind(RandomBinding.class); +molang.eval("random.select(1, random.max_roll)"); // 1 or 6 +``` + +- The class's `@Binding` names it in the scope. Give several names for aliases: `@Binding({"random", "rng"})`. +- Methods can take and return `float`, `double`, `int`, `long`, `short`, `boolean` and `String`; arguments are converted from MoLang values. A `void` method evaluates to `0`. +- A parameter annotated with `@Entity` receives the entity, and a parameter of type `ExecutionContext` receives the execution context. Neither consumes a MoLang argument. +- A `Lazy` parameter receives its argument unevaluated: `get()` evaluates it, so the method decides whether to. +- A varargs parameter, such as `float... values`, receives all the remaining arguments. +- The compiler only supports `@Entity` of these and rejects methods with the others, so call those from interpreted code. + +To bind the non-static members of an object instead, use `bindInstance` and choose the names yourself: + +```java +public final class PlayerApi { + private final Player player; + + public PlayerApi(Player player) { + this.player = player; + } + + @Binding("health") + public float health() { + return player.getHealth(); + } +} + +molang.bindInstance(PlayerApi.class, new PlayerApi(player), "player", "p"); +molang.eval("p.health()"); +``` + +### Existing methods + +`@BindExternalFunction` exposes a static method of another class without writing a wrapper. +Put it on your binding class, once per function: + +```java +@Binding("geometry") +@BindExternalFunction(at = Math.class, name = "hypot", args = {double.class, double.class}, pure = true) +@BindExternalFunction(at = Math.class, name = "cbrt", args = {double.class}, pure = true) +public final class GeometryBinding { +} + +molang.bind(GeometryBinding.class); +molang.eval("geometry.hypot(3, 4)"); // 5.0 +molang.eval("geometry.cbrt(27)"); // 3.0 +``` + +`as = "..."` publishes it under a different name. + +### Pure functions + +`pure = true` on `@Binding` or `@BindExternalFunction` promises that the function always returns the same result for the same arguments and has no side effects. +The compiler then calls it **while compiling** when all its arguments are constants and puts the result in the class instead. +Don't mark anything that reads game state or randomness as pure. + +## Interpreter speed of Java bindings + +The compiler calls `@Binding` methods directly. The interpreter calls them through a method handle prepared once, which is a little slower than calling a lambda. +Turn on `warnOnReflectiveFunctionUsage(true)` on the interpreter to print each method called this way to `System.err`, once, and find the ones worth a lambda. + +To give the interpreter a lambda while compiled code keeps calling the method, bind the class with a **backing object** that holds a `Function` under the same name. +The backing object must return its properties from `entries()`: + +```java +Map backing = Map.of("select", ObjectProperty.property( + (Function) (ctx, args) -> NumberValue.of(RandomBinding.select( + args.next().eval().getAsNumber(), + args.next().eval().getAsNumber())), + false)); + +ObjectValue backingObject = new ObjectValue() { + @Override + public @Nullable ObjectProperty getProperty(@NotNull String name) { + return backing.get(name); + } + + @Override + public @NotNull Map entries() { + return backing; + } +}; + +engine.scope().set("random", JavaObjectBinding.of(RandomBinding.class, null, backingObject)); +``` + +Now `engine.interpreter()` runs the lambda for `random.select(...)` and `engine.compiler()` calls the static method. +A backing function must have the same `pure` flag as the method, otherwise `JavaObjectBinding.of` throws. + +:::note +`JavaObjectBinding` is marked as an internal API, so this may change between versions. +::: diff --git a/mochafloats/gettingstarted/index.md b/mochafloats/gettingstarted/index.md new file mode 100644 index 0000000..f915243 --- /dev/null +++ b/mochafloats/gettingstarted/index.md @@ -0,0 +1,11 @@ +--- +description: "Adding mochafloats to your project, evaluating MoLang and exposing your own values to it." +--- + +# Getting Started + +This section takes you from an empty project to MoLang that reads your game's state: + +1. [Installation](./installation.md) — add the library and pick a module. +2. [Evaluating MoLang](./usage.md) — create an interpreter, evaluate expressions and parse them once for repeated use. +3. [Bindings](./bindings.md) — expose your own values, functions and Java classes to expressions, such as a `query` object. diff --git a/mochafloats/gettingstarted/installation.md b/mochafloats/gettingstarted/installation.md new file mode 100644 index 0000000..48226ad --- /dev/null +++ b/mochafloats/gettingstarted/installation.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 1 +description: "How to add mochafloats to a Gradle or Maven project, and which module to pick." +--- + +import MochaFloatsVersion from '@site/src/components/MochaFloatsVersion'; + +# Installation + +## Requirements + +mochafloats needs **Java 24 or newer**: every module is compiled for Java 24, and the compiler uses the ClassFile API that became final in that release. +Minecraft 26.1 and newer already runs on Java 25. + +The latest version is . + +## Which module do I need? + +| If you want to… | Depend on | +|--------------------------------------------------------------|--------------------| +| Parse MoLang, inspect the syntax tree or send it over the network | `parser` | +| Evaluate MoLang | `runtime` | +| Evaluate MoLang **and** compile it to bytecode | `runtime-compiler` | + +Every module pulls in the ones below it (`runtime-compiler` → `runtime` → `parser` → `lexer`), so declare only one. + +:::tip +If your mod already depends on [PlayerAnimationLibrary](/pal/intro), you already have mochafloats: PAL exposes `runtime` as an API dependency and ships `lexer`, `parser` and `runtime` inside its jar. +Don't bundle a second copy. If you also need the compiler, add `runtime-compiler` with the same version PAL uses. +::: + +## Gradle + +`gradle.properties`: + +```properties +mochafloats_version = +``` + +`build.gradle` / `build.gradle.kts` (the snippet works in both the Groovy and the Kotlin DSL): + +```gradle +repositories { + mavenCentral() + maven { + name = "RedlanceMinecraft" + url = uri("https://repo.redlance.org/public") + } +} + +dependencies { + implementation("org.redlance.mochafloats:runtime:${property("mochafloats_version")}") +} +``` + +Swap `runtime` for `parser` or `runtime-compiler` according to the table above. + +## Maven + +```xml + + + redlance + https://repo.redlance.org/public + + + + + + org.redlance.mochafloats + runtime + 6.1.0 + + +``` + +## Transitive dependencies + +- `parser` depends on Netty's `netty-buffer`, which it uses for the [network format](../features/serialization.md). Minecraft already ships Netty. +- The POMs also list `org.jetbrains:annotations` and `com.google.j2objc:j2objc-annotations`. They only contain annotations and aren't needed at runtime. + +Nothing else: there's no Javassist or other bytecode library. + +## Bundling it inside a Minecraft mod + +If your mod ships mochafloats inside its own jar (jar-in-jar), embed the mochafloats modules themselves and nothing they depend on: + +- `lexer`, `parser` and `runtime` — plus `runtime-compiler` if you compile expressions. +- **Not** Netty: Minecraft provides it, so disable transitive dependencies for the embedded artifacts. + +With Fabric Loom, for example: + +```groovy +dependencies { + implementation("org.redlance.mochafloats:runtime:${property("mochafloats_version")}") + + include("org.redlance.mochafloats:lexer:${property("mochafloats_version")}") { transitive = false } + include("org.redlance.mochafloats:parser:${property("mochafloats_version")}") { transitive = false } + include("org.redlance.mochafloats:runtime:${property("mochafloats_version")}") { transitive = false } +} +``` + +In the Kotlin DSL the closure is `{ isTransitive = false }`. + +On NeoForge, use your build plugin's jar-in-jar configuration (`jarJar` in ModDevGradle) the same way. +The jars declare `FMLModType: GAMELIBRARY` in their manifest, so NeoForge loads them next to your mod, and compiled functions can implement your mod's interfaces — something that crashes with Mocha ([#27](https://github.com/unnamed/mocha/issues/27)). diff --git a/mochafloats/gettingstarted/usage.md b/mochafloats/gettingstarted/usage.md new file mode 100644 index 0000000..9f34951 --- /dev/null +++ b/mochafloats/gettingstarted/usage.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 2 +description: "Creating an interpreter, evaluating and caching MoLang expressions, scopes and parse errors." +--- + +# Evaluating MoLang + +## Creating an interpreter + +`MolangInterpreter` lives in the `runtime` module. `MolangInterpreter.standard()` gives you one with the usual bindings already in place: + +```java +MolangInterpreter molang = MolangInterpreter.standard(); +``` + +| Name | What it is | +|--------------------|----------------------------------------------------------| +| `math` | The [standard math library](../language.md#math-functions). | +| `variable` / `v` | Variables that are kept between evaluations. | +| `temp` / `t` | Variables that only live for one evaluation (added on every call). | + +`query` is **not** part of the standard set: queries describe your game objects, so you [bind them yourself](./bindings.md). + +An interpreter can carry an **entity**, the object its expressions are about — an animated player, a mob, a particle. +Functions you bind receive it through their execution context: + +```java +MolangInterpreter molang = MolangInterpreter.standard(player); +``` + +To start from an empty scope instead, use `MolangInterpreter.create(entity, builder -> builder.set("name", value))`. + +## Evaluating text + +`eval` parses a string and runs it. The result is always a `float`: + +```java +float a = molang.eval("math.sqrt(3 * 3 + 4 * 4)"); // 5.0 +float b = molang.eval("math.abs(-5) + 5"); // 10.0 +float c = molang.eval("v.x = 2; return v.x * 3;"); // 6.0 +``` + +A script with several statements separated by `;` returns the value of its `return`. + +## Parsing once, evaluating many times + +`eval(String)` parses the text again on every call. +For anything that runs repeatedly — a keyframe evaluated every frame, for example — parse it once with `MolangParser` and keep the result: + +```java +List wave = MolangParser.parseAll("math.sin(v.t * 360) * 30"); // throws ParseException + +molang.eval("v.t = 0.25"); +float value = molang.eval(wave); // 30.0 +``` + +`prepareEval` does the same in one step and hands you a `Supplier`: + +```java +Supplier wave = molang.prepareEval("math.sin(v.t * 360) * 30"); +float value = wave.get(); +``` + +:::tip +`Supplier` boxes the result into a new `Float` on every call. In hot code, keep the `List` and call `eval(list)`, which returns a primitive `float`. +::: + +An expression that is just a number — `5`, `-1.5` — is returned straight away, without allocating anything. + +## Where values live + +Every call to `eval` works on its own copy of the interpreter's scope: + +- `temp` / `t` is created fresh for each call, so temporary variables never leak from one evaluation to the next. +- `variable` / `v` is shared: the copy points at the same object, so a value written by one evaluation is visible to the next one. +- While an expression runs, its scope is read-only: a script can write `v.x = 1`, but it can't replace `math` or any other top-level binding. + +```java +molang.eval("v.counter = 1"); +molang.eval("v.counter = v.counter + 1"); +molang.eval("v.counter"); // 2.0 + +molang.eval("t.value = 5"); +molang.eval("t.value"); // 0.0, temp variables are gone after each call +``` + +## Bindings for a single evaluation + +Pass a `Consumer` as the second argument to add bindings that only exist for that one call. +It runs on the copied scope, before the scope becomes read-only, so the interpreter's own scope is never touched: + +```java +List scaled = MolangParser.parseAll("this * 2"); + +molang.eval(scaled); // 0.0 +molang.eval(scaled, scope -> scope.set("this", NumberValue.of(21))); // 42.0 +molang.eval(scaled); // 0.0 again +``` + +This is handy when the same parsed expression is evaluated for many different objects — give each call its own `this`, `context` or query object instead of creating an interpreter per object. + +## Parse errors + +`MolangParser.parseAll` throws a `ParseException` (an `IOException`) that tells you where the problem is: + +```java +try { + MolangParser.parseAll("math.sin("); +} catch (ParseException e) { + Cursor cursor = e.cursor(); // line and column, may be null +} +``` + +`eval(String)` and `prepareEval` never throw for bad syntax: they return `0` instead. +Register a handler to find out when that happens: + +```java +molang.handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +``` + +## What evaluation returns + +- Booleans are numbers: comparisons and `!` give `1` or `0`, and `true`/`false` are `1`/`0`. +- Dividing by zero gives `0`, and so does any `NaN` or infinity — the interpreter never returns them. +- Names that don't exist evaluate to `0` without an error, so a misspelled query silently gives `0`. +- If the final value isn't a number (a string, an object), the result is `0`. + +## Interpreter and compiler together + +The `runtime-compiler` module adds `MochaEngine`, which pairs an interpreter with a [compiler](../features/compiling.md) that share one scope: + +```java +MochaEngine engine = MochaEngine.createStandard(); + +engine.interpreter().eval("v.x = 3"); +MochaFunction function = engine.compiler().compile("math.pow(2, 10)"); +float result = function.evaluate(); // 1024.0 +``` + +`MochaEngine.from(interpreter)` wraps an interpreter you already have. + +## Threads + +The interpreter doesn't lock anything, and the standard `variable` binding is a plain map. +Give each independent owner its own interpreter — PlayerAnimationLibrary creates one per animation controller — instead of sharing one between threads. diff --git a/mochafloats/intro.md b/mochafloats/intro.md new file mode 100644 index 0000000..5fd9428 --- /dev/null +++ b/mochafloats/intro.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 1 +description: "mochafloats — a fast MoLang lexer, parser, interpreter and bytecode compiler for Java, forked from Mocha." +--- + +# Introduction + +mochafloats is a Java library that reads and runs [MoLang](https://bedrock.dev/docs/stable/Molang), the small expression language Minecraft Bedrock uses for data-driven values. +It contains a lexer, a parser, an interpreter and a compiler that turns expressions into JVM bytecode. + +It started as a fork of [Mocha](https://github.com/unnamed/mocha) by Unnamed Team and is maintained by the PlayerAnimationLibrary team. +[PlayerAnimationLibrary](/pal/intro) and [Emotecraft](/emotecraft/gettingstarted) run every MoLang expression in their animations through it. + +```java +MolangInterpreter molang = MolangInterpreter.standard(); + +float result = molang.eval("math.sqrt(3 * 3 + 4 * 4)"); // 5.0 +float swing = molang.eval("v.t = 0.25; return -math.sin(v.t * 360) * 30;"); // -30.0 +``` + +## What MoLang looks like + +Everything in MoLang evaluates to a number: `true` is `1`, `false` is `0`, and a whole script is basically one formula. +It has variables (`variable.x`, or `v.x` for short), host-provided values (`query.anim_time`), a `math` library, conditionals and loops: + +``` +v.speed = q.ground_speed * 4; +return q.is_on_ground ? math.sin(q.anim_time * 360 * v.speed) * 20 : 0; +``` + +See [MoLang support](./language.md) for everything mochafloats understands. + +## What is different from Mocha + +mochafloats computes with 32-bit `float` values instead of `double`, splits the library into modules, drops Javassist for the JDK's own ClassFile API and adds a binary format for sending parsed expressions over the network. +It also fixes bugs that are still present in Mocha 3.0.1, such as `-math.sin(90) * 30` evaluating to `0`, and its results were checked against Bedrock itself. + +[Why mochafloats?](./why_mochafloats.md) lists every difference, with the results of running the same expressions on both libraries. + +## Modules + +| Module | Contains | Depends on | +|--------------------|-------------------------------------------------------------------------------------|--------------------------| +| `lexer` | The tokenizer. | — | +| `parser` | The parser, the syntax tree and the network format for it. | `lexer`, Netty `ByteBuf` | +| `runtime` | `MolangInterpreter`, scopes, values, bindings and the standard `math` library. | `parser` | +| `runtime-compiler` | `MolangCompiler`, which compiles expressions to bytecode, and `MochaEngine`. | `runtime` | + +Each module brings the ones it depends on, so you only ever declare one of them. +Most projects only need `runtime`. + +## Where to start + +1. [Add the library to your project](./gettingstarted/installation.md) +2. [Evaluate your first expressions](./gettingstarted/usage.md) +3. [Expose your own values and functions to MoLang](./gettingstarted/bindings.md) +4. [Compile hot expressions to bytecode](./features/compiling.md) if interpreting them isn't fast enough + +Coming from Mocha or an older mochafloats? Read [Migrating to mochafloats 6](./migration/migrating_from_mocha.md). + +## Credits and license + +mochafloats is a fork of [Mocha](https://github.com/unnamed/mocha) © Unnamed Team, and like Mocha it is distributed under the [MIT license](https://github.com/PlayerAnimationLibrary/mochafloats/blob/HEAD/license.txt). +Parts of these pages are based on [Mocha's documentation](https://github.com/unnamed/mocha/tree/v3.0.0/docs). + +The source code lives at [PlayerAnimationLibrary/mochafloats](https://github.com/PlayerAnimationLibrary/mochafloats). diff --git a/mochafloats/language.md b/mochafloats/language.md new file mode 100644 index 0000000..d55171e --- /dev/null +++ b/mochafloats/language.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 5 +description: "The MoLang syntax, operators, keywords and math functions mochafloats supports, and where it differs from Bedrock." +--- + +# MoLang support + +mochafloats follows [Bedrock's MoLang](https://bedrock.dev/docs/stable/Molang), and 6.1 was checked against Bedrock itself. +This page lists exactly what it understands, so you know what you can write in an expression. + +## Literals + +| Literal | Examples | Notes | +|----------|------------------------------------------|-------------------------------------------------------------| +| Number | `5`, `0.25`, `-1.5`, `1.5e3`, `2.5E-4` | Stored as a 32-bit `float`. Scientific notation is supported. | +| Boolean | `true`, `false` | The same as `1` and `0`. | +| String | `'minecraft:pig'` | Single quotes only, no escape sequences. `"text"` is a syntax error. | + +## Names and scopes + +Names are case-insensitive and made of letters, digits and `_`. +Every expression starts from these top-level names: + +| Name | Provided by | Lifetime | +|------------------|-----------------------------|-----------------------------------------------------------| +| `math` | `MolangInterpreter.standard()` | Read-only. | +| `variable` / `v` | `MolangInterpreter.standard()` | Kept between evaluations of the same interpreter. | +| `temp` / `t` | every evaluation | Thrown away when the evaluation ends. | +| `query` / `q` and anything else | you, see [Bindings](./gettingstarted/bindings.md) | Whatever you decide. | + +A name that doesn't exist evaluates to `0`. + +## Operators + +From the tightest binding to the loosest: + +| Operators | Meaning | +|-------------------------------|---------------------------------------------------------------| +| `.` `()` `[]` | Property access, call, array index | +| `-x` `!x` | Negation, logical not | +| `->` | Evaluate the right side with the Java object on the left as the entity | +| `*` `/` | Multiplication, division (dividing by zero gives `0`) | +| `+` `-` | Addition, subtraction | +| `<` `<=` `>` `>=` | Comparison, gives `1` or `0` | +| `==` `!=` | Equality. Two strings compare by their text | +| `&&` | Logical and | +| `\|\|` | Logical or | +| `??` | The left side, or the right side if the left side doesn't exist. `0` counts as existing | +| `c ? a : b`, `c ? a` | Conditional. Without `:` it gives `0` when `c` is false. Chained conditionals group from the right: `a ? 1 : b ? 2 : 3` is `a ? 1 : (b ? 2 : 3)` | +| `=` | Assignment, to `v.*` and `t.*`. It takes everything on its right: `v.x = c ? 5 : 6` assigns the conditional, and `v.a = v.b = 1` assigns both | + +There is no `%` operator; use `math.mod(a, b)`. + +## Statements and control flow + +- `;` separates statements. A script with several statements returns the value of its `return`, and stops there. Empty statements (`;;`) are skipped. +- `{ ... }` groups statements into a block. +- `c ? { ... } : { ... }` runs the block of the chosen branch, and `c ? { ... }` runs its block when `c` is true. +- `loop(count, { ... })` runs a block `count` times. +- `for_each(t.item, array, { ... })` runs a block once for every element of an array, with `t.item` set to the element. +- `break` and `continue` work inside both loops. + +``` +t.total = 0; +loop(10, { + t.total = t.total + 1; + (t.total >= 5) ? break; +}); +return t.total; // 5 +``` + +A missing operand (`1 +`) or a trailing comma in a call (`math.abs(1,)`) is a syntax error, as in Bedrock. + +## Math functions + +Angles are in **degrees**: `math.sin` and `math.cos` take degrees, and `math.asin`, `math.acos`, `math.atan` and `math.atan2` return degrees. + +| Function | Result | +|--------------------------------------------|-------------------------------------------------------------------------| +| `math.abs(x)` | Absolute value. | +| `math.sign(x)` | `-1`, `0` or `1`. | +| `math.copy_sign(x, sign)` | `x` with the sign of `sign`. | +| `math.min(a, b)`, `math.max(a, b)` | The smaller / bigger value. | +| `math.clamp(x, min, max)` | `x` limited to the range. | +| `math.floor(x)`, `math.ceil(x)` | Round down / up. | +| `math.round(x)` | Round to the nearest whole number; halves round away from zero (`-2.5` → `-3`). | +| `math.trunc(x)` | Drop the fractional part (`-2.7` → `-2`). | +| `math.mod(a, b)` | Remainder of `a / b`, with the sign of `a` (`math.mod(-7, 3)` → `-1`). | +| `math.sqrt(x)`, `math.pow(a, b)` | Square root, power. | +| `math.exp(x)`, `math.ln(x)` | `e` to the power of `x`, natural logarithm. | +| `math.sin(deg)`, `math.cos(deg)` | Sine and cosine of an angle in degrees. | +| `math.asin(x)`, `math.acos(x)`, `math.atan(x)` | Inverse functions, in degrees. | +| `math.atan2(y, x)` | Angle of the point `(x, y)`, in degrees. | +| `math.d2r(deg)`, `math.r2d(rad)` | Degrees to radians and back. | +| `math.lerp(a, b, t)` | Linear interpolation from `a` to `b`. | +| `math.inverse_lerp(a, b, x)` | Where `x` lies between `a` and `b`, as `0`–`1` (`0` if `a == b`). | +| `math.lerprotate(a, b, t)` | Interpolation between two angles the short way round. | +| `math.min_angle(deg)` | The angle wrapped into `-180`–`180`. | +| `math.hermite_blend(t)` | `3t² - 2t³`, a smooth `0`–`1` curve. | +| `math.random(min, max)` | A random number between the two bounds, in either order. | +| `math.random_integer(min, max)` | A random whole number from the smaller bound to the bigger one, both included. | +| `math.die_roll(count, low, high)` | The sum of `count` random numbers between `low` and `high`. | +| `math.die_roll_integer(count, low, high)` | The sum of `count` random whole numbers from `low` to `high`, both included. | +| `math.pi`, `math.e` | The constants π and e. | + +`math.sign`, `math.copy_sign`, `math.inverse_lerp`, `math.d2r`, `math.r2d` and `math.e` don't exist in Mocha. + +:::info +PlayerAnimationLibrary adds the `math.ease_*` easing functions on top of this list; they belong to PAL, not to mochafloats. See [PAL's MoLang page](/pal/molang#easing-functions). +::: + +## Results + +- Results are 32-bit floats: about 7 significant digits, and whole numbers are exact up to 16,777,216. +- Dividing by zero gives `0`, and `NaN` and infinities are turned into `0`, both interpreted and in [compiled functions](./features/compiling.md). +- Strings, objects and functions evaluate to `0` when a number is needed. + +## Differences from Bedrock and Blockbench + +Of everything checked against Bedrock, one difference is left, and it's deliberate: `math.sqrt` of a negative number and `math.ln(0)` give `0`, where Bedrock gives `NaN` and `-Infinity`. A `NaN` that reaches a bone breaks the model. + +:::note +Blockbench previews animations with [MolangJS](https://github.com/JannisX11/MolangJS), which differs from Bedrock and mochafloats in a few places. In Blockbench: + +- `'a' == 'b'` is `1`: strings count as `0`. +- `math.round(-2.5)` is `-2`. +- `math.random_integer(3, 1)` is always `2`. +- `math.ln(0)` is `-Infinity`. +- `1 +` and `math.abs(1,)` are accepted. + +Avoid these in animations if what you see in Blockbench has to match the game exactly. +::: + +Versions before 6.1 differ from Bedrock in more places; see [Upgrading to 6.1](./migration/upgrading_to_6_1.md). diff --git a/mochafloats/migration/_category_.json b/mochafloats/migration/_category_.json new file mode 100644 index 0000000..0f64ee1 --- /dev/null +++ b/mochafloats/migration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Migration", + "position": 6 +} diff --git a/mochafloats/migration/index.md b/mochafloats/migration/index.md new file mode 100644 index 0000000..ee9499d --- /dev/null +++ b/mochafloats/migration/index.md @@ -0,0 +1,8 @@ +--- +description: "Moving to mochafloats from Mocha, and updating between mochafloats versions." +--- + +# Migration + +- [Migrating to mochafloats 6](./migrating_from_mocha.md) — move a project from Mocha 3, or from mochafloats 5 and earlier: the dependency, the packages and every changed call. +- [Upgrading to 6.1](./upgrading_to_6_1.md) — what evaluates differently after updating from mochafloats 6.0. diff --git a/mochafloats/migration/migrating_from_mocha.md b/mochafloats/migration/migrating_from_mocha.md new file mode 100644 index 0000000..599718f --- /dev/null +++ b/mochafloats/migration/migrating_from_mocha.md @@ -0,0 +1,216 @@ +--- +sidebar_position: 1 +sidebar_label: "Migrating to mochafloats 6" +description: "Moving a project from Mocha 3, or from mochafloats 5 and earlier, to mochafloats 6: dependencies, packages, the split MochaEngine, floats and what evaluates differently." +--- + +# Migrating to mochafloats 6 + +This page moves a project to mochafloats 6 from: + +- **Mocha 3** — `team.unnamed:mocha`, the original library. +- **mochafloats 5 or earlier** — `com.zigythebird:mochafloats`, versions 1.0 to 5.0.1. They kept Mocha's `team.unnamed.mocha` packages and its all-in-one `MochaEngine`, and already computed with floats. + +mochafloats 6 keeps the same concepts — scopes, bindings, `@Binding`, compiled interfaces — so most code needs a new dependency, new imports and a few renamed calls. +Each step says whom it applies to. + +## Before and after + +The same code on Mocha 3 and on mochafloats 6. Both print `2.0 30.0 5.0 0.75` for `seconds`, `angle`, `scaled` and `strength`. + +```java title="Mocha 3" +import team.unnamed.mocha.MochaEngine; +import team.unnamed.mocha.parser.ast.Expression; +import team.unnamed.mocha.runtime.MochaFunction; +import team.unnamed.mocha.runtime.binding.Binding; +import team.unnamed.mocha.runtime.compiled.MochaCompiledFunction; +import team.unnamed.mocha.runtime.compiled.Named; +import team.unnamed.mocha.runtime.value.MutableObjectBinding; +import team.unnamed.mocha.runtime.value.NumberValue; + +@Binding("game") +public final class GameBindings { + @Binding("ticks_to_seconds") + public static double ticksToSeconds(double ticks) { + return ticks / 20; + } +} + +public interface Falloff extends MochaCompiledFunction { + double apply(@Named("distance") double distance, @Named("radius") double radius); +} + +MochaEngine engine = MochaEngine.createStandard(); +engine.handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +engine.bind(GameBindings.class); + +MutableObjectBinding query = new MutableObjectBinding(); +query.set("anim_time", NumberValue.of(1.25)); +query.setFunction("scaled", value -> value * 4); +engine.scope().set("query", query); +engine.scope().set("q", query); + +double seconds = engine.eval("game.ticks_to_seconds(40)"); + +List wave = engine.parse("math.sin(q.anim_time * 72) * 30"); +double angle = engine.eval(wave); + +MochaFunction prepared = engine.prepareEval("q.scaled(q.anim_time)"); +double scaled = prepared.evaluate(); + +Falloff falloff = engine.compile("math.clamp(1 - distance / radius, 0, 1)", Falloff.class); +double strength = falloff.apply(2, 8); +``` + +```java title="mochafloats 6" +import org.redlance.mocha.parser.MolangParser; +import org.redlance.mocha.parser.ast.Expression; +import org.redlance.mocha.runtime.MochaEngine; +import org.redlance.mocha.runtime.binding.Binding; +import org.redlance.mocha.runtime.compiled.MochaCompiledFunction; +import org.redlance.mocha.runtime.compiled.Named; +import org.redlance.mocha.runtime.value.MutableObjectBinding; +import org.redlance.mocha.runtime.value.NumberValue; + +@Binding("game") +public final class GameBindings { + @Binding("ticks_to_seconds") + public static float ticksToSeconds(float ticks) { + return ticks / 20; + } +} + +public interface Falloff extends MochaCompiledFunction { + float apply(@Named("distance") float distance, @Named("radius") float radius); +} + +MochaEngine engine = MochaEngine.createStandard(); +engine.interpreter().handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +engine.compiler().handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +engine.interpreter().bind(GameBindings.class); + +MutableObjectBinding query = new MutableObjectBinding(); +query.set("anim_time", NumberValue.of(1.25F)); +query.setFunction("scaled", value -> value * 4); +engine.scope().set("query", query); +engine.scope().set("q", query); + +float seconds = engine.interpreter().eval("game.ticks_to_seconds(40)"); + +List wave = MolangParser.parseAll("math.sin(q.anim_time * 72) * 30"); +float angle = engine.interpreter().eval(wave); + +Supplier prepared = engine.interpreter().prepareEval("q.scaled(q.anim_time)"); +float scaled = prepared.get(); + +Falloff falloff = engine.compiler().compile("math.clamp(1 - distance / radius, 0, 1)", Falloff.class); +float strength = falloff.apply(2, 8); +``` + +On mochafloats 5 the first version works once `double` becomes `float`. + +## 1. Swap the dependency + +*Everyone.* + +| You depend on | Replace it with | +|------------------------------------------------------------------------|----------------------------------------------| +| `team.unnamed:mocha`, and `org.javassist:javassist` if you declared it | `org.redlance.mochafloats:runtime-compiler` | +| `com.zigythebird:mochafloats` | `org.redlance.mochafloats:runtime-compiler` | + +The old artifacts were a single jar with both the interpreter and the compiler; `runtime-compiler` is the module that has both. +If you never compile expressions, depend on `runtime` instead. The repository and the build snippets are on [Installation](../gettingstarted/installation.md). + +- mochafloats 6 needs **Java 24 or newer**, like mochafloats 5. Mocha 3 and mochafloats 1.0 ran on Java 8, and mochafloats 1.1 to 4.x on Java 16 or 17. +- A mod that bundles the library in its jar now bundles `lexer`, `parser`, `runtime` and, if it compiles, `runtime-compiler` — see [Bundling it inside a Minecraft mod](../gettingstarted/installation.md#bundling-it-inside-a-minecraft-mod). +- If your mod gets mochafloats through PlayerAnimationLibrary, don't add it yourself — see the tip in [Which module do I need?](../gettingstarted/installation.md#which-module-do-i-need) + +## 2. Update the imports + +*Everyone.* + +Replace `team.unnamed.mocha` with `org.redlance.mocha`. Every class keeps its name and subpackage, except these: + +| Before | mochafloats 6 | +|-----------------------------------------------------------------|---------------------------------------------------------------------| +| `team.unnamed.mocha.MochaEngine` | `org.redlance.mocha.runtime.MochaEngine` | +| `team.unnamed.mocha.util.CaseInsensitiveStringHashMap` | `org.redlance.mocha.runtime.util.CaseInsensitiveStringHashMap` | +| `team.unnamed.mocha.util.ExprBytesUtils` | `org.redlance.mocha.parser.util.ExprBytesUtils` | +| `team.unnamed.mocha.util.ExpressionListUtils` | `org.redlance.mocha.parser.util.ExpressionListUtils` | +| `team.unnamed.mocha.util.network.ProtocolUtils`, `VarIntUtils` | `org.redlance.mocha.parser.util.network.ProtocolUtils`, `VarIntUtils` | +| `team.unnamed.mocha.parser.ast.DoubleExpression` (Mocha 3) | `org.redlance.mocha.parser.ast.FloatExpression` | + +This command renames them all in your Java and Kotlin sources, the moved classes included: + +```bash +grep -rl --include='*.java' --include='*.kt' 'team\.unnamed\.mocha' . | xargs perl -pi -e ' + s/team\.unnamed\.mocha\.MochaEngine\b/org.redlance.mocha.runtime.MochaEngine/g; + s/team\.unnamed\.mocha\.util\.CaseInsensitiveStringHashMap\b/org.redlance.mocha.runtime.util.CaseInsensitiveStringHashMap/g; + s/team\.unnamed\.mocha\.util\.(ExprBytesUtils|ExpressionListUtils|network)\b/org.redlance.mocha.parser.util.$1/g; + s/team\.unnamed\.mocha\.parser\.ast\.DoubleExpression\b/org.redlance.mocha.parser.ast.FloatExpression/g; + s/team\.unnamed\.mocha\./org.redlance.mocha./g' +``` + +## 3. Update the calls + +*Everyone.* + +`MochaEngine` used to parse, evaluate, compile and bind by itself. In mochafloats 6 it's a class that holds an interpreter and a compiler sharing one scope: + +| Before | mochafloats 6 | +|------------------------------------------------------|------------------------------------------------------------------------| +| `MochaEngine.createStandard()`, `create()`, and their forms taking an entity | unchanged, or `MolangInterpreter.standard()` and `create()` if you don't compile | +| `MochaEngine.create(entity, builder -> ...)` | `MochaEngine.from(MolangInterpreter.create(entity, builder -> ...))` | +| `engine.eval(...)`, every overload | `engine.interpreter().eval(...)` | +| `engine.prepareEval(...)` → `MochaFunction` | `engine.interpreter().prepareEval(...)` → `Supplier` | +| `engine.parse(code)` | `MolangParser.parseAll(code)` | +| `engine.compile(...)`, every overload | `engine.compiler().compile(...)` | +| `engine.bind(type)`, `engine.bindInstance(...)` | `engine.interpreter().bind(type)`, `bindInstance(...)` | +| `engine.warnOnReflectiveFunctionUsage(true)` | `engine.interpreter().warnOnReflectiveFunctionUsage(true)` | +| `engine.handleParseExceptions(handler)` | set it on both `engine.interpreter()` and `engine.compiler()` | +| `engine.postCompile(consumer)` | `engine.compiler().postCompile(consumer)` | +| `engine.scope()` | unchanged: the scope both share | + +A `Supplier` boxes every result. In hot code, keep the parsed `List` and call `eval(list)`, which returns a primitive `float`. + +*Mocha 3 and mochafloats 4 or earlier*, which compiled with Javassist: + +| Before | mochafloats 6 | +|--------------------------------------------------|--------------------------------------| +| `new MolangCompiler(entity, classLoader, scope)` | `new MolangCompiler(entity, scope)` | +| `engine.classPool()`, `compiler.classPool()` | removed, there's no Javassist | + +## 4. From `double` to `float` + +*Mocha 3 only.* Every mochafloats version already computes with floats. + +- `Value.getAsNumber()`, `NumberValue.value()` and `MochaFunction.evaluate()` return `float`. `Value.of` and `NumberValue.of` accept both `float` and `double`. +- `ObjectValue.setFunction` takes `FloatFunction1`, `FloatFunction2` and `FloatFunction3` instead of `DoubleFunction1`–`3`, plus `FloatFunction` with no arguments. Lambdas usually compile unchanged once their arithmetic is `float`. +- The static methods of `MochaMath` and its `PI` constant are `float`. +- `@Binding` methods may still use `double`; arguments and results are converted. Using `float` avoids the conversions. +- A custom `ExpressionVisitor` overrides `visitFloat` instead of `visitDouble`. Create literals with `FloatExpression.of(value)`; the constructor taking a number is private. +- Compiled interfaces can keep `double` parameters and results, but `float` matches what the compiler computes with. + +## 5. Other API changes + +*Mocha 3 and early mochafloats versions.* mochafloats 5 already worked this way. + +- `Expression` and `Value` are `sealed`: you can no longer implement `Expression` yourself. +- Every expression has a `write(ByteBuf)` method for the [network format](../features/serialization.md). +- `toString()` on an expression prints MoLang instead of debug trees like `Call(Access(...))`. + +## 6. What evaluates differently + +Content tuned around the old results may need another look. + +**From Mocha 3:** + +- Expressions Mocha got wrong give the right result: `-math.sin(90) * 30` is `-30` instead of `0`, and `1e3` is `1000` instead of `0`. [Why mochafloats?](../why_mochafloats.md) has the full list. +- MoLang evaluates like Bedrock where Mocha didn't: `v.x = c ? 5 : 6` assigns `5` or `6` instead of `c`, strings compare by their text, `??` keeps a `0`, `math.round(-2.5)` is `-3`, and the random functions include both bounds. See [MoLang that evaluates like Bedrock](../why_mochafloats.md#molang-that-evaluates-like-bedrock). +- Syntax Mocha accepted, such as `1 +` or a trailing comma in `math.abs(1,)`, is a parse error. +- Results are floats, so very large or very precise values round differently: `16777217` becomes `16777216`, and `0.1 + 0.2` is `0.3` instead of `0.30000000000000004`. + +**From mochafloats 5 or earlier:** + +- mochafloats 5.0.1 evaluates like mochafloats 6.0, so everything in [Upgrading to 6.1](./upgrading_to_6_1.md) applies: the same table of Bedrock results, syntax errors, compiled functions and Java bindings. +- Before 5.0.1, a minus or `!` in front of a function call evaluated to `0`: `-math.sin(90) * 30` was `0` instead of `-30`, and `!math.abs(0)` was `0` instead of `1`. diff --git a/mochafloats/migration/upgrading_to_6_1.md b/mochafloats/migration/upgrading_to_6_1.md new file mode 100644 index 0000000..da72580 --- /dev/null +++ b/mochafloats/migration/upgrading_to_6_1.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 2 +description: "What changes when you update mochafloats from 6.0 to 6.1: MoLang that now evaluates like Bedrock, syntax errors, compiled functions, Java bindings and the network format." +--- + +# Upgrading to 6.1 + +6.1 doesn't remove or change any public API, so code written for 6.0 compiles against it unchanged. +Coming from mochafloats 5 or earlier? Start with [Migrating to mochafloats 6](./migrating_from_mocha.md). +What changes is the result of some MoLang: 6.1 was checked against Bedrock in the game and now agrees with it, and bugs in the parser, the compiler and the Java bindings are fixed. + +## MoLang that evaluates differently + +| Expression | 6.0 | 6.1 | Bedrock | +|---------------------------------------------------------|-------------|-------------|-------------| +| `v.x = 1 ? 5 : 6; return v.x;` | `1` | `5` | `5` | +| `1 ? 2 : 3 ? 4 : 5` | `4` | `2` | `2` | +| `v.w = 0; 1 ? { v.w = 4; } : { v.w = 5; }; return v.w;` | `0` | `4` | `4` | +| `v.zero = 0; return v.zero ?? 7;` | `7` | `0` | `0` | +| `'a' == 'b'` | `1` | `0` | `0` | +| `math.round(-2.5)` | `-2` | `-3` | `-3` | +| `math.random_integer(1, 3)` | 1 or 2 | 1 to 3 | 1 to 3 | +| `math.random(1, 1)`, `math.random(5, 1)` | throws | `1`, 1 to 5 | `1`, 1 to 5 | +| `math.die_roll(1, 5, 6)` | 1.25 to 2.5 | 5 to 6 | 5 to 6 | +| `v.a = 1;; v.b = 2; return v.b;` | `1` | `2` | `2` | + +What to look for in existing animations: + +- **`v.x = condition ? a : b`** assigns `a` or `b`; 6.0 assigned the condition. Content that wraps the conditional in parentheses, `v.x = (condition ? a : b)`, gives the same result in both versions. For the same reason, `v.a = v.b = 1` sets both variables, where 6.0 set only `v.a`. +- **Chained conditionals:** `a ? 1 : b ? 2 : 3` means `a ? 1 : (b ? 2 : 3)`; 6.0 read it as `(a ? 1 : b) ? 2 : 3`. +- **Blocks in a full conditional**, `c ? { ... } : { ... }`, run; in 6.0 neither block did. +- **`??`** falls back only when the left side doesn't exist: a variable set to `0` is kept. +- **Strings** compare by their text; in 6.0 any two strings were equal. +- **`math.round`** rounds halves away from zero. +- **Random functions** include both bounds and accept them in either order, and `math.die_roll` returns a sum between `count × low` and `count × high`. In 6.0 an equal or reversed pair of bounds made `math.random` and `math.random_integer` throw an exception out of `eval`. + +## Syntax errors are reported + +| Source | 6.0 | 6.1 | +|----------------------|----------------------------------------|-----------------| +| `1 +` | an empty script, evaluates to `0` | parse error | +| `math.abs(1,)` | `1`, with `null` as a second argument | parse error | +| `v.a = 1;; v.b = 2` | only `v.a = 1`, the rest is dropped | both statements | + +For the first two, `MolangParser.parseAll` throws a `ParseException`, and `eval(String)` returns `0` and passes the exception to the handler set with `handleParseExceptions`, as for any other syntax error. 6.0 reported neither, and Bedrock rejects both. + +## Compiled functions + +- A fraction counts as true: with `a = 0.5`, `a && 1` is `1` and `a ? 10 : 20` is `10`. 6.0 cut fractions to whole numbers first, so they were false. +- Dividing by zero gives `0`, and `NaN` or infinities, from arithmetic or returned by a Java method, become `0`, as in the interpreter. A method whose `@Binding` sets `skipChecking = true` is trusted and its result returned as it is. +- `t.*` assignments work in functions that return `double`, `int` or `boolean`; in 6.0 they failed verification. +- `c ? a` without `:` compiles. +- A construct the compiler doesn't support makes `compile` throw an `UnsupportedOperationException` that names it. In 6.0 some of them failed with an `IllegalArgumentException` about the operand stack, `loop` and `for_each` compiled to nothing, so the function returned a wrong value, and methods that take an `ExecutionContext`, a `Lazy` or varargs received `null`. + +If `compile` now throws for one of your expressions, evaluate that expression with the interpreter. See [what the compiler supports](../features/compiling.md#what-the-compiler-supports). + +## Java bindings + +- Interpreted calls are faster: `math.*` functions are called directly instead of through reflection, and other `@Binding` methods through a method handle that is prepared once. An expression with `math.sin` and `math.cos` takes 135 ns instead of 304 ns, and a call to a `@Binding` method 102 ns instead of 174 ns. +- `warnOnReflectiveFunctionUsage(true)` reports each reflectively called method once. 6.0 printed a line on every call, `math.*` included. +- Parameters of type `ExecutionContext`, `Lazy` and varargs receive their values in interpreted code. In 6.0 they received `null`, and primitive varargs threw a `ClassCastException`. +- A class with a single `@BindExternalFunction` binds it; 6.0 only read repeated annotations. +- A `Function` that returns `null` evaluates to `0`; in 6.0, `q.f() + 1` threw a `NullPointerException`. + +## Network format and printing + +- The binary format is unchanged, so 6.0 and 6.1 can exchange expressions. The exception is an empty string, `''`: 6.0 can't read it back, whichever version wrote it, and 6.1 reads it from both. +- An unknown operator byte throws an `IllegalArgumentException`; 6.0 read it as the first operator. +- `toString()` keeps the parentheses an expression needs: `(1 + 2) * 3` prints as `(1+2)*3` instead of `1+2*3`, so the printed text parses back into the same expression. +- `break` and `continue` expressions implement `equals` and `hashCode`. + +*Times measured with JMH on JDK 25 (Apple Silicon), average per call.* diff --git a/mochafloats/why_mochafloats.md b/mochafloats/why_mochafloats.md new file mode 100644 index 0000000..6a94cca --- /dev/null +++ b/mochafloats/why_mochafloats.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 2 +description: "What mochafloats fixes and adds compared to Mocha 3.0.1, with the results of running the same expressions on both." +--- + +# Why mochafloats? + +Mocha 3.0.1, released in March 2025, is the last version of the original library. +Its `main` branch hasn't changed since, the bug reports [#27](https://github.com/unnamed/mocha/issues/27) and [#29](https://github.com/unnamed/mocha/issues/29) are still open, and the pull request that ported it to `float` ([#22](https://github.com/unnamed/mocha/pull/22)) was never merged. + +mochafloats picked up from there: well over a hundred commits on top of Mocha 3.0.1, released as versions up to 6.1.0. +This page lists what that changed. + +## At a glance + +| | Mocha 3.0.1 | mochafloats 6.1.0 | +|------------------------------------------|---------------------------------------|------------------------------------------------------| +| Number type | `double` | `float` | +| Java | 8+ | 24+ | +| Bytecode generation | Javassist 3.30.2 (≈ 795 KB extra jar) | JDK ClassFile API, no extra jar | +| What you ship to only interpret MoLang | ≈ 930 KB (Mocha + Javassist) | ≈ 125 KB (`lexer` + `parser` + `runtime`) | +| Interpreting `math.sin(...) * 45 + math.cos(...)` | 248 ns | 157 ns | +| Compiled functions | regular classes, never unloaded | hidden classes, unloaded with the function | +| Sending parsed expressions over network | — | built in, on Netty `ByteBuf` | +| Checked against Bedrock | — | [yes](#molang-that-evaluates-like-bedrock) | +| `-math.sin(90) * 30` | `0` | `-30` | +| `v.x = 1 ? 5 : 6; return v.x;` | `1` | `5`, like Bedrock | +| `1.0E-4 * 10000` | `0` and a parse error | `1` | +| `math.PI` on a Turkish system | `0` | `3.1415927` | +| `math.e`, `math.sign`, `math.inverse_lerp` | `0` (missing) | implemented | +| Bundled inside a NeoForge mod | crashes when compiling ([#27](https://github.com/unnamed/mocha/issues/27)) | works | +| Compiled calls to instance methods | fail verification ([#29](https://github.com/unnamed/mocha/issues/29)) | work | + +:::info How these results were measured +Every expression on this page was run on both libraries, interpreted and compiled, with a standard engine (`createStandard()`) on JDK 25. +Where a query appears, `q.length('abc')` is a Java method bound with `@Binding` that returns the length of its argument. +The Bedrock results come from a behaviour pack whose animation controllers evaluate each expression in the game and report the result. +::: + +## Bugs fixed only in mochafloats + +### A minus or `!` in front of a function call + +Mocha attaches a leading `-` or `!` to the function name instead of the call, so `-math.abs(5)` is read as `(-math.abs)(5)`: it calls a negated function, which evaluates to `0`. +Negating a `sin` or `cos` is one of the most common things an animation does, and every such expression silently produced `0`. + +| Expression | Mocha 3.0.1 | mochafloats | +|-----------------------|-------------|-------------| +| `-math.sin(90) * 30` | `0` | `-30` | +| `-math.abs(3) * 2` | `0` | `-6` | +| `!math.abs(0)` | `0` | `1` | +| `-q.length('abc')` | `0` | `-3` | +| `!q.length('')` | `0` | `1` | + +The results are the same interpreted and compiled. Fixed in [#55](https://github.com/PlayerAnimationLibrary/mochafloats/pull/55). + +### MoLang that evaluates like Bedrock + +mochafloats 6.1 was checked against Bedrock itself, and against five other MoLang implementations where Bedrock wasn't tested. +These expressions give different results in Mocha: + +| Expression | Mocha 3.0.1 | mochafloats 6.1 | Bedrock | +|--------------------------------------------------------|-------------|-----------------|---------| +| `v.x = 1 ? 5 : 6; return v.x;` | `1` | `5` | `5` | +| `1 ? 2 : 3 ? 4 : 5` | `4` | `2` | `2` | +| `v.w = 0; 1 ? { v.w = 4; } : { v.w = 5; }; return v.w;` | `0` | `4` | `4` | +| `v.zero = 0; return v.zero ?? 7;` | `7` | `0` | `0` | +| `'a' == 'b'` | `1` | `0` | `0` | +| `math.round(-2.5)` | `-2` | `-3` | `-3` | +| `math.random_integer(1, 3)` | 1 or 2 | 1 to 3 | 1 to 3 | +| `math.random(1, 1)` | throws | `1` | `1` | +| `math.die_roll(1, 5, 6)` | 1.25 to 2.5 | 5 to 6 | 5 to 6 | + +- Mocha binds `=` tighter than `? :`, so `v.x = c ? 5 : 6` assigns `c`; and it groups chained conditionals from the left. +- In `c ? { ... } : { ... }`, Mocha never runs either block. +- Mocha's `??` falls back on `0` as well as on a missing value. +- Mocha compares strings as numbers, so any two strings are equal. +- Mocha's `math.random`, `math.random_integer` and `math.die_roll_integer` throw an `IllegalArgumentException` out of `eval` when both bounds are equal or reversed, and `random_integer` never returns its upper bound. +- Mocha's `math.die_roll` sums whole numbers from `low` to `low + high - 1` and divides the total by 4. + +The one deliberate difference from Bedrock is in [MoLang support](./language.md#differences-from-bedrock-and-blockbench). + +### Numbers in scientific notation + +Mocha's lexer stops reading a number at the `e`, so anything written in scientific notation fails to parse and the whole expression evaluates to `0`. +That's the format Java, JavaScript and many exporters print very small and very large numbers in. + +| Expression | Mocha 3.0.1 | mochafloats | +|-------------------|------------------------------------------------------------|-------------| +| `1.0E-4 * 10000` | `0`, parse error *Expected a semicolon, but was IDENTIFIER(E)* | `1` | +| `2.5e-1` | `0` | `0.25` | +| `1.5E+2` | `0` | `150` | + +### Names on a Turkish (or Azerbaijani) system + +MoLang names are case-insensitive, and Mocha lowercases them with the system's language rules. +In Turkish the lowercase of `I` is the dotless `ı`, so on a Turkish system `math.PI` looks up `pı`, finds nothing and evaluates to `0`. + +| Expression (Turkish system locale) | Mocha 3.0.1 | mochafloats | +|------------------------------------|-------------|-------------| +| `math.PI` | `0` | `3.1415927` | +| `MATH.MIN(1, 2)` | `0` | `1` | + +mochafloats lowercases with `Locale.ROOT`, so the result no longer depends on the player's language. Fixed in [#18](https://github.com/PlayerAnimationLibrary/mochafloats/pull/18). + +### Compiling calls to Java methods + +Mocha's compiler generates invalid bytecode for several kinds of Java calls, so the expression can't be compiled at all: + +| Expression | Mocha 3.0.1 | mochafloats | +|-----------------------------------------|---------------------------------------------------|-------------| +| `q.length('abc') + 1` | compile error: *stack underflow* | `4` | +| `q.body_float('chest', 'power') - 100` | compile error: *stack underflow* | `100` | +| `offset.calc(1)` (an instance method) | `VerifyError`: *Bad type on operand stack* | `4` | + +- A call left the type of its last parameter as the "expected type" for the rest of the expression, so an arithmetic operation after a call with a `String`, `int`, `long`, `double` or `boolean` parameter broke. Fixed in [#55](https://github.com/PlayerAnimationLibrary/mochafloats/pull/55) ([#54](https://github.com/PlayerAnimationLibrary/mochafloats/issues/54)). +- For instance methods, the arguments were pushed before the object the method is called on. This is Mocha's open issue [#29](https://github.com/unnamed/mocha/issues/29). + +### Compiled code that disagrees with the interpreter + +| Compiled | Mocha 3.0.1 | mochafloats | +|-------------------------------------------------------|---------------------------------------------------|-------------| +| `a && 1` with `a = 0.5` | `0` | `1` | +| `a ? 10 : 20` with `a = 0.5` | `20` | `10` | +| `1 / a` with `a = 0` | `Infinity` | `0` | +| `-(a > b) ? 10 : 20` with `a = 5`, `b = 4` | `20` | `10` | +| `!(a > b)` with `a = 1`, `b = 2`, returning `boolean` | `ArrayIndexOutOfBoundsException` while compiling | `true` | + +Mocha's compiler turns a number into a boolean by cutting it to an integer first, so every value between `-1` and `1` is false. +It doesn't apply MoLang's division-by-zero rule, it flips the truth of a negated boolean, and it emits a broken jump for `!` when the surrounding code expects a `boolean`. +mochafloats' compiled functions give the interpreter's results, and constructs the compiler doesn't support are rejected with an error that names them instead of turning into invalid bytecode. See [what the compiler supports](./features/compiling.md#what-the-compiler-supports). + +### Java bindings + +| With bindings made with `@Binding` / lambdas | Mocha 3.0.1 | mochafloats | +|---------------------------------------------------------------------|-----------------------------|------------------| +| A class with a single `@BindExternalFunction` | nothing is bound | bound | +| A method parameter of type `ExecutionContext`, `Lazy` or varargs | receives `null`, or throws | works | +| `q.f() + 1` where the `Function` returns `null` | `NullPointerException` | `1` | +| `warnOnReflectiveFunctionUsage(true)`, 3 evaluations with 2 calls | 6 warnings, `math.*` included | each reflective method once | + +### Compiled classes that collide and never go away + +Mocha names every compiled class after the current millisecond plus a random number below 2024. +When many expressions are compiled at once — for example while resources load — two of them eventually get the same name, and the second fails with `frozen class (cannot edit)`. +The classes it defines are also never unloaded, and Javassist's global `ClassPool` keeps a copy of each one. + +Compiling 20,000 expressions in a loop: + +| | Mocha 3.0.1 | mochafloats | +|-----------------------------------|-------------|-------------| +| Failed compilations | 242 | 0 | +| Generated classes unloaded by GC | 0 | 20,000 | +| Heap still used after GC | 75 MB | 2 MB | + +mochafloats numbers its classes with an atomic counter and defines them as [hidden classes](https://openjdk.org/jeps/371), which the JVM can unload once nothing references the compiled function anymore. + +### Crash when bundled inside a NeoForge mod + +When Mocha is shipped inside a mod's jar, NeoForge loads it in a separate module layer, and compiling a function that implements one of the mod's interfaces crashes with `IllegalAccessError: superinterface check failed` ([#27](https://github.com/unnamed/mocha/issues/27), still open). +mochafloats' jars declare `FMLModType: GAMELIBRARY`, so NeoForge loads them next to the mods that use them. + +## Features only in mochafloats + +### A faster interpreter + +| Interpreted, pre-parsed | Mocha 3.0.1 | mochafloats 6.1 | +|-------------------------------------------------------------------|--------------------|---------------------| +| `5` | 71 ns, 664 bytes | **0.9 ns, 0 bytes** | +| `t.t = 3; return 3*t.t*t.t - 2*t.t*t.t*t.t;` | 180 ns, 1000 bytes | **167 ns, 712 bytes** | +| `t.a = 1.25; return math.sin(t.a * 50) * 45 + math.cos(t.a * 20);` | 248 ns, 1408 bytes | **157 ns, 824 bytes** | +| A method bound with `@Binding` | 137 ns, 968 bytes | **102 ns, 696 bytes** | + +*Measured with JMH on JDK 25 (Apple Silicon), average time and allocation per call.* + +- An expression that is just a number, like most keyframe values, is returned straight away; Mocha copies the whole scope and creates an interpreter for it on every call. +- `math.*` functions are called directly instead of through reflection. +- Other `@Binding` methods are called through a method handle that is prepared once. + +Compiled functions run in about half a nanosecond for simple expressions in Mocha, mochafloats and Moonflower's molang-compiler alike. On the arithmetic and math expressions above, mochafloats' compiled code is 10 to 15% slower than the other two, because it also applies MoLang's rules for division by zero and `NaN`. + +
+Compared with other Java MoLang libraries + +| Interpreted | mochafloats 6.1 | Mocha 3.0.1 | bedrockk/MoLang | +|-------------------------------------------|-----------------|-------------|-----------------| +| `5` | 0.9 ns | 71 ns | 74 ns | +| arithmetic with temp variables (above) | 167 ns | 180 ns | 2212 ns | +| `math.sin` and `math.cos` (above) | 157 ns | 248 ns | 2159 ns | + +| Compiled | mochafloats 6.1 | Mocha 3.0.1 | molang-compiler | +|-------------------------------------------|-----------------|-------------|-----------------| +| `5` | 0.50 ns | 0.46 ns | 0.52 ns | +| arithmetic with temp variables (above) | 0.51 ns | 0.45 ns | 0.44 ns | +| `math.sin` and `math.cos` (above) | 5.3 ns | 4.7 ns | 4.6 ns | + +In this test molang-compiler 3.1.1.19 returned `189` instead of `-27` for the arithmetic expression, and bedrockk/MoLang computed `math.sin` and `math.cos` in radians; their times are shown as measured. + +
+ +### Floats everywhere + +Literals, values, function arguments and results are 32-bit floats, the same type Minecraft's models use for part rotations, offsets and scales. +A keyframe value goes from MoLang to the model without being converted from `double` to `float` and back. + +### Pick only what you need + +The library is split into [four modules](./intro.md#modules). +A mod that only interprets MoLang ships `lexer`, `parser` and `runtime` — about 125 KB — while Mocha can't even create an interpreter without Javassist on the classpath. + +### No Javassist + +The compiler builds classes with the JDK's ClassFile API (`java.lang.classfile`, final since Java 24) and defines them as hidden classes. +There is no third-party bytecode library to ship, shade or conflict with another mod's copy. + +### Sending expressions over the network + +Parsed expressions can be written to a Netty `ByteBuf` and read back on the other side, without turning them back into text and parsing them again. +PlayerAnimationLibrary's binary animation format, the one that goes over the network, stores expressions this way. See [Sending expressions over the network](./features/serialization.md). + +### More of the `math` library + +`math.e`, `math.sign`, `math.copy_sign`, `math.inverse_lerp`, `math.d2r` and `math.r2d` are implemented; in Mocha they evaluate to `0`. + +| Expression | Mocha 3.0.1 | mochafloats | +|----------------------------------|-------------|-------------| +| `math.e` | `0` | `2.7182817` | +| `math.sign(-4)` | `0` | `-1` | +| `math.copy_sign(3, -1)` | `0` | `-3` | +| `math.inverse_lerp(0, 10, 2.5)` | `0` | `0.25` | +| `math.d2r(180)` | `0` | `3.1415927` | +| `math.r2d(math.pi)` | `0` | `180` | + +### Values that only exist for one evaluation + +`MolangInterpreter.eval(expressions, scope -> ...)` lets you add bindings that only that one evaluation can see — for example a `this` or a `context` object — without touching the interpreter's shared scope. See [Bindings for a single evaluation](./gettingstarted/usage.md#bindings-for-a-single-evaluation). + +### Syntax trees that print as MoLang + +`toString()` on a parsed expression prints it in MoLang syntax — `math.sin(q.anim_time*50)*45` instead of `Call(Access(Identifier(math), sin), [...])` — with parentheses wherever they're needed, so the text parses back into the same expression. See [Printing expressions as text](./features/serialization.md#printing-expressions-as-text). + +### Smaller changes + +- `Expression`, `Value`, `MolangLexer` and `MolangParser` are `sealed`, so a `switch` over them can be checked for exhaustiveness. +- The compiler's constant folding can be replaced or turned off: `new MolangCompiler(entity, scope, null)`. +- `ObjectValue.setFunction` accepts functions with no arguments. +- The runtime is annotated for [J2ObjC](https://developers.google.com/j2objc) and falls back to plain reflection where `java.lang.invoke` isn't available. +- The test suite checks 141 expressions against [MolangJS](https://github.com/JannisX11/MolangJS), the MoLang implementation Blockbench uses, both interpreted and compiled, and CI builds and tests every pull request. + +## What it costs you + +- **Java 24 or newer.** The ClassFile API doesn't exist before Java 24. Minecraft 26.1 and newer runs on Java 25. +- **Float precision.** A float holds about 7 significant digits, and whole numbers are only exact up to 16,777,216: `16777217` evaluates to `16777216`. That's plenty for animation, but large counters lose precision — a world's game time in ticks (`q.time_stamp` in PAL) passes that limit after about 9.7 days of running, so prefer values that stay small, like `q.anim_time`. +- **A different API.** Packages moved to `org.redlance.mocha`, and `MochaEngine` was split into an interpreter and a compiler. [Migrating to mochafloats 6](./migration/migrating_from_mocha.md) maps every old call to its new place. + +Already on mochafloats 6.0? [Upgrading to 6.1](./migration/upgrading_to_6_1.md) lists what changed. diff --git a/pal/intro.md b/pal/intro.md index 9711305..f041ee8 100644 --- a/pal/intro.md +++ b/pal/intro.md @@ -14,7 +14,7 @@ Here are a couple of the features the mod provides that you might find very usef - Provides an easy way to play JSON animations made in Blockbench **and** Blender, while allowing you to modify them using code. - Enables smooth transitions between all animations. - Has a priority system, so conflicts between two mods trying to animate the player at the same time can be resolved. -- Almost full [MoLang](./molang.md) support using [Mocha](https://unnamed.team/docs/mocha) — even stuff like arrow `->` operators and variables! +- Almost full [MoLang](./molang.md) support using [mochafloats](/mochafloats/intro), our fork of Mocha — even stuff like arrow `->` operators and variables! ## Why should I use this over PlayerAnimator? diff --git a/pal/molang.md b/pal/molang.md index e343468..dcef447 100644 --- a/pal/molang.md +++ b/pal/molang.md @@ -9,7 +9,7 @@ description: "MoLang in PAL: syntax, built-in queries, math functions and how to If you are unfamiliar with what MoLang is and want to use it to create an animation, refer to the [MoLang page in the Emotecraft wiki](/emotecraft/creatingemotes/molang). ::: -PAL evaluates MoLang with [mochafloats](https://github.com/PlayerAnimationLibrary/mochafloats), a fork of [Mocha](https://unnamed.team/docs/mocha) that computes with floats instead of doubles. +PAL evaluates MoLang with [mochafloats](/mochafloats/intro), our fork of [Mocha](https://github.com/unnamed/mocha) that computes with floats instead of doubles and fixes bugs Mocha still has — see [Why mochafloats?](/mochafloats/why_mochafloats). Almost all of Bedrock MoLang's functionality is present, and all MoLang queries and functions have been implemented — [here is the full documentation for Bedrock MoLang](https://bedrock.dev/docs/stable/Molang). MoLang lets you compute a keyframe value while the animation plays instead of baking it into keyframes, so you can react to the player's state or build motion that never repeats itself. diff --git a/plugins/mochafloats-version-plugin.js b/plugins/mochafloats-version-plugin.js new file mode 100644 index 0000000..602b488 --- /dev/null +++ b/plugins/mochafloats-version-plugin.js @@ -0,0 +1,27 @@ +const MAVEN_METADATA_URL = + 'https://repo.redlance.org/public/org/redlance/mochafloats/runtime/maven-metadata.xml'; + +module.exports = function mochafloatsVersionPlugin() { + return { + name: 'mochafloats-version-plugin', + + async loadContent() { + try { + const response = await fetch(MAVEN_METADATA_URL); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const xml = await response.text(); + const release = xml.match(/([^<]+)<\/release>/); + return {release: release ? release[1] : null}; + } catch (error) { + console.warn('[mochafloats-version-plugin] Failed to fetch the version:', error); + return {release: null}; + } + }, + + async contentLoaded({content, actions}) { + actions.setGlobalData(content); + }, + }; +}; diff --git a/src/components/MochaFloatsVersion.tsx b/src/components/MochaFloatsVersion.tsx new file mode 100644 index 0000000..d2b686f --- /dev/null +++ b/src/components/MochaFloatsVersion.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {usePluginData} from '@docusaurus/useGlobalData'; + +export default function MochaFloatsVersion() { + const data = usePluginData('mochafloats-version-plugin') as {release: string | null} | undefined; + + if (!data?.release) { + return not found; + } + + return {data.release}; +} diff --git a/src/pages/index.md b/src/pages/index.md index 2a6769e..829a8c6 100644 --- a/src/pages/index.md +++ b/src/pages/index.md @@ -26,5 +26,13 @@ Official documentation for PlayerAnimationLibrary, Emotecraft and other projects linkTitle="View Documentation" /> +
+ +
\ No newline at end of file diff --git a/static/_redirects b/static/_redirects index 20c6ca9..f2f8b50 100644 --- a/static/_redirects +++ b/static/_redirects @@ -2,6 +2,7 @@ /emotecraft /emotecraft/gettingstarted 308 /pal /pal/intro 308 /playeranimationlibrary /pal/intro 308 +/mochafloats /mochafloats/intro 308 /blog/2025-07-12-emotectaft-300 /blog/2025-07-12-emotecraft-300 308 /pal/how_to_add_lib_to_mod /pal/gettingstarted/how_to_add_lib_to_mod 308 /pal/how_to_play_animations /pal/gettingstarted/how_to_play_animations 308 diff --git a/tsconfig.json b/tsconfig.json index 920d7a6..4e6f77d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,11 @@ // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { - "baseUrl": "." + // TypeScript 7 removed baseUrl, which @docusaurus/tsconfig still sets + "baseUrl": null, + "paths": { + "@site/*": ["./*"] + } }, "exclude": [".docusaurus", "build"] }