From 9e54789b12172e254dd104bb695e1f7d39529734 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 11 Oct 2026 11:51:36 +0200 Subject: [PATCH 1/3] Lua: a real modulo is fmod again, never the float % Measured on the 3.0.0 client, Lua's float % differs from fmod with the ModuloReal correction for a negative dividend, and the fmod form is the one which agrees with Jass (-0.1 % 7: 6.900000064 against 6.899999616). The integer % by a positive literal agrees with Jass and stays. --- AGENTS.md | 2 +- .../providers/LuaEnsureTypeProvider.java | 9 ---- .../imtranslation/ImTranslator.java | 6 +-- .../imtranslation/LuaNativeLowering.java | 44 +++++++++++-------- .../lua/translation/ExprTranslation.java | 3 +- .../tests/LuaBackendAuditTests.java | 21 +++++---- docs/WC3_RUNTIME.md | 1 + 7 files changed, 43 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd66114fc..e9503cd6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -303,7 +303,7 @@ Rules for backend work: ### Reference semantics for arithmetic * Integer/real division and modulo semantics are centralized: `WurstOperator.moduloInteger/moduloReal` implement the Blizzard.j formula (truncated remainder, plus divisor if negative) and Jass `div` truncates toward zero. -* The Lua helpers (`__wurst_intDiv`, `__wurst_modInt`, `__wurst_modReal`, built in `LuaNativeLowering`), the interpreter's `MathProvider` mocks, and constant folding (`SimpleRewrites`) must all stay consistent with those helpers — never reimplement div/mod locally. +* The Lua helpers (`__wurst_intDiv`, `__wurst_modInt`, `__wurst_modReal`, built in `LuaNativeLowering`), the interpreter's `MathProvider` mocks, and constant folding (`SimpleRewrites`) must all stay consistent with those helpers — never reimplement div/mod locally. A real modulo on Lua is `math.fmod` with the sign correction, never Lua's float `%`, which in the game differs from it and from Jass for a negative dividend ([`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). * Jass `==` on reals allows a difference of 0.001 (`WurstOperator.jassRealEquals`), while `!=` and the orderings are exact, so on the Jass target `not (a == b)` is not `a != b` for reals. Lua compares exactly, by design: the tolerance would cost every real comparison. Both interpreters use that helper; the optimiser must not swap one for the other or fold nearly equal reals on Jass. * The game reads Jass real literals inexactly and computes Lua reals with a 24-bit mantissa (measurements: [`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). So on both targets real folding (`SimpleRewrites.foldRealExactly`) reads and writes only exact literals and folds an operation only when its exact result is one. Never fold in double or float, and never treat one `R2I` result as proof of a precision. The test runtime's Lua computes in double, so a test that runs the emitted Lua does not show the game's rounding. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java index 17765ad76..3494f39fc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java @@ -66,13 +66,4 @@ public ILconstInt __wurst_rawFloorModInt(ILconstInt a, ILconstInt b) { public ILconstReal __wurst_rawFmodReal(ILconstReal a, ILconstReal b) { return ILconstReal.create(a.getVal() % b.getVal()); } - - /** Lua 5.3's float {@code %}: fmod, plus the divisor when {@code fmod * b < 0}. */ - public ILconstReal __wurst_rawFloorModReal(ILconstReal a, ILconstReal b) { - float m = a.getVal() % b.getVal(); - if (m * b.getVal() < 0) { - m += b.getVal(); - } - return ILconstReal.create(m); - } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index e5f548366..df92a8f94 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -202,7 +202,6 @@ public T canonical(T copy) { @Nullable public ImFunction luaRawFmodIntFunc = null; @Nullable public ImFunction luaRawFmodRealFunc = null; @Nullable public ImFunction luaRawFloorModIntFunc = null; - @Nullable public ImFunction luaRawFloorModRealFunc = null; @Nullable public ImFunction luaRawConcatFunc = null; /** {@code x or ""}: a string operand which may be nil, made safe for {@link #luaRawConcatFunc}. */ @Nullable public ImFunction luaRawOrEmptyFunc = null; @@ -247,8 +246,7 @@ public boolean isTrapFreeLuaIntrinsicCall(ImFunctionCall call) { || (divisor instanceof ImRealVal realVal && Double.parseDouble(realVal.getValR()) != 0.0); return nonZeroDivisor && (target == luaRawFloorDivIntFunc || target == luaRawFmodIntFunc - || target == luaRawFloorModIntFunc || target == luaRawFmodRealFunc - || target == luaRawFloorModRealFunc); + || target == luaRawFloorModIntFunc || target == luaRawFmodRealFunc); } /** * Whether {@code f} is one of the Lua stubs which only read a table: a keyed-map read, or a hashtable @@ -273,7 +271,7 @@ public List pinnedFunctions() { for (ImFunction f : new ImFunction[]{ ensureIntFunc, ensureBoolFunc, ensureRealFunc, ensureStrFunc, stringConcatFunc, luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFmodRealFunc, luaRawFloorModIntFunc, - luaRawFloorModRealFunc, luaRawConcatFunc, + luaRawConcatFunc, luaRawOrEmptyFunc, luaRawR2IFunc, luaRawToNumberIntFunc, luaRawToNumberRealFunc, luaRawToIntegerFunc, luaRawToStringFunc, luaIntDivFunc, luaModIntFunc, luaModRealFunc, debugPrintFunction, errorFunc, genericNewMarker, globalInitFunc}) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index b60747f0e..723f4f492 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -596,7 +596,6 @@ private static final class DivModFunctions { private ImFunction rawFmodInt; private ImFunction rawFloorModInt; private ImFunction rawFmodReal; - private ImFunction rawFloorModReal; private ImFunction intDiv; private ImFunction modInt; private ImFunction modReal; @@ -630,15 +629,14 @@ ImFunction modInt() { } /** - * Lua 5.3 computes a float % as fmod plus the divisor when {@code fmod * b < 0}, which for a - * positive b is the ModuloReal correction (-0.0 stays -0.0 on both), except where that product - * underflows to zero: a subnormal remainder and a divisor below 1. So the operator is taken - * from b >= 1 on, and never for NaN. + * Always fmod and the ModuloReal correction, never Lua's float %: in the game the two differ for a + * negative dividend, and fmod is the one which agrees with Jass (measured on the 3.0.0 client: -0.1 % 7 + * gives 6.900000064 with % and 6.899999616 with fmod, as Jass; -0.0000001 % 1 gives 0.999999872 with % + * and 1 with fmod, as Jass). The test Lua computes in double, where the two agree. */ ImFunction modReal() { if (modReal == null) { - modReal = buildMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), - WurstOperator.GREATER_EQ, JassIm.ImRealVal("1."), rawFmodReal(), rawFloorModReal()); + modReal = buildFmodMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), rawFmodReal()); translator.luaModRealFunc = modReal; created.add(modReal); } @@ -686,16 +684,6 @@ private ImFunction rawFmodReal() { return rawFmodReal; } - /** Lua's float {@code %}; only correct for a divisor of at least 1, which the caller guarantees. */ - private ImFunction rawFloorModReal() { - if (rawFloorModReal == null) { - rawFloorModReal = rawNative("__wurst_rawFloorModReal", TypesHelper.imReal()); - translator.luaRawFloorModRealFunc = rawFloorModReal; - created.add(rawFloorModReal); - } - return rawFloorModReal; - } - /** A native leaf with two params and a return, translated as a Lua backend intrinsic. */ private static ImFunction rawNative(String name, ImType numType) { ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false); @@ -738,7 +726,7 @@ private static ImFunction buildIntDiv(ImFunction rawFloorDiv) { /** * local r - * if b [> 0 | >= 1.] then r = rawFloorMod(a, b) + * if b > 0 then r = rawFloorMod(a, b) * else r = rawFmod(a, b); if r < 0 then r = r + b end end * return r * (Wurst mod follows Blizzard.j's ModuloInteger/ModuloReal: truncated remainder, plus the @@ -773,6 +761,26 @@ private static ImFunction buildMod(String name, ImType numType, ImExpr zeroLiter JassIm.ImVars(r), body, Collections.emptyList()); } + /** local r = rawFmod(a, b); if r < 0 then r = r + b end; return r */ + private static ImFunction buildFmodMod(String name, ImType numType, ImExpr zeroLiteral, ImFunction rawFmod) { + ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false); + ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "b", false); + ImVar r = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "r", false); + + ImStmts body = JassIm.ImStmts( + JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), call(rawFmod, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b))), + JassIm.ImIf(SYNTHETIC_TRACE, + JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(r), zeroLiteral)), + JassIm.ImStmts(JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), + JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImVarAccess(r), JassIm.ImVarAccess(b))))), + JassIm.ImStmts() + ), + JassIm.ImReturn(SYNTHETIC_TRACE, JassIm.ImVarAccess(r)) + ); + return JassIm.ImFunction(SYNTHETIC_TRACE, name, JassIm.ImTypeVars(), JassIm.ImVars(a, b), numType.copy(), + JassIm.ImVars(r), body, Collections.emptyList()); + } + private static ImFunctionCall call(ImFunction f, ImExpr... args) { return JassIm.ImFunctionCall(SYNTHETIC_TRACE, f, JassIm.ImTypeArguments(), JassIm.ImExprs(args), false, CallType.NORMAL); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 93274b74f..107a22081 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -168,7 +168,7 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { if (e.getFunc() == tr.imTr.luaRawFloorDivIntFunc) { return LuaAst.LuaExprBinary(left, LuaAst.LuaOpFloorDiv(), right); } - if (e.getFunc() == tr.imTr.luaRawFloorModIntFunc || e.getFunc() == tr.imTr.luaRawFloorModRealFunc) { + if (e.getFunc() == tr.imTr.luaRawFloorModIntFunc) { return LuaAst.LuaExprBinary(left, LuaAst.LuaOpMod(), right); } if (e.getFunc() == tr.imTr.luaRawConcatFunc) { @@ -218,7 +218,6 @@ static boolean isBackendIntrinsic(ImFunction function, LuaTranslator tr) { || function == tr.imTr.luaRawFmodIntFunc || function == tr.imTr.luaRawFmodRealFunc || function == tr.imTr.luaRawFloorModIntFunc - || function == tr.imTr.luaRawFloorModRealFunc || function == tr.imTr.luaRawConcatFunc || function == tr.imTr.luaRawOrEmptyFunc || LuaKeyedMap.readStubName(tr.imTr, function) != null diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index ee7d3f05b..2c72d1544 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -5841,9 +5841,10 @@ public void optimizedMovedImHelpersHaveNoDanglingReferences() { assertTrue("repro must exercise string concat lowering", compiled.contains(" .. ")); assertFalse("the raw concat primitive is an operator, not a call", compiled.contains("__wurst_rawConcat")); assertTrue("repro must exercise integer div lowering", compiled.contains(" // ")); - // the divisors are literals once inlined, so both lower to Lua's % (see modByADivisorThatBecomesALiteral...) + // the int divisor is a literal once inlined, so it lowers to Lua's %; a real modulo is always fmod + // (see modByADivisorThatBecomesALiteral...) assertTrue("repro must exercise integer and real mod lowering:\n" + compiled, - compiled.contains(" % 2)") && compiled.contains(" % 2.)")); + compiled.contains(" % 2)") && compiled.contains("math.fmod(")); assertFalse("raw numeric primitive calls must not survive Lua emission", compiled.contains("__wurst_rawF")); } @@ -7502,10 +7503,11 @@ public void aLongStringConcatenationLoadsAndRuns() throws IOException { } /** - * A divisor which only becomes a literal once the helper is inlined (here a parameter, in a map + * An int divisor which only becomes a literal once the helper is inlined (here a parameter, in a map * often a constant) is Lua's %, with no fmod and no sign correction: the helper tests the divisor - * and the test folds. A real divisor below 1 keeps fmod, and so does a runtime divisor, whose - * test runs. + * and the test folds; a runtime divisor keeps the test. A real modulo is always fmod, whatever the + * divisor: in the game Lua's float % differs from fmod and from Jass for a negative dividend (measured + * on the 3.0.0 client, -0.1 % 7 gives 6.900000064 with % and 6.899999616 with fmod and in Jass). */ @Test public void modByADivisorThatBecomesALiteralIsTheLuaOperator() { @@ -7534,8 +7536,9 @@ public void modByADivisorThatBecomesALiteralIsTheLuaOperator() { assertTrue(fixed, fixed.contains(" % 1000003)")); assertFalse(fixed, fixed.contains("fmod") || fixed.contains("< 0")); String fixedReal = topLevelFunctionBodyWithPrefix(compiled, "fixedReal"); - assertTrue(fixedReal, fixedReal.contains(" % 360.)")); - assertFalse(fixedReal, fixedReal.contains("fmod") || fixedReal.contains("< 0")); + assertTrue("a real modulo is fmod, also by a literal of at least 1:\n" + fixedReal, + fixedReal.contains("math.fmod(")); + assertFalse(fixedReal, fixedReal.contains(" % ")); String belowOne = topLevelFunctionBodyWithPrefix(compiled, "belowOne"); assertTrue("a real divisor below 1 keeps fmod:\n" + belowOne, belowOne.contains("math.fmod(")); assertFalse(belowOne, belowOne.contains(" % ")); @@ -7667,7 +7670,7 @@ public void oldGenericHandleReadsNeedNoIntNormalisation() throws IOException { assertFalse(init, init.contains("math.tointeger") || init.contains("tonumber") || init.contains("__wurst_ensureInt")); } - /** -0.0 stays -0.0 under Lua's % as under fmod and its correction: 1 / r is minus infinity. */ + /** -0.0 stays -0.0 under fmod and its correction, which a real modulo always is: 1 / r is minus infinity. */ @Test public void modKeepsTheSignOfANegativeZeroOnLua() throws IOException { test().testLua(true).inline().localOptimizations().executeProg().lines( @@ -7684,6 +7687,6 @@ public void modKeepsTheSignOfANegativeZeroOnLua() throws IOException { " testSuccess()"); String compiled = topLevelFunctionBodyWithPrefix(compiledLua("modKeepsTheSignOfANegativeZeroOnLua"), "init_Test"); - assertTrue(compiled, compiled.contains(" % 360.)")); + assertTrue(compiled, compiled.contains("math.fmod(") && !compiled.contains(" % ")); } } diff --git a/docs/WC3_RUNTIME.md b/docs/WC3_RUNTIME.md index 2f52569ab..5ddffc7a4 100644 --- a/docs/WC3_RUNTIME.md +++ b/docs/WC3_RUNTIME.md @@ -45,6 +45,7 @@ Every client simulates the same game from the same inputs; anything that makes t * **measured, 3.0.0** Jass literals and `S2R` are not correctly rounded and the two disagree: `0.1` reads one float high, `1.1` one float low, `S2R("100.0")` one above 100. Short exact binary fractions (`0.5`, `0.75`, `100.0`, `123456.78125`) read exactly; integer parts past 2^24 are truncated. This is why `SimpleRewrites.foldRealExactly` folds only exact literals and exact results. * **measured, 3.0.0** Lua reals have a 24-bit mantissa, not 53: `(2^24 + 1) - 2^24` is 0, the literal `16777217.` reads as 16777216, `0.9999999999999999` reads as exactly 1, and `0.7 + 0.1 + 0.1 + 0.1` computes to 1 - 2^-23. Literals are rounded to nearest and each arithmetic result is truncated, which neither double nor 32-bit float arithmetic reproduces. One `R2I` result is not evidence of a precision: `R2I(0.7 + 0.1 + 0.1 + 0.1)` is 0 for that reason, not because of doubles. * **measured, 3.0.0** `R2I` and the arithmetic `x // 1 | 0` agree on every value probed, the 32-bit edges and values outside the range included (`LuaNativeLowering.lowerRealToInt`). +* **measured, 3.0.0** Lua's float `%` and `math.fmod` with the sign correction agree for a non-negative dividend, but not for a negative one, and there only the fmod form agrees with Jass: `-0.1 % 7.` gives 6.900000064 with `%` and 6.899999616 with fmod, as Jass; `-0.0000001 % 1.` gives 0.999999872 and 1, Jass 1. Both forms differ from Jass where the game's Lua arithmetic does: `360. % 7.` is 3.0000153 and `7.5 % 360.` is 7.4999995 in Lua either way, 3 and 7.5 in Jass. The integer `%` agrees with Jass's `mod` for a positive divisor on every value probed, the 32-bit edges included. So a real modulo is `math.fmod` with the sign correction, and only an integer one by a positive literal is `%` (`LuaNativeLowering`). ## Establishing a fact From 4a3ed1621da0b576750e79629b29618caee1640c Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 11 Oct 2026 12:07:16 +0200 Subject: [PATCH 2/3] Lua: a real modulo is Blizzard.j's ModuloReal formula, as on Jass Measured on the 3.0.0 client, neither math.fmod nor Lua's float % gives Jass's real modulo in the game (360 % 7: 3.0000153), while a - R2I(a / b) * b, plus b when negative, computed in the game's Lua matches Jass on every probed operand both read alike. --- AGENTS.md | 2 +- .../providers/LuaEnsureTypeProvider.java | 4 - .../imtranslation/ImTranslator.java | 5 +- .../imtranslation/LuaNativeLowering.java | 77 +++++++++++-------- .../lua/translation/ExprTranslation.java | 1 - .../tests/LuaBackendAuditTests.java | 27 +++---- docs/WC3_RUNTIME.md | 2 +- 7 files changed, 63 insertions(+), 55 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e9503cd6c..c261d842a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -303,7 +303,7 @@ Rules for backend work: ### Reference semantics for arithmetic * Integer/real division and modulo semantics are centralized: `WurstOperator.moduloInteger/moduloReal` implement the Blizzard.j formula (truncated remainder, plus divisor if negative) and Jass `div` truncates toward zero. -* The Lua helpers (`__wurst_intDiv`, `__wurst_modInt`, `__wurst_modReal`, built in `LuaNativeLowering`), the interpreter's `MathProvider` mocks, and constant folding (`SimpleRewrites`) must all stay consistent with those helpers — never reimplement div/mod locally. A real modulo on Lua is `math.fmod` with the sign correction, never Lua's float `%`, which in the game differs from it and from Jass for a negative dividend ([`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). +* The Lua helpers (`__wurst_intDiv`, `__wurst_modInt`, `__wurst_modReal`, built in `LuaNativeLowering`), the interpreter's `MathProvider` mocks, and constant folding (`SimpleRewrites`) must all stay consistent with those helpers — never reimplement div/mod locally. A real modulo on Lua is Blizzard.j's `ModuloReal` formula, as on Jass, never `math.fmod` or Lua's float `%`, which differ from Jass in the game ([`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). * Jass `==` on reals allows a difference of 0.001 (`WurstOperator.jassRealEquals`), while `!=` and the orderings are exact, so on the Jass target `not (a == b)` is not `a != b` for reals. Lua compares exactly, by design: the tolerance would cost every real comparison. Both interpreters use that helper; the optimiser must not swap one for the other or fold nearly equal reals on Jass. * The game reads Jass real literals inexactly and computes Lua reals with a 24-bit mantissa (measurements: [`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). So on both targets real folding (`SimpleRewrites.foldRealExactly`) reads and writes only exact literals and folds an operation only when its exact result is one. Never fold in double or float, and never treat one `R2I` result as proof of a precision. The test runtime's Lua computes in double, so a test that runs the emitted Lua does not show the game's rounding. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java index 3494f39fc..d3466d70a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java @@ -62,8 +62,4 @@ public ILconstInt __wurst_rawFmodInt(ILconstInt a, ILconstInt b) { public ILconstInt __wurst_rawFloorModInt(ILconstInt a, ILconstInt b) { return ILconstInt.create(Math.floorMod(a.getVal(), b.getVal())); } - - public ILconstReal __wurst_rawFmodReal(ILconstReal a, ILconstReal b) { - return ILconstReal.create(a.getVal() % b.getVal()); - } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index df92a8f94..47c6b7380 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -200,7 +200,6 @@ public T canonical(T copy) { // Exact synthetic nodes owned by LuaNativeLowering; backend intrinsic recognition must use identity. @Nullable public ImFunction luaRawFloorDivIntFunc = null; @Nullable public ImFunction luaRawFmodIntFunc = null; - @Nullable public ImFunction luaRawFmodRealFunc = null; @Nullable public ImFunction luaRawFloorModIntFunc = null; @Nullable public ImFunction luaRawConcatFunc = null; /** {@code x or ""}: a string operand which may be nil, made safe for {@link #luaRawConcatFunc}. */ @@ -246,7 +245,7 @@ public boolean isTrapFreeLuaIntrinsicCall(ImFunctionCall call) { || (divisor instanceof ImRealVal realVal && Double.parseDouble(realVal.getValR()) != 0.0); return nonZeroDivisor && (target == luaRawFloorDivIntFunc || target == luaRawFmodIntFunc - || target == luaRawFloorModIntFunc || target == luaRawFmodRealFunc); + || target == luaRawFloorModIntFunc); } /** * Whether {@code f} is one of the Lua stubs which only read a table: a keyed-map read, or a hashtable @@ -270,7 +269,7 @@ public List pinnedFunctions() { List result = new ArrayList<>(); for (ImFunction f : new ImFunction[]{ ensureIntFunc, ensureBoolFunc, ensureRealFunc, ensureStrFunc, stringConcatFunc, - luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFmodRealFunc, luaRawFloorModIntFunc, + luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFloorModIntFunc, luaRawConcatFunc, luaRawOrEmptyFunc, luaRawR2IFunc, luaRawToNumberIntFunc, luaRawToNumberRealFunc, luaRawToIntegerFunc, luaRawToStringFunc, luaIntDivFunc, luaModIntFunc, luaModRealFunc, diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index 723f4f492..d13eaf838 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -498,6 +498,7 @@ public void visit(ImOperatorCall call) { */ private static void lowerRealToInt(ImProg prog, ImTranslator translator) { ImFunction[] shim = {null}; + List added = new ArrayList<>(); prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImFunctionCall call) { @@ -507,27 +508,38 @@ public void visit(ImFunctionCall call) { return; } if (shim[0] == null) { - shim[0] = buildRealToInt(translator); + shim[0] = buildRealToInt(translator, added); } call.replaceBy(JassIm.ImFunctionCall(call.attrTrace(), shim[0], JassIm.ImTypeArguments(), JassIm.ImExprs(call.getArguments().removeAll()), false, CallType.NORMAL)); } }); if (shim[0] != null) { - prog.getFunctions().add(translator.luaRawR2IFunc); + prog.getFunctions().addAll(added); prog.getFunctions().add(shim[0]); } } + /** + * The R2I intrinsic, printed as Lua arithmetic by ExprTranslation#realToInt: made once per program, and added to + * {@code addTo} by whichever asks first. The R2I lowering and the real modulo share it. + */ + private static ImFunction rawRealToInt(ImTranslator translator, List addTo) { + if (translator.luaRawR2IFunc == null) { + ImVar arg = JassIm.ImVar(SYNTHETIC_TRACE, TypesHelper.imReal(), "x", false); + translator.luaRawR2IFunc = JassIm.ImFunction(SYNTHETIC_TRACE, "__wurst_rawR2I", JassIm.ImTypeVars(), + JassIm.ImVars(arg), TypesHelper.imInt(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.singletonList(FunctionFlagEnum.IS_NATIVE)); + addTo.add(translator.luaRawR2IFunc); + } + return translator.luaRawR2IFunc; + } + /** return rawR2I(x): one intrinsic call, printed as Lua arithmetic by ExprTranslation#realToInt. */ - private static ImFunction buildRealToInt(ImTranslator translator) { + private static ImFunction buildRealToInt(ImTranslator translator, List addTo) { ImType realType = TypesHelper.imReal(); ImType intType = TypesHelper.imInt(); - ImVar arg = JassIm.ImVar(SYNTHETIC_TRACE, realType.copy(), "x", false); - ImFunction rawR2I = JassIm.ImFunction(SYNTHETIC_TRACE, "__wurst_rawR2I", JassIm.ImTypeVars(), - JassIm.ImVars(arg), intType.copy(), JassIm.ImVars(), JassIm.ImStmts(), - Collections.singletonList(FunctionFlagEnum.IS_NATIVE)); - translator.luaRawR2IFunc = rawR2I; + ImFunction rawR2I = rawRealToInt(translator, addTo); ImVar x = JassIm.ImVar(SYNTHETIC_TRACE, realType.copy(), "x", false); ImStmts body = JassIm.ImStmts(JassIm.ImReturn(SYNTHETIC_TRACE, JassIm.ImFunctionCall(SYNTHETIC_TRACE, rawR2I, @@ -595,7 +607,6 @@ private static final class DivModFunctions { private ImFunction rawFloorDivInt; private ImFunction rawFmodInt; private ImFunction rawFloorModInt; - private ImFunction rawFmodReal; private ImFunction intDiv; private ImFunction modInt; private ImFunction modReal; @@ -629,14 +640,15 @@ ImFunction modInt() { } /** - * Always fmod and the ModuloReal correction, never Lua's float %: in the game the two differ for a - * negative dividend, and fmod is the one which agrees with Jass (measured on the 3.0.0 client: -0.1 % 7 - * gives 6.900000064 with % and 6.899999616 with fmod, as Jass; -0.0000001 % 1 gives 0.999999872 with % - * and 1 with fmod, as Jass). The test Lua computes in double, where the two agree. + * Blizzard.j's ModuloReal, which a real modulo calls on Jass: {@code a - R2I(a / b) * b}, plus b when that + * is negative, with R2I as the game's Lua computes it. Measured on the 3.0.0 client, that gives Jass's + * result in the game's Lua for every operand both read alike, where math.fmod and Lua's float % do not: + * 360 % 7 is 3 (fmod and % give 3.0000153), 7.5 % 360 is 7.5 (both give 7.4999995), -7.5 % 2.5 is 0 + * (fmod gives 2.4999993). The test Lua computes in double, where all three agree. */ ImFunction modReal() { if (modReal == null) { - modReal = buildFmodMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), rawFmodReal()); + modReal = buildBlizzardModReal(rawRealToInt(translator, created)); translator.luaModRealFunc = modReal; created.add(modReal); } @@ -675,15 +687,6 @@ ImFunction rawFloorModInt() { return rawFloorModInt; } - private ImFunction rawFmodReal() { - if (rawFmodReal == null) { - rawFmodReal = rawNative("__wurst_rawFmodReal", TypesHelper.imReal()); - translator.luaRawFmodRealFunc = rawFmodReal; - created.add(rawFmodReal); - } - return rawFmodReal; - } - /** A native leaf with two params and a return, translated as a Lua backend intrinsic. */ private static ImFunction rawNative(String name, ImType numType) { ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false); @@ -761,24 +764,34 @@ private static ImFunction buildMod(String name, ImType numType, ImExpr zeroLiter JassIm.ImVars(r), body, Collections.emptyList()); } - /** local r = rawFmod(a, b); if r < 0 then r = r + b end; return r */ - private static ImFunction buildFmodMod(String name, ImType numType, ImExpr zeroLiteral, ImFunction rawFmod) { - ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false); - ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "b", false); - ImVar r = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "r", false); + /** + * local q = a / b; local r = a - rawR2I(q) * b; if r < 0 then r = r + b end; return r + * (Blizzard.j's ModuloReal. The quotient is a local because the intrinsic is printed in place only for a + * variable; the integer it gives converts to a real in the multiplication, as I2R does.) + */ + private static ImFunction buildBlizzardModReal(ImFunction rawR2I) { + ImType realType = TypesHelper.imReal(); + ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, realType.copy(), "a", false); + ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, realType.copy(), "b", false); + ImVar q = JassIm.ImVar(SYNTHETIC_TRACE, realType.copy(), "q", false); + ImVar r = JassIm.ImVar(SYNTHETIC_TRACE, realType.copy(), "r", false); ImStmts body = JassIm.ImStmts( - JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), call(rawFmod, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b))), + JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(q), + JassIm.ImOperatorCall(WurstOperator.DIV_REAL, JassIm.ImExprs(JassIm.ImVarAccess(a), JassIm.ImVarAccess(b)))), + JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), JassIm.ImOperatorCall(WurstOperator.MINUS, + JassIm.ImExprs(JassIm.ImVarAccess(a), JassIm.ImOperatorCall(WurstOperator.MULT, + JassIm.ImExprs(call(rawR2I, JassIm.ImVarAccess(q)), JassIm.ImVarAccess(b)))))), JassIm.ImIf(SYNTHETIC_TRACE, - JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(r), zeroLiteral)), + JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(r), JassIm.ImRealVal("0."))), JassIm.ImStmts(JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImVarAccess(r), JassIm.ImVarAccess(b))))), JassIm.ImStmts() ), JassIm.ImReturn(SYNTHETIC_TRACE, JassIm.ImVarAccess(r)) ); - return JassIm.ImFunction(SYNTHETIC_TRACE, name, JassIm.ImTypeVars(), JassIm.ImVars(a, b), numType.copy(), - JassIm.ImVars(r), body, Collections.emptyList()); + return JassIm.ImFunction(SYNTHETIC_TRACE, "__wurst_modReal", JassIm.ImTypeVars(), JassIm.ImVars(a, b), + realType.copy(), JassIm.ImVars(q, r), body, Collections.emptyList()); } private static ImFunctionCall call(ImFunction f, ImExpr... args) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 107a22081..e2b2caca1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -216,7 +216,6 @@ static boolean isBackendIntrinsic(ImFunction function, LuaTranslator tr) { return unaryIntrinsicName(function, tr) != null || function == tr.imTr.luaRawFloorDivIntFunc || function == tr.imTr.luaRawFmodIntFunc - || function == tr.imTr.luaRawFmodRealFunc || function == tr.imTr.luaRawFloorModIntFunc || function == tr.imTr.luaRawConcatFunc || function == tr.imTr.luaRawOrEmptyFunc diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index 2c72d1544..b53aa2bbc 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -5841,10 +5841,10 @@ public void optimizedMovedImHelpersHaveNoDanglingReferences() { assertTrue("repro must exercise string concat lowering", compiled.contains(" .. ")); assertFalse("the raw concat primitive is an operator, not a call", compiled.contains("__wurst_rawConcat")); assertTrue("repro must exercise integer div lowering", compiled.contains(" // ")); - // the int divisor is a literal once inlined, so it lowers to Lua's %; a real modulo is always fmod - // (see modByADivisorThatBecomesALiteral...) + // the int divisor is a literal once inlined, so it lowers to Lua's %; a real modulo is always Blizzard.j's + // formula (see modByADivisorThatBecomesALiteral...) assertTrue("repro must exercise integer and real mod lowering:\n" + compiled, - compiled.contains(" % 2)") && compiled.contains("math.fmod(")); + compiled.contains(" % 2)") && compiled.contains(" * 2.))") && !compiled.contains("math.fmod(")); assertFalse("raw numeric primitive calls must not survive Lua emission", compiled.contains("__wurst_rawF")); } @@ -7505,9 +7505,9 @@ public void aLongStringConcatenationLoadsAndRuns() throws IOException { /** * An int divisor which only becomes a literal once the helper is inlined (here a parameter, in a map * often a constant) is Lua's %, with no fmod and no sign correction: the helper tests the divisor - * and the test folds; a runtime divisor keeps the test. A real modulo is always fmod, whatever the - * divisor: in the game Lua's float % differs from fmod and from Jass for a negative dividend (measured - * on the 3.0.0 client, -0.1 % 7 gives 6.900000064 with % and 6.899999616 with fmod and in Jass). + * and the test folds; a runtime divisor keeps the test. A real modulo is always Blizzard.j's ModuloReal, + * whatever the divisor, as on Jass: in the game math.fmod and Lua's float % differ from Jass (measured on the + * 3.0.0 client, 360 % 7 gives 3.0000153 with both and 3 with the formula and in Jass). */ @Test public void modByADivisorThatBecomesALiteralIsTheLuaOperator() { @@ -7536,12 +7536,13 @@ public void modByADivisorThatBecomesALiteralIsTheLuaOperator() { assertTrue(fixed, fixed.contains(" % 1000003)")); assertFalse(fixed, fixed.contains("fmod") || fixed.contains("< 0")); String fixedReal = topLevelFunctionBodyWithPrefix(compiled, "fixedReal"); - assertTrue("a real modulo is fmod, also by a literal of at least 1:\n" + fixedReal, - fixedReal.contains("math.fmod(")); - assertFalse(fixedReal, fixedReal.contains(" % ")); + assertTrue("a real modulo is Blizzard.j's formula, the truncation printed in place:\n" + fixedReal, + fixedReal.contains(" / 360.)") && fixedReal.contains("// 1) | 0)")); + assertFalse(fixedReal, fixedReal.contains(" % ") || fixedReal.contains("math.fmod(") + || fixedReal.contains("__wurst_rawR2I(")); String belowOne = topLevelFunctionBodyWithPrefix(compiled, "belowOne"); - assertTrue("a real divisor below 1 keeps fmod:\n" + belowOne, belowOne.contains("math.fmod(")); - assertFalse(belowOne, belowOne.contains(" % ")); + assertTrue("a real divisor below 1 is the same formula:\n" + belowOne, belowOne.contains(" / 0.5)")); + assertFalse(belowOne, belowOne.contains(" % ") || belowOne.contains("math.fmod(")); String varying = topLevelFunctionBodyWithPrefix(compiled, "varying"); assertTrue("a runtime divisor is tested:\n" + varying, varying.contains(" % ") && varying.contains("math.fmod(")); @@ -7670,7 +7671,7 @@ public void oldGenericHandleReadsNeedNoIntNormalisation() throws IOException { assertFalse(init, init.contains("math.tointeger") || init.contains("tonumber") || init.contains("__wurst_ensureInt")); } - /** -0.0 stays -0.0 under fmod and its correction, which a real modulo always is: 1 / r is minus infinity. */ + /** -0.0 stays -0.0 under Blizzard.j's ModuloReal, which a real modulo always is: 1 / r is minus infinity. */ @Test public void modKeepsTheSignOfANegativeZeroOnLua() throws IOException { test().testLua(true).inline().localOptimizations().executeProg().lines( @@ -7687,6 +7688,6 @@ public void modKeepsTheSignOfANegativeZeroOnLua() throws IOException { " testSuccess()"); String compiled = topLevelFunctionBodyWithPrefix(compiledLua("modKeepsTheSignOfANegativeZeroOnLua"), "init_Test"); - assertTrue(compiled, compiled.contains("math.fmod(") && !compiled.contains(" % ")); + assertTrue(compiled, compiled.contains(" / 360.)") && !compiled.contains(" % ") && !compiled.contains("math.fmod(")); } } diff --git a/docs/WC3_RUNTIME.md b/docs/WC3_RUNTIME.md index 5ddffc7a4..edb216f9b 100644 --- a/docs/WC3_RUNTIME.md +++ b/docs/WC3_RUNTIME.md @@ -45,7 +45,7 @@ Every client simulates the same game from the same inputs; anything that makes t * **measured, 3.0.0** Jass literals and `S2R` are not correctly rounded and the two disagree: `0.1` reads one float high, `1.1` one float low, `S2R("100.0")` one above 100. Short exact binary fractions (`0.5`, `0.75`, `100.0`, `123456.78125`) read exactly; integer parts past 2^24 are truncated. This is why `SimpleRewrites.foldRealExactly` folds only exact literals and exact results. * **measured, 3.0.0** Lua reals have a 24-bit mantissa, not 53: `(2^24 + 1) - 2^24` is 0, the literal `16777217.` reads as 16777216, `0.9999999999999999` reads as exactly 1, and `0.7 + 0.1 + 0.1 + 0.1` computes to 1 - 2^-23. Literals are rounded to nearest and each arithmetic result is truncated, which neither double nor 32-bit float arithmetic reproduces. One `R2I` result is not evidence of a precision: `R2I(0.7 + 0.1 + 0.1 + 0.1)` is 0 for that reason, not because of doubles. * **measured, 3.0.0** `R2I` and the arithmetic `x // 1 | 0` agree on every value probed, the 32-bit edges and values outside the range included (`LuaNativeLowering.lowerRealToInt`). -* **measured, 3.0.0** Lua's float `%` and `math.fmod` with the sign correction agree for a non-negative dividend, but not for a negative one, and there only the fmod form agrees with Jass: `-0.1 % 7.` gives 6.900000064 with `%` and 6.899999616 with fmod, as Jass; `-0.0000001 % 1.` gives 0.999999872 and 1, Jass 1. Both forms differ from Jass where the game's Lua arithmetic does: `360. % 7.` is 3.0000153 and `7.5 % 360.` is 7.4999995 in Lua either way, 3 and 7.5 in Jass. The integer `%` agrees with Jass's `mod` for a positive divisor on every value probed, the 32-bit edges included. So a real modulo is `math.fmod` with the sign correction, and only an integer one by a positive literal is `%` (`LuaNativeLowering`). +* **measured, 3.0.0** A real modulo is Blizzard.j's `ModuloReal` on Jass, `a - I2R(R2I(a / b)) * b` plus `b` when negative. The same formula in the game's Lua gives Jass's result for every dividend both read alike (54 of 54 probed); Lua's float `%` and `math.fmod` do not: `360. % 7.` is 3.0000153 with either and 3 in Jass, `7.5 % 360.` is 7.4999995 and 7.5, and `-7.5 % 2.5` is 2.4999993 with fmod and its correction and 0 in Jass. The integer `%` agrees with Jass's `ModuloInteger` for a positive divisor on every value probed, the 32-bit edges included. So a real modulo is that formula with `R2I` as Lua arithmetic, and only an integer one by a positive literal is `%` (`LuaNativeLowering`). ## Establishing a fact From c86ef4f206dfb2d8fa1029ea4279be7c3abe1b10 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 11 Oct 2026 12:19:58 +0200 Subject: [PATCH 3/3] Interpreter: a real modulo is ModuloReal's formula too WurstOperator.moduloReal, which the interpreter and its ModuloReal mock use, was the exact remainder, while Jass and now Lua compute a - R2I(a / b) * b in float. It is that formula now, with the interpreter's measured R2I shared as WurstOperator.realToInt. --- AGENTS.md | 2 +- .../providers/StringProvider.java | 9 ++--- .../de/peeeq/wurstscript/WurstOperator.java | 18 ++++++++- .../tests/LuaBackendAuditTests.java | 37 +++++++++++++++++-- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c261d842a..cad5a37d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -302,7 +302,7 @@ Rules for backend work: ### Reference semantics for arithmetic -* Integer/real division and modulo semantics are centralized: `WurstOperator.moduloInteger/moduloReal` implement the Blizzard.j formula (truncated remainder, plus divisor if negative) and Jass `div` truncates toward zero. +* Integer/real division and modulo semantics are centralized: `WurstOperator.moduloInteger` is Blizzard.j's ModuloInteger (truncated remainder, plus divisor if negative), `WurstOperator.moduloReal` is ModuloReal's formula itself (`a - R2I(a / b) * b` in float, plus divisor if negative, not the exact remainder), and Jass `div` truncates toward zero. * The Lua helpers (`__wurst_intDiv`, `__wurst_modInt`, `__wurst_modReal`, built in `LuaNativeLowering`), the interpreter's `MathProvider` mocks, and constant folding (`SimpleRewrites`) must all stay consistent with those helpers — never reimplement div/mod locally. A real modulo on Lua is Blizzard.j's `ModuloReal` formula, as on Jass, never `math.fmod` or Lua's float `%`, which differ from Jass in the game ([`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). * Jass `==` on reals allows a difference of 0.001 (`WurstOperator.jassRealEquals`), while `!=` and the orderings are exact, so on the Jass target `not (a == b)` is not `a != b` for reals. Lua compares exactly, by design: the tolerance would cost every real comparison. Both interpreters use that helper; the optimiser must not swap one for the other or fold nearly equal reals on Jass. * The game reads Jass real literals inexactly and computes Lua reals with a 24-bit mantissa (measurements: [`docs/WC3_RUNTIME.md`](docs/WC3_RUNTIME.md) "Numbers"). So on both targets real folding (`SimpleRewrites.foldRealExactly`) reads and writes only exact literals and folds an operation only when its exact result is one. Never fold in double or float, and never treat one `R2I` result as proof of a precision. The test runtime's Lua computes in double, so a test that runs the emitted Lua does not show the game's rounding. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java index 212a03e5c..18e4b64bd 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java @@ -1,6 +1,7 @@ package de.peeeq.wurstio.jassinterpreter.providers; import de.peeeq.wurstio.jassinterpreter.InterpreterException; +import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.intermediatelang.ILconst; import de.peeeq.wurstscript.intermediatelang.ILconstBool; import de.peeeq.wurstscript.intermediatelang.ILconstInt; @@ -123,13 +124,9 @@ private static long[] roundedParts(float value, int precision) { return new long[] {whole, digits}; } - /** Measured on the 3.0.0 client: truncates, and wraps to 32 bits rather than saturating - * (1e10 is 1410065408). */ + /** See {@link WurstOperator#realToInt}, measured on the 3.0.0 client. */ public ILconstInt R2I(ILconstReal i) { - double value = i.getVal(); - // Truncated in double and wrapped by remainder: a cast to long would saturate above its range. - double truncated = value < 0 ? Math.ceil(value) : Math.floor(value); - return new ILconstInt((int) (long) (truncated % 4294967296.0)); + return new ILconstInt(WurstOperator.realToInt(i.getVal())); } public ILconstReal I2R(ILconstInt i) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java index 8f699004c..87597ab48 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java @@ -221,15 +221,29 @@ public static int jassModuloInteger(int a, int b) { return a % b; } - /** Reference semantics for Wurst's real {@code mod}; see {@link #moduloInteger}. */ + /** + * Reference semantics for Wurst's real {@code mod}: Blizzard.j's ModuloReal, which a real modulo is on both + * targets, {@code a - I2R(R2I(a / b)) * b} plus b when that is negative, each step in float as in the game. + * That is not the exact remainder: the quotient rounds, and for a quotient past 2^24 the result can leave + * [0, b) altogether. Measured on the 3.0.0 client, Jass and the same formula in the game's Lua agree on every + * probed operand; math.fmod and Lua's float % do not (360 % 7 is 3.0000153 with either). + */ public static float moduloReal(float a, float b) { - float r = a % b; + float quotient = a / b; + float r = a - (float) realToInt(quotient) * b; if (r < 0) { r += b; } return r; } + /** Jass's R2I, measured on the 3.0.0 client: truncates, and wraps to 32 bits rather than saturating (1e10 is 1410065408). */ + public static int realToInt(double value) { + // Truncated in double and wrapped by remainder: a cast to long would saturate above its range. + double truncated = value < 0 ? Math.ceil(value) : Math.floor(value); + return (int) (long) (truncated % 4294967296.0); + } + /** * Measured on the 3.0.0 client: Jass {@code ==} on two reals is true when they differ by at most * this much ({@code 1.0 == 1.0009} is true, {@code 1.0 == 1.0011} is false), while {@code !=} and diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index b53aa2bbc..0699f8af3 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -7583,10 +7583,10 @@ public void modByADivisorThatBecomesALiteralIsTheLuaOperator() { }; /** - * Wurst mod is Blizzard.j's ModuloInteger/ModuloReal: the truncated remainder, plus the divisor - * when it is negative. Lua's % agrees for an int divisor above 0 and a real one of at least 1, - * which the helper tests at run time and the optimiser folds for a literal. Against Jass and the - * interpreter, once with the helper called and once inlined and folded. + * Wurst mod is Blizzard.j's ModuloInteger/ModuloReal: for an int the truncated remainder, plus the + * divisor when it is negative, which Lua's % agrees with for a divisor above 0 (the helper tests it at + * run time and the optimiser folds it for a literal); for a real ModuloReal's formula. Against Jass and + * the interpreter, once with the helper called and once inlined and folded. */ @Test public void modAgreesWithJassForEveryDivisorSign() { @@ -7598,6 +7598,35 @@ public void modAgreesWithJassForEveryDivisorSignWhenFolded() { test().testLua(true).luaOnly(false).inline().localOptimizations().executeProg().lines(MOD_SEMANTICS_PROG); } + /** + * A real modulo is ModuloReal's formula, {@code a - R2I(a / b) * b} plus b when negative, in every + * run: the interpreter, Jass and Lua. It is not the exact remainder: in float, 1e9 % 3 is -29 by the + * formula and 1 by fmod. The comparisons are exact ({@code !=}), as Jass's {@code ==} on reals has a + * tolerance. + */ + @Test + public void realModIsModuloRealsFormulaInEveryRun() { + test().testLua(true).luaOnly(false).withStdLib().executeProg().lines( + "package Test", + "@noinline function w(real x) returns real", + " return x", + "function formula(real a, real b) returns real", + // the int converts to a real in the multiplication, as I2R does (the test Lua has no I2R) + " var m = a - R2I(a / b) * b", + " if m < 0.", + " m = m + b", + " return m", + "function same(real a, real b) returns boolean", + " return not (a != b)", + "init", + " if same(w(1000000000.) % 3., formula(w(1000000000.), 3.))", + " and same(w(360.) % 7., formula(w(360.), 7.))", + " and same(w(-7.5) % w(2.5), formula(w(-7.5), w(2.5)))", + " and same(w(-0.1) % 7., formula(w(-0.1), 7.))", + " and same(w(7.5) % 360., formula(w(7.5), 360.))", + " testSuccess()"); + } + /** * The {@code or ""} guard of a concatenation operand is placed before inlining, where * {@code x.str()} is a call that might answer nil. Inlined it is the {@code tostring} of I2S, and