Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const config: Config = {

plugins: [
'./plugins/maven-versions-plugin',
'./plugins/mochafloats-version-plugin',
[
'@docusaurus/plugin-content-docs',
{
Expand All @@ -81,6 +82,15 @@ const config: Config = {
sidebarPath: './sidebars.ts',
},
],
[
'@docusaurus/plugin-content-docs',
{
id: 'mochafloats',
path: "mochafloats",
routeBasePath: "mochafloats",
sidebarPath: './sidebars.ts',
},
],
],

themeConfig: {
Expand Down Expand Up @@ -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",
Expand All @@ -135,6 +152,10 @@ const config: Config = {
{
to: "/emotecraft/gettingstarted",
label: "Emotecraft Documentation",
},
{
to: "/mochafloats/intro",
label: "MochaFloats Documentation",
}
],
},
Expand Down
4 changes: 4 additions & 0 deletions mochafloats/features/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Features",
"position": 4
}
116 changes: 116 additions & 0 deletions mochafloats/features/compiling.md
Original file line number Diff line number Diff line change
@@ -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);
```
10 changes: 10 additions & 0 deletions mochafloats/features/index.md
Original file line number Diff line number Diff line change
@@ -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.
65 changes: 65 additions & 0 deletions mochafloats/features/serialization.md
Original file line number Diff line number Diff line change
@@ -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<Expression> expressions = MolangParser.parseAll("math.sin(q.anim_time * 360) * 30");

// sender
ExprBytesUtils.writeExpressions(expressions, buf);

// receiver
List<Expression> 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<Expression> 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.
4 changes: 4 additions & 0 deletions mochafloats/gettingstarted/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Getting Started",
"position": 3
}
Loading