diff --git a/AGENTS.md b/AGENTS.md index fd66114fc..cad5a37d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -302,8 +302,8 @@ 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. +* 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/LuaEnsureTypeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java index 17765ad76..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,17 +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()); - } - - /** 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/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/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..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,9 +200,7 @@ 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 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 +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 == luaRawFloorModRealFunc); + || target == luaRawFloorModIntFunc); } /** * Whether {@code f} is one of the Lua stubs which only read a table: a keyed-map read, or a hashtable @@ -272,8 +269,8 @@ public List pinnedFunctions() { List result = new ArrayList<>(); for (ImFunction f : new ImFunction[]{ ensureIntFunc, ensureBoolFunc, ensureRealFunc, ensureStrFunc, stringConcatFunc, - luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFmodRealFunc, luaRawFloorModIntFunc, - luaRawFloorModRealFunc, luaRawConcatFunc, + luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFloorModIntFunc, + 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..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,8 +607,6 @@ private static final class DivModFunctions { private ImFunction rawFloorDivInt; private ImFunction rawFmodInt; private ImFunction rawFloorModInt; - private ImFunction rawFmodReal; - private ImFunction rawFloorModReal; private ImFunction intDiv; private ImFunction modInt; private ImFunction modReal; @@ -630,15 +640,15 @@ 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. + * 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 = buildMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), - WurstOperator.GREATER_EQ, JassIm.ImRealVal("1."), rawFmodReal(), rawFloorModReal()); + modReal = buildBlizzardModReal(rawRealToInt(translator, created)); translator.luaModRealFunc = modReal; created.add(modReal); } @@ -677,25 +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; - } - - /** 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 +729,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 +764,36 @@ private static ImFunction buildMod(String name, ImType numType, ImExpr zeroLiter JassIm.ImVars(r), body, Collections.emptyList()); } + /** + * 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(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), 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, "__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) { 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..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 @@ -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) { @@ -216,9 +216,7 @@ 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.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..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 @@ -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 Blizzard.j's + // formula (see modByADivisorThatBecomesALiteral...) assertTrue("repro must exercise integer and real mod lowering:\n" + compiled, - compiled.contains(" % 2)") && 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 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() { @@ -7534,11 +7536,13 @@ 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 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(")); @@ -7579,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() { @@ -7594,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 @@ -7667,7 +7700,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 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( @@ -7684,6 +7717,6 @@ public void modKeepsTheSignOfANegativeZeroOnLua() throws IOException { " testSuccess()"); String compiled = topLevelFunctionBodyWithPrefix(compiledLua("modKeepsTheSignOfANegativeZeroOnLua"), "init_Test"); - assertTrue(compiled, compiled.contains(" % 360.)")); + assertTrue(compiled, compiled.contains(" / 360.)") && !compiled.contains(" % ") && !compiled.contains("math.fmod(")); } } diff --git a/docs/WC3_RUNTIME.md b/docs/WC3_RUNTIME.md index 2f52569ab..edb216f9b 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** 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