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 18e4b64bd..ed44272e8 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 @@ -68,14 +68,14 @@ public ILconstReal S2R(@Nullable ILconstString s) { return new ILconstReal(Float.isInfinite(value) ? 0 : value); } - /** Measured on the 3.0.0 client: three decimals, rounded half up. The integer part wraps to 32 bits - * (3e9 prints -1294967296.000), and a negative value which rounds to zero keeps its sign. */ + /** Measured on the 3.0.0 client: three decimals, rounded half up, the whole integer part (a computed 3e9 prints + * 3000000000.000, 5000000320 prints 5000000320.000), and a negative value which rounds to zero keeps its sign. + * A real literal past 2^31 is read wrapped by Jass (the literal 3000000000. is negative), which R2S shows. */ public ILconstString R2S(ILconstReal r) { float x = r.getVal(); - long[] parts = roundedParts(Math.abs(x), 3); - int whole = (int) (x < 0 ? -parts[0] : parts[0]); - String sign = x < 0 && whole == 0 ? "-" : ""; - return ILconstString.fromText(sign + whole + "." + StringUtils.leftPad(Long.toString(parts[1]), 3, '0')); + Rounded parts = roundedParts(Math.abs(x), 3); + String sign = x < 0 ? "-" : ""; + return ILconstString.fromText(sign + parts.whole() + "." + StringUtils.leftPad(Long.toString(parts.digits()), 3, '0')); } /** Measured on the 3.0.0 client: the sign comes first, then the integer part left-padded to @@ -91,37 +91,42 @@ public ILconstString R2SW(ILconstReal r, ILconstInt width, ILconstInt precision) + ", beyond what the interpreter models (precision up to " + R2SW_MAX_PRECISION + ", width up to " + R2SW_MAX_WIDTH + ")."); } - long[] parts = roundedParts(Math.abs(x), digits); - // The signed value wraps to 32 bits, as in R2S; the sign is printed ahead of the padding. - int wrapped = (int) (x < 0 ? -parts[0] : parts[0]); - String sign = wrapped < 0 || (wrapped == 0 && x < 0) ? "-" : ""; - String whole = StringUtils.leftPad(Long.toString(Math.abs((long) wrapped)), (int) Math.max(0, padding)); - String fraction = digits == 0 ? "0" : StringUtils.leftPad(Long.toString(parts[1]), digits, '0'); + Rounded parts = roundedParts(Math.abs(x), digits); + // The whole integer part, as in R2S; the sign is printed ahead of the padding. + String sign = x < 0 ? "-" : ""; + String whole = StringUtils.leftPad(parts.whole().toString(), (int) Math.max(0, padding)); + String fraction = digits == 0 ? "0" : StringUtils.leftPad(Long.toString(parts.digits()), digits, '0'); return ILconstString.fromText(sign + whole + "." + fraction); } private static final int R2SW_MAX_PRECISION = 18; private static final int R2SW_MAX_WIDTH = 1000; + /** The whole part of a non-negative value, exact even past the long range, and its fraction digits. */ + private record Rounded(java.math.BigInteger whole, long digits) { + } + /** * The whole part and the fraction digits of a non-negative value, rounded half up at the given * precision and carried into the whole part. The fraction is scaled in float arithmetic, as the * engine does, which is what shows at nine digits: its R2SW(r, 1, 9) is not the exact decimal. */ - private static long[] roundedParts(float value, int precision) { - // Math.floor is exact for any float, and the integer part is wrapped to 32 bits by remainder - // rather than by a cast to long, which would saturate above Long.MAX_VALUE. Past 2^56 every - // float is a multiple of 2^32, so its wrapped part is 0. + private static Rounded roundedParts(float value, int precision) { + if (!Float.isFinite(value)) { + // not measured; the game's reals do not reach it through arithmetic (a division by zero stops the thread) + return new Rounded(java.math.BigInteger.ZERO, 0); + } + // Math.floor is exact for any float, and so is its decimal expansion. double floor = Math.floor(value); - long whole = (long) (floor % 4294967296.0); + java.math.BigInteger whole = new java.math.BigDecimal(floor).toBigInteger(); float fraction = (float) (value - floor); long limit = (long) Math.pow(10, precision); long digits = (long) (fraction * (float) limit + 0.5f); if (digits >= limit) { - whole++; + whole = whole.add(java.math.BigInteger.ONE); digits -= limit; } - return new long[] {whole, digits}; + return new Rounded(whole, digits); } /** See {@link WurstOperator#realToInt}, measured on the 3.0.0 client. */ 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 87597ab48..03bbebcdd 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 @@ -163,7 +163,7 @@ public ILconst evaluateBinaryOperator(ILconst left, case DIV_INT: return new ILconstInt(((ILconstInt) left).getVal() / ((ILconstInt) right.get()).getVal()); case DIV_REAL: - return new ILconstReal(getReal(left) / getReal(right.get())); + return new ILconstReal(divideReal(getReal(left), getReal(right.get()))); case EQ: { ILconst rightVal = right.get(); rejectUnresolvedDefault(left, rightVal); @@ -229,7 +229,7 @@ public static int jassModuloInteger(int a, int b) { * 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 quotient = a / b; + float quotient = divideReal(a, b); float r = a - (float) realToInt(quotient) * b; if (r < 0) { r += b; @@ -237,11 +237,24 @@ public static float moduloReal(float a, float 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). */ + /** + * A real division. Measured on the 3.0.0 client, a real division by zero stops the thread in Jass ({@code 1. / 0.}, + * {@code 0. / 0.} and ModuloReal by 0 alike), so it is the same error as an integer one here, not an infinity. + */ + public static float divideReal(float a, float b) { + if (b == 0) { + throw new ArithmeticException("/ by zero"); + } + return a / b; + } + + /** + * Jass's R2I, measured on the 3.0.0 client: truncates, and saturates outside the 32-bit range (a computed 5000000320 + * is 2147483647, its negative -2147483648, a computed 1e20 2147483647), as Java's cast does. A real literal past + * 2^31 is read wrapped by Jass (the literal 1e10 is 1410065408.), which R2I of it shows, not R2I itself. + */ 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); + return (int) value; } /** diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstInt.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstInt.java index a8ebf00a6..8f9bdc042 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstInt.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstInt.java @@ -1,5 +1,7 @@ package de.peeeq.wurstscript.intermediatelang; +import de.peeeq.wurstscript.WurstOperator; + import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeInt; @@ -71,7 +73,7 @@ public ILconstNum div(ILconstNum other) { if (other instanceof ILconstInt iLconstInt) { return create(val / iLconstInt.getVal()); } else if (other instanceof ILconstReal iLconstReal) { - return ILconstReal.create(val / iLconstReal.getVal()); + return ILconstReal.create(WurstOperator.divideReal(val, iLconstReal.getVal())); } else { throw new Error(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java index c131c0acb..f39d65004 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java @@ -1,5 +1,7 @@ package de.peeeq.wurstscript.intermediatelang; +import de.peeeq.wurstscript.WurstOperator; + import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeReal; @@ -74,9 +76,9 @@ public ILconstNum mul(ILconstNum other) { @Override public ILconstNum div(ILconstNum other) { if (other instanceof ILconstInt iLconstInt) { - return create(val / iLconstInt.getVal()); + return create(WurstOperator.divideReal(val, iLconstInt.getVal())); } else if (other instanceof ILconstReal iLconstReal) { - return create(val / iLconstReal.getVal()); + return create(WurstOperator.divideReal(val, iLconstReal.getVal())); } else { throw new Error(); } 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 e2b2caca1..28345fe76 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 @@ -251,7 +251,8 @@ static LuaExpr keyedMapRead(String stub, LuaExpr table, LuaExpr key) { * float, and {@code | 0} turns it into an integer, which it can because the range was checked; a * negative real is truncated as the negated floor of its negation. Both middle values are integers, * which Lua counts as true even when they are 0, so {@code and}/{@code or} pick the right branch. - * NaN, the infinities and everything outside the range call the native. + * Outside the range R2I saturates, in Jass and in the game's Lua alike (measured on the 3.0.0 client: a computed + * 5000000320 is 2147483647), so the bound is answered in place, and NaN is 0 as in the interpreter. */ static LuaExpr realToInt(LuaExpr x) { LuaExpr nonNegative = LuaAst.LuaExprBinary( @@ -265,13 +266,20 @@ static LuaExpr realToInt(LuaExpr x) { LuaExpr truncated = floorToInt(x.copy()); LuaExpr negatedTruncated = LuaAst.LuaExprUnary(LuaAst.LuaOpMinus(), floorToInt(LuaAst.LuaExprUnary(LuaAst.LuaOpMinus(), x.copy()))); - LuaExpr nativeCall = LuaAst.LuaExprFunctionCallByName("R2I", LuaAst.LuaExprlist(x.copy())); + LuaExpr saturated = LuaAst.LuaExprBinary( + LuaAst.LuaExprBinary(LuaAst.LuaExprBinary(x.copy(), LuaAst.LuaOpGreater(), LuaAst.LuaExprRealVal("0.0")), + LuaAst.LuaOpAnd(), LuaAst.LuaExprIntVal("2147483647")), + LuaAst.LuaOpOr(), + LuaAst.LuaExprBinary( + LuaAst.LuaExprBinary(LuaAst.LuaExprBinary(x.copy(), LuaAst.LuaOpLess(), LuaAst.LuaExprRealVal("0.0")), + LuaAst.LuaOpAnd(), LuaAst.LuaExprIntVal("-2147483648")), + LuaAst.LuaOpOr(), LuaAst.LuaExprIntVal("0"))); return LuaAst.LuaExprBinary( LuaAst.LuaExprBinary(nonNegative, LuaAst.LuaOpAnd(), truncated), LuaAst.LuaOpOr(), LuaAst.LuaExprBinary( LuaAst.LuaExprBinary(negative, LuaAst.LuaOpAnd(), negatedTruncated), - LuaAst.LuaOpOr(), nativeCall)); + LuaAst.LuaOpOr(), saturated)); } private static LuaExpr floorToInt(LuaExpr x) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java index 7a793649e..34238d0d4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java @@ -132,6 +132,9 @@ public class LuaNatives { // Truncate toward zero. Not math.modf: that returns TWO values // (which expand into enclosing argument lists) and its integral // part is a float (tostring would yield "3.0" instead of "3"). + // Saturates outside the 32-bit range, as the game's R2I does (measured on the 3.0.0 client). + f.getBody().add(LuaAst.LuaLiteral("if x >= 2147483647 then return 2147483647 end")); + f.getBody().add(LuaAst.LuaLiteral("if x <= -2147483648 then return -2147483648 end")); f.getBody().add(LuaAst.LuaLiteral("if x >= 0 then")); f.getBody().add(LuaAst.LuaLiteral(" return math.floor(x)")); f.getBody().add(LuaAst.LuaLiteral("end")); @@ -148,7 +151,10 @@ public class LuaNatives { f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); f.getBody().add(LuaAst.LuaLiteral("if x >= 0.0 and x < 2147483648.0 then return x // 1 | 0 end")); f.getBody().add(LuaAst.LuaLiteral("if x < 0.0 and x > -2147483649.0 then return -(-x // 1 | 0) end")); - f.getBody().add(LuaAst.LuaLiteral("return R2I(x)")); + // R2I saturates outside the range (measured on the 3.0.0 client), and NaN is 0 as in the interpreter + f.getBody().add(LuaAst.LuaLiteral("if x > 0.0 then return 2147483647 end")); + f.getBody().add(LuaAst.LuaLiteral("if x < 0.0 then return -2147483648 end")); + f.getBody().add(LuaAst.LuaLiteral("return 0")); }); addNative("__wurst_rawToInteger", f -> { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java index 0c0d27998..4fd3d00a2 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java @@ -406,11 +406,17 @@ public void derivedAndLessCommonConstants() { ); } - /** Every expected value here was measured on the 3.0.0 client. */ + /** + * Every expected value here was measured on the 3.0.0 client. A real past 2^31 is computed (w keeps it from + * folding): Jass reads a real literal past 2^31 wrapped (the literal 3000000000. is negative), which the + * interpreter does not model, and which is what R2S, R2SW and R2I of such a literal show. + */ @Test public void stringNativesAsMeasured() { test().withStdLib().executeProg(true).testLua(false).lines( "package Test", + "@noinline function w(real x) returns real", + " return x", "function measured(string name, string actual, string expected)", " if actual != expected", " testFail(name + \" gave \" + actual)", @@ -433,11 +439,8 @@ public void stringNativesAsMeasured() { " measured(\"R2S carry\", R2S(0.9996), \"1.000\")", " measured(\"R2S negative\", R2S(-1.5), \"-1.500\")", " measured(\"R2S negative to zero\", R2S(-0.0004), \"-0.000\")", - " measured(\"R2S wraps\", R2S(3000000000.), \"-1294967296.000\")", - " measured(\"R2S negative wraps\", R2S(-3000000000.), \"1294967296.000\")", - " measured(\"R2S 1e10\", R2S(10000000000.), \"1410065408.000\")", - // Past 2^56 a real is a multiple of 2^32, so its integer part wraps to 0. - " measured(\"R2S and R2SW beyond the long range\", R2S(100000000000000000000.) + \",\" + R2SW(100000000000000000000., 1, 2), \"0.000,0.00\")", + " measured(\"R2S of a computed 3e9\", R2S(w(30000.) * w(100000.)), \"3000000000.000\")", + " measured(\"R2S of a computed -3e9\", R2S(-(w(30000.) * w(100000.))), \"-3000000000.000\")", " measured(\"R2SW padded\", R2SW(1.5, 8, 2), \" 1.50\")", " measured(\"R2SW negative padded\", R2SW(-1.5, 8, 3), \"- 1.500\")", " measured(\"R2SW precision 0\", R2SW(1.5, 2, 0), \" 2.0\")", @@ -446,12 +449,13 @@ public void stringNativesAsMeasured() { " measured(\"R2SW negative to zero\", R2SW(-0.4, 1, 0), \"-0.0\")", " measured(\"R2SW integer\", R2SW(7., 4, 0), \" 7.0\")", " measured(\"R2SW carry\", R2SW(0.9996, 1, 3), \"1.000\")", - // Wraps like R2S: 3e9 is measured, and -3e9 wraps to the positive value R2S prints for it. - " measured(\"R2SW wraps\", R2SW(3000000000., 1, 1) + \",\" + R2SW(-3000000000., 1, 3), \"-1294967296.0,1294967296.000\")", + " measured(\"R2SW of a computed 3e9\", R2SW(w(30000.) * w(100000.), 1, 1) + \",\"" + + " + R2SW(-(w(30000.) * w(100000.)), 1, 3), \"3000000000.0,-3000000000.000\")", " measured(\"R2SW smallest width\", R2SW(1., -2147483647 - 1, 1), \"1.0\")", " measured(\"R2I truncates\", I2S(R2I(1.9)) + \",\" + I2S(R2I(-1.9)), \"1,-1\")", - " measured(\"R2I wraps\", I2S(R2I(10000000000.)) + \",\" + I2S(R2I(-10000000000.)), \"1410065408,-1410065408\")", - " measured(\"R2I beyond the long range\", I2S(R2I(100000000000000000000.)) + \",\" + I2S(R2I(-100000000000000000000.)), \"0,0\")", + " measured(\"R2I saturates\", I2S(R2I(w(50000.) * w(100000.))) + \",\" + I2S(R2I(-(w(50000.) * w(100000.))))," + + " \"2147483647,-2147483648\")", + " measured(\"R2I saturates far out\", I2S(R2I(w(50000.) * w(100000.) * w(50000.) * w(100000.) * w(4.))), \"2147483647\")", " measured(\"SubString end before start\", SubString(\"abcdef\", 3, 1), \"def\")", " measured(\"SubString empty range\", SubString(\"abcdef\", 3, 3), \"\")", " measured(\"SubString at the end\", SubString(\"abcdef\", 6, 8), \"\")", 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 0699f8af3..2e385e589 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 @@ -7604,6 +7604,71 @@ public void modAgreesWithJassForEveryDivisorSignWhenFolded() { * formula and 1 by fmod. The comparisons are exact ({@code !=}), as Jass's {@code ==} on reals has a * tolerance. */ + /** + * Measured on the 3.0.0 client, a real division or modulo by zero stops the thread in Jass ({@code 1. / 0.}, + * {@code 0. / 0.}, {@code 5. % 0.} and ModuloReal by 0 alike). The interpreters, which model Jass, stop too, as + * for an integer division by zero, rather than going on with an infinity. + */ + @Test + public void realDivisionByZeroStopsTheInterpreters() { + for (String expr : new String[]{"w(1.) / w(0.)", "w(5.) % w(0.)", "w(0.) / w(0.)"}) { + Throwable stopped = null; + try { + test().executeProg().lines( + "package Test", + "native testSuccess()", + "@noinline function w(real x) returns real", + " return x", + "init", + " let x = " + expr, + " if x == x or x != x", + " testSuccess()"); + } catch (Throwable t) { + stopped = t; + } + assertTrue("a real division by zero stops: " + expr, stopped != null && String.valueOf(stopped.getMessage()).contains("by zero")); + } + } + + /** + * The Jass interpreter divides through {@code ILconstNum.div}, which a program never reaches when the IM + * interpreter stops first, so it is checked on its own: a real division by zero stops, an integer one too. + */ + @Test + public void theJassInterpretersRealDivisionByZeroStops() { + de.peeeq.wurstscript.intermediatelang.ILconstReal zero = de.peeeq.wurstscript.intermediatelang.ILconstReal.create(0f); + org.testng.Assert.assertThrows(ArithmeticException.class, + () -> de.peeeq.wurstscript.intermediatelang.ILconstReal.create(1f).div(zero)); + org.testng.Assert.assertThrows(ArithmeticException.class, + () -> de.peeeq.wurstscript.intermediatelang.ILconstReal.create(0f).div(zero)); + org.testng.Assert.assertThrows(ArithmeticException.class, + () -> de.peeeq.wurstscript.intermediatelang.ILconstInt.create(1).div(zero)); + org.testng.Assert.assertThrows(ArithmeticException.class, + () -> de.peeeq.wurstscript.intermediatelang.ILconstReal.create(1f) + .div(de.peeeq.wurstscript.intermediatelang.ILconstInt.create(0))); + assertEquals(0.5f, ((de.peeeq.wurstscript.intermediatelang.ILconstReal) + de.peeeq.wurstscript.intermediatelang.ILconstReal.create(1f).div( + de.peeeq.wurstscript.intermediatelang.ILconstReal.create(2f))).getVal(), 0f); + } + + /** + * Measured on the 3.0.0 client, R2I saturates outside the 32-bit range, in Jass and in the game's Lua alike: a + * computed 5000000320 is 2147483647, its negative -2147483648. The interpreters and the test Lua's R2I agree, so + * a real modulo whose quotient leaves the range is the same everywhere too. + */ + @Test + public void r2iSaturatesInEveryRun() { + test().testLua(true).luaOnly(false).withStdLib().executeProg().lines( + "package Test", + "@noinline function w(real x) returns real", + " return x", + "init", + " let big = w(50000.) * w(100000.)", + " if R2I(big) == 2147483647 and R2I(-big) == -2147483648 and R2I(big * big) == 2147483647", + " and big % 1. > 2000000000.", + " testSuccess()"); + } + @Test public void realModIsModuloRealsFormulaInEveryRun() { test().testLua(true).luaOnly(false).withStdLib().executeProg().lines( diff --git a/docs/WC3_RUNTIME.md b/docs/WC3_RUNTIME.md index edb216f9b..482f68f33 100644 --- a/docs/WC3_RUNTIME.md +++ b/docs/WC3_RUNTIME.md @@ -44,8 +44,10 @@ 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** `R2I` truncates, and saturates outside the 32-bit range, in Jass and in the game's Lua alike: a computed 5000000320 is 2147483647, its negative -2147483648, a computed 1e20 2147483647. Inside the range the arithmetic `x // 1 | 0` agrees with it; outside, the Lua lowering answers the bound itself (`LuaNativeLowering.lowerRealToInt`, `ExprTranslation.realToInt`). Integer arithmetic wraps at 32 bits on both (`2147483647 + 1` is -2147483648). +* **measured, 3.0.0** `R2S` and `R2SW` print the whole integer part of a computed value (3e9 is `3000000000.000`, -3e9 `-3000000000.000`); a computed 5e9 prints as `5000000320.000`, which the interpreter does not model. Jass reads a real literal whose integer part passes 2^31 wrapped to 32 bits, signed: the literal `3000000000.` is negative and `10000000000.` reads as 1410065408. What `R2S`, `R2SW` and `R2I` of such a literal answer is that, not the native; the interpreter does not model it, and Lua reads the literal correctly. * **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`). +* **measured, 3.0.0** A real division by zero stops the thread in Jass: `1. / 0.`, `-1. / 0.`, `0. / 0.`, `5. % 0.` and `ModuloReal(5., 0.)` all do. The game's Lua goes on: `1. / 0.` is 1.7014e38 (the largest float, not an infinity), `-1. / 0.` its negative, `0. / 0.` is 1, and the Lua real modulo by 0 answers a number. The interpreters stop as Jass does (`WurstOperator.divideReal`); the Lua backend adds no check, which would cost every real division. ## Establishing a fact