Repository navigation
6.1.0: faster interpreter, Bedrock-compatible MoLang and compiler fixes - #58
Merged
Merged
Conversation
ReflectiveFunction called MethodHandle.invokeWithArguments, which builds a new spreader on every call. The handle is now adapted once to (Object[])Object and called with invokeExact. MochaMath registered a function for every math binding, but it didn't override set or entries(), so nothing was stored and JavaObjectBinding found no backing functions: every interpreted math.* call went through reflection. The functions are now stored and exposed as backing functions, with the same purity as their bindings. pi and e stay out of entries() so their fields are still inlined as constants. warnOnReflectiveFunctionUsage printed a line on every call, including calls that don't use reflection. It now reports each reflectively called method once. JMH, JDK 25, interpreting pre-parsed expressions (Mocha 3.0.1 / before / after): math.sin(v.t * 50) * 45 + math.cos(v.t * 20) 240 / 304 / 132 ns jq.speed(v.t) + 1 (instance @binding method) 137 / 174 / 106 ns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
ReflectiveFunction wrapped these arguments in a JavaValue and then ran them through JavaTypes.convert, which returns null for anything but numbers, booleans and strings. ExecutionContext and Lazy parameters received null, varargs parameters received null (or a one-element array through invokeWithArguments), and primitive varargs threw a ClassCastException. Values wrapped in a JavaValue are now passed as they are when the parameter accepts them, varargs are collected into an array of the component type with each element converted, and a Lazy whose type argument is itself parameterized (Lazy<ExecutionContext<?>>) no longer throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
… zero @BindExternalFunction is repeatable, and Java only wraps it into BindExternalFunction.Multiple when it's repeated. JavaObjectBinding only read the container, so a class with a single external function bound nothing. It now uses getDeclaredAnnotationsByType, which returns both. A bound Function returning null worked as a whole expression, but "q.nothing() + 1" threw a NullPointerException because visitCall passed the null on. It now reads as Value.nil(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
…x random ranges
Behaviour decided by comparing MolangJS, bridge's molang, bedrockk/MoLang,
Moonflower's molang-compiler and molang-py:
- In "c ? { ... } : { ... }" neither block ran: the branch evaluated to
the block's function, which was never called. The chosen block now
runs, like the block of "c ? { ... }" already did (every library that
supports both blocks and conditionals).
- "??" fell back on any falsy value, so with v.x = 0, "v.x ?? 7" was 7.
It now only falls back when the variable or property doesn't exist
(4 of 5).
- math.random, math.random_integer, math.die_roll and
math.die_roll_integer threw IllegalArgumentException when the bounds
were equal or reversed, which escaped MolangInterpreter.eval. No other
library throws: the bounds can now come in either order and be equal.
- math.random_integer and math.die_roll_integer never returned the upper
bound. Both bounds are now included: MolangJS, bridge and molang-py
include it, molang-compiler excludes it, bedrockk throws.
- math.die_roll summed values from low to low + high and divided the sum
by 4. It now sums random numbers between low and high (3 of 5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
Checked in Bedrock with a behaviour pack: - "'a' == 'b'" is 0 and "'a' != 'b'" is 1 in Bedrock. mochafloats compared every string as 0, so two strings were always equal. The libraries didn't agree here (only bridge compares the text). Two strings now compare by their text; a string and a number still compare as numbers. - math.round(-2.5) is -3 in Bedrock, which rounds halves away from zero. Math.round gave -2. Positive values round the same way as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
… and print parseable text
Behaviour decided by comparing MolangJS, bridge's molang, bedrockk/MoLang,
Moonflower's molang-compiler and molang-py:
- "v.x = c ? 5 : 6" assigned c and then evaluated "(v.x = c) ? 5 : 6".
Assignment now takes everything on its right (4 of 5 libraries), and
"v.a = v.b = 1" assigns both.
- "a ? 1 : b ? 2 : 3" was grouped as "(a ? 1 : b) ? 2 : 3". Conditionals
now group from the right (4 of 5).
- An empty statement (";;"), a missing operand ("1 +") or a trailing
comma in a call ("math.abs(1,)") made parseSingle return null, which
silently dropped the rest of the script or put null into the
arguments. They are now parse errors (3 to 4 of 5). "{}" and a
semicolon before "}" still parse.
toString() dropped every grouping parenthesis, so "(1 + 2) * 3" printed
as "1+2*3". It now prints parentheses wherever the parser would group
differently without them, and the true branch of a conditional is
wrapped when it ends with a "?" that has no ":". A test parses 20,000
random expressions, prints them and checks that they parse back into
the same tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
The library comparison made ";;" a parse error (3 of 5 libraries), but
Bedrock itself accepts "v.a = 1;; v.b = 2; return v.b;" and skips the
empty statement, as Mocha 3 and MolangJS do. Empty statements, at the
top level and in blocks, are now skipped instead of rejected.
Bedrock does reject a trailing comma ("math.abs(-1,)") and a missing
operand ("1 +"), so those stay parse errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
…erators ProtocolUtils.writeString writes an empty string and null the same way, and readString returns null for both, so an expression containing '' was written fine but threw a NullPointerException when read back. A string literal is never null, so StringExpression now reads it as "". readString itself is unchanged, because callers rely on it returning null. readEnum turned an unknown ordinal into the first constant, so corrupt data or a newer peer silently changed an operator into AND, LOGICAL_NEGATION or BREAK. It now throws. StatementExpression (break, continue) now implements equals and hashCode like the other expressions, so equal trees compare equal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
…n't compile Temp variables: an assignment compiled its value with the method's return type and stored it with fstore, so t.* failed verification in functions returning double, int or boolean. The value is now always a float, and an assignment keeps it on the stack, so a script ending with one compiles. Booleans: float, double and long values were converted to boolean by truncating them to an int first, so 0.5 was false and "a && 1", "a ? 10 : 20" and "!a" gave the wrong result for fractional values. They are now compared with zero. Arithmetic results and field reads are converted to the type the surrounding code expects instead of always being left as floats. Numbers: like the interpreter, dividing by zero gives zero, and NaN or infinite results of arithmetic and of Java methods (unless the binding sets skipChecking) become zero. This costs about 0.07 ns per call of the benchmark function. Unsupported constructs: variable/v, properties of ObjectValues such as a query made of lambdas, non-static fields, ->, ??, array access, blocks, break, continue, loop, for_each, lambda functions and methods taking an ExecutionContext, a Lazy or varargs used to emit invalid bytecode or, for loop and for_each, silently compile to 0. They now throw an UnsupportedOperationException that names the construct. loop and for_each are no longer folded as constants, which is what made them disappear. "a ? b" without an else branch is now supported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
The readme and the docs folder still described Mocha 3: Java 8+, Javassist, team.unnamed:mocha on Maven Central, Mocha's build badge and Discord, and an API where MochaEngine evaluates and compiles by itself. They now describe the modules, the Redlance repository, Java 24 and the interpreter/compiler API, and link to the full documentation. gradle.properties set group=team.unnamed; it's now org.redlance.mochafloats, which the publications already used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
…hing The only workflow ran on workflow_dispatch and ran "./gradlew publish", which doesn't depend on test: pushes and pull requests were never built, and a release could be published with failing tests. - build.yml now runs "./gradlew build" on pushes to main and on pull requests. It doesn't use any secret, so it also works for pull requests from forks. - publish.yml keeps the manual trigger, the secrets and the steps of the old workflow, and runs "./gradlew build publish". The test task no longer runs generateExpectations, so the tests don't need Node.js and use the committed expectations.txt. build.yml regenerates it with the Node.js the runner already has and fails if it differs, so a tests.txt change can't be merged without regenerating it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011GF7DmQcK3Qv6knGWZRJdy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The documentation now lives only in PlayerAnimationLibrary/Documentation, published at docs.zigythebird.com/mochafloats, which the readme links to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
math.*and@Bindingcalls went through a method-handle spreader built on every call; an expression withmath.sinandmath.costakes 135 ns instead of 304 ns (Mocha 3.0.1: 249 ns).=and? :, blocks in conditionals,??, string comparison,math.round, the random functions and empty statements.0.toString()and@BindExternalFunction; CI now builds and tests every push tomainand every pull request.Every fix has regression tests; 88 tests pass.
Behaviour changes
These change what existing MoLang evaluates to. Each one was decided by Bedrock, tested in the game with a behaviour pack whose animation controllers report each result, or, where Bedrock wasn't tested, by the majority of MolangJS 1.7.1, bridge's
molang2.0.1, bedrockk/MoLang, Moonflower's molang-compiler 3.1.1.19 and molang-py 1.0.1.v.x = 1 ? 5 : 6; return v.x;1551 ? 2 : 3 ? 4 : 5422v.w = 0; 1 ? { v.w = 4; } : { v.w = 5; }; return v.w;044v.zero = 0; return v.zero ?? 7;700'a' == 'b'100math.round(-2.5)-2-3-3math.random_integer(1, 3)math.random(1, 1),math.random(5, 1)1, 1 to 51, 1 to 5math.die_roll(1, 5, 6)v.a = 1;; v.b = 2; return v.b;1, the rest is dropped22math.abs(-1,),1 +1,0, no errorKept on purpose:
math.sqrt(-1)andmath.ln(0)still give0(Bedrock givesNaNand-Infinity), because aNaNthat reaches a bone breaks the model.1 / 0is0, as in Bedrock.Interpreter
ReflectiveFunctioncalledMethodHandle.invokeWithArguments, which builds a spreader on every call. The handle is now adapted once and called withinvokeExact.MochaMathregistered a function for every binding, but didn't overridesetorentries(), so nothing was stored and everymath.*call went through reflection. The functions now back the bindings, with the same purity;piandestay inlineable constants.warnOnReflectiveFunctionUsageprinted a line on every call, even for calls that don't use reflection. It now reports each reflectively called method once. PlayerAnimationLibrary turns this option on.ExecutionContext,Lazyand varargs parameters of Java bindings receivednull, and primitive varargs threw aClassCastException.@BindExternalFunctionbound nothing, because only the repeated form was read.Functionreturningnullmadeq.f() + 1throw aNullPointerException.Parser
parseSinglereturnnull, which dropped the rest of the script or putnullinto the arguments.toString()dropped grouping parentheses, so(1 + 2) * 3printed as1+2*3. It now prints text that parses back into the same tree, checked on 20,000 random expressions.Compiler
t.*assignments failed verification in functions returningdouble,intorboolean.booleanby truncating them, so0.5was false ina && 1,a ? 10 : 20and!a.NaNor infinite results now give0, like in the interpreter.variable, properties ofObjectValues,->,??, array access, blocks,break,continue,loop,for_each, lambda functions andExecutionContext/Lazy/varargs parameters now throw anUnsupportedOperationExceptionthat names the construct. Before, they generated invalid bytecode, andloop/for_eachsilently compiled to0.a ? bwithout:is now supported.Network format
''couldn't be read back:ProtocolUtils.readStringreturnsnullfor an empty string.StatementExpression(break,continue) now implementsequalsandhashCode.Build, CI and docs
build.ymlruns./gradlew buildon pushes tomainand on pull requests, without secrets. The manual workflow is nowpublish.ymland runs./gradlew build publish, so a release can't be published with failing tests.testtask no longer needs Node.js; CI checks thatexpectations.txtmatchestests.txt.groupingradle.propertiesdescribe mochafloats instead of Mocha 3.docs/is removed: the documentation now lives only in PlayerAnimationLibrary/Documentation, published at docs.zigythebird.com/mochafloats.Performance
JMH 1.37, JDK 25, Apple Silicon, average time per call:
5math.sin(v.t * 50) * 45 + math.cos(v.t * 20)@Bindinginstance method callCompiled functions stay within about 10% of Mocha's and Moonflower's molang-compiler; the new division and
NaNchecks cost 0.05 to 0.6 ns per call.🤖 Generated with Claude Code