Skip to main content

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 and #29 are still open, and the pull request that ported it to float (#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.1mochafloats 6.1.0
Number typedoublefloat
Java8+24+
Bytecode generationJavassist 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 ns157 ns
Compiled functionsregular classes, never unloadedhidden classes, unloaded with the function
Sending parsed expressions over network—built in, on Netty ByteBuf
Checked against Bedrock—yes
-math.sin(90) * 300-30
v.x = 1 ? 5 : 6; return v.x;15, like Bedrock
1.0E-4 * 100000 and a parse error1
math.PI on a Turkish system03.1415927
math.e, math.sign, math.inverse_lerp0 (missing)implemented
Bundled inside a NeoForge modcrashes when compiling (#27)works
Compiled calls to instance methodsfail verification (#29)work
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.

ExpressionMocha 3.0.1mochafloats
-math.sin(90) * 300-30
-math.abs(3) * 20-6
!math.abs(0)01
-q.length('abc')0-3
!q.length('')01

The results are the same interpreted and compiled. Fixed in #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:

ExpressionMocha 3.0.1mochafloats 6.1Bedrock
v.x = 1 ? 5 : 6; return v.x;155
1 ? 2 : 3 ? 4 : 5422
v.w = 0; 1 ? { v.w = 4; } : { v.w = 5; }; return v.w;044
v.zero = 0; return v.zero ?? 7;700
'a' == 'b'100
math.round(-2.5)-2-3-3
math.random_integer(1, 3)1 or 21 to 31 to 3
math.random(1, 1)throws11
math.die_roll(1, 5, 6)1.25 to 2.55 to 65 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.

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.

ExpressionMocha 3.0.1mochafloats
1.0E-4 * 100000, parse error Expected a semicolon, but was IDENTIFIER(E)1
2.5e-100.25
1.5E+20150

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.1mochafloats
math.PI03.1415927
MATH.MIN(1, 2)01

mochafloats lowercases with Locale.ROOT, so the result no longer depends on the player's language. Fixed in #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:

ExpressionMocha 3.0.1mochafloats
q.length('abc') + 1compile error: stack underflow4
q.body_float('chest', 'power') - 100compile error: stack underflow100
offset.calc(1) (an instance method)VerifyError: Bad type on operand stack4
  • 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 (#54).
  • For instance methods, the arguments were pushed before the object the method is called on. This is Mocha's open issue #29.

Compiled code that disagrees with the interpreter​

CompiledMocha 3.0.1mochafloats
a && 1 with a = 0.501
a ? 10 : 20 with a = 0.52010
1 / a with a = 0Infinity0
-(a > b) ? 10 : 20 with a = 5, b = 42010
!(a > b) with a = 1, b = 2, returning booleanArrayIndexOutOfBoundsException while compilingtrue

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.

Java bindings​

With bindings made with @Binding / lambdasMocha 3.0.1mochafloats
A class with a single @BindExternalFunctionnothing is boundbound
A method parameter of type ExecutionContext, Lazy<T> or varargsreceives null, or throwsworks
q.f() + 1 where the Function returns nullNullPointerException1
warnOnReflectiveFunctionUsage(true), 3 evaluations with 2 calls6 warnings, math.* includedeach 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.1mochafloats
Failed compilations2420
Generated classes unloaded by GC020,000
Heap still used after GC75 MB2 MB

mochafloats numbers its classes with an atomic counter and defines them as hidden classes, 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, 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-parsedMocha 3.0.1mochafloats 6.1
571 ns, 664 bytes0.9 ns, 0 bytes
t.t = 3; return 3*t.t*t.t - 2*t.t*t.t*t.t;180 ns, 1000 bytes167 ns, 712 bytes
t.a = 1.25; return math.sin(t.a * 50) * 45 + math.cos(t.a * 20);248 ns, 1408 bytes157 ns, 824 bytes
A method bound with @Binding137 ns, 968 bytes102 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
Interpretedmochafloats 6.1Mocha 3.0.1bedrockk/MoLang
50.9 ns71 ns74 ns
arithmetic with temp variables (above)167 ns180 ns2212 ns
math.sin and math.cos (above)157 ns248 ns2159 ns
Compiledmochafloats 6.1Mocha 3.0.1molang-compiler
50.50 ns0.46 ns0.52 ns
arithmetic with temp variables (above)0.51 ns0.45 ns0.44 ns
math.sin and math.cos (above)5.3 ns4.7 ns4.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. 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.

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.

ExpressionMocha 3.0.1mochafloats
math.e02.7182817
math.sign(-4)0-1
math.copy_sign(3, -1)0-3
math.inverse_lerp(0, 10, 2.5)00.25
math.d2r(180)03.1415927
math.r2d(math.pi)0180

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.

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.

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 and falls back to plain reflection where java.lang.invoke isn't available.
  • The test suite checks 141 expressions against 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 maps every old call to its new place.

Already on mochafloats 6.0? Upgrading to 6.1 lists what changed.