From eeb71363a5fa2b0e48b52ba797637f16ec969dc9 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 12 Oct 2026 09:51:23 +0200 Subject: [PATCH] Jass: print a large real literal as the float it means, a compile-time real without an exponent --- .../wurstio/CompiletimeFunctionRunner.java | 16 +++++- .../translation/imtojass/ExprTranslation.java | 51 ++++++++++++++++++- .../tests/CompiletimeHashtableTests.java | 15 ++++++ .../wurstscript/tests/CompiletimeTests.java | 26 ++++++++++ .../wurstscript/tests/InterpreterTests.java | 4 +- .../tests/LuaBackendAuditTests.java | 39 ++++++++++++++ docs/WC3_RUNTIME.md | 2 +- 7 files changed, 147 insertions(+), 6 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 6be94c336..39b1eb6d3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -480,13 +480,25 @@ private String literalText(ILconstString value, Element trace) { return value.text(); } + /** + * A real as a decimal literal both backends read: the shortest decimal which reads back as the + * same float, without the exponent Java prints from 10^7 on and below 10^-3, which Jass does not parse. + */ + private static String realLiteral(Element trace, float value) { + if (!Float.isFinite(value)) { + throw new InterpreterException(trace, "The compile-time real " + value + " has no literal."); + } + String plain = new java.math.BigDecimal(Float.toString(value)).toPlainString(); + return plain.contains(".") ? plain : plain + "."; + } + private ImExpr constantToExpr(Element trace, ILconst value, @Nullable ImType expectedType) { if (value instanceof ILconstBool iLconstBool) { return JassIm.ImBoolVal(iLconstBool.getVal()); } else if (value instanceof ILconstInt iLconstInt) { return JassIm.ImIntVal(iLconstInt.getVal()); } else if (value instanceof ILconstReal iLconstReal) { - return JassIm.ImRealVal("" + iLconstReal.getVal()); + return JassIm.ImRealVal(realLiteral(trace, iLconstReal.getVal())); } else if (value instanceof ILconstString iLconstString) { return JassIm.ImStringVal(literalText(iLconstString, trace)); } else if (value instanceof ILconstNull) { @@ -1110,7 +1122,7 @@ private ImExpr constantToExprHashtable(Element trace, ImVar htVar, IlConstHandle JassIm.ImVarAccess(htVar), JassIm.ImIntVal(parentKey), JassIm.ImIntVal(childKey), - JassIm.ImRealVal("" + iv.getVal()) + JassIm.ImRealVal(realLiteral(trace, iv.getVal())) ), false, CallType.NORMAL)); } else if (v instanceof ILconstString iv) { if (saveStr == null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java index 9613ec3dd..2e8b35ac2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java @@ -8,6 +8,8 @@ import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.types.TypesHelper; +import java.math.BigDecimal; + import static de.peeeq.wurstscript.jassAst.JassAst.*; public class ExprTranslation { @@ -111,7 +113,54 @@ public static JassExpr translate(ImOperatorCall e, ImToJassTranslator translator public static JassExpr translate(ImRealVal e, ImToJassTranslator translator) { - return JassExprRealVal(e.getValR()); + return realLiteral(e); + } + + private static final BigDecimal TWO_TO_24 = BigDecimal.valueOf(1 << 24); + /** The largest power of two whose literal Jass reads exactly. */ + private static final int MAX_FACTOR_EXPONENT = 30; + + /** + * The Jass for the float a real literal means. Jass reads a literal's integer part as a 32-bit + * integer and truncates it past 2^24 (measured on the 3.0.0 client: the literal 3000000000. is + * negative, 10000000000. reads as 1410065408, 16777219. as 16777218), while it reads an exact + * float below 2^31 exactly. So past 2^24, where every float is an integer, the backend prints + * the float's own digits, and from 2^31 on a product of exact factors, its odd part and powers + * of two, which Jass multiplies exactly. A literal past the largest float, which the interpreter + * reads as infinity, is the largest float: Jass has no product which gives infinity (measured + * too: 2^150 compares below 3e38 and prints as 0.000, while the largest float compares above it). + */ + private static JassExpr realLiteral(ImRealVal e) { + String literal = e.getValR(); + if (new BigDecimal(literal).abs().compareTo(TWO_TO_24) <= 0) { + return JassExprRealVal(literal); + } + float value = Float.parseFloat(literal); + float magnitude = Math.min(Math.abs(value), Float.MAX_VALUE); + JassExpr result; + if (magnitude < 2147483648f) { + result = JassExprRealVal(new BigDecimal(magnitude).toBigInteger() + "."); + } else { + // magnitude = odd * 2^exponent, with odd below 2^24 + int exponent = Math.getExponent(magnitude) - 23; + long odd = (long) Math.scalb(magnitude, -exponent); + int zeros = Long.numberOfTrailingZeros(odd); + odd >>= zeros; + exponent += zeros; + if (odd == 1) { + int factor = Math.min(exponent, MAX_FACTOR_EXPONENT); + result = JassExprRealVal((1L << factor) + "."); + exponent -= factor; + } else { + result = JassExprRealVal(odd + "."); + } + while (exponent > 0) { + int factor = Math.min(exponent, MAX_FACTOR_EXPONENT); + result = JassExprBinary(result, JassOpMult(), JassExprRealVal((1L << factor) + ".")); + exponent -= factor; + } + } + return value < 0 ? JassExprUnary(JassOpMinus(), result) : result; } public static JassExpr translate(ImStatementExpr e, ImToJassTranslator translator) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java index 744088c4c..6c392007b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java @@ -52,6 +52,21 @@ public void valuesOfDifferentTypesShareAKeyAndAreReplayed() { " testSuccess()")); } + /** Java prints these reals with an exponent, which Jass does not parse; the replay writes decimals. */ + @Test + public void largeAndSmallRealsAreReplayedAsDecimals() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true).lines(program( + "@noinline function w(real x) returns real", + " return x", + "@compiletime", + "function fill()", + " SaveReal(h, 1, 1, w(10000.) * w(1000.))", + " SaveReal(h, 1, 2, w(1.) / w(1024.))", + "init", + " if LoadReal(h, 1, 1) == 10000000. and LoadReal(h, 1, 2) == 0.0009765625", + " testSuccess()")); + } + @Test public void flushedAndRemovedEntriesAreNotReplayed() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true).lines(program( diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index e8a30f8ea..cd5386487 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java @@ -4,8 +4,12 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import org.slf4j.LoggerFactory; +import com.google.common.base.Charsets; +import com.google.common.io.Files; import org.testng.annotations.Test; +import java.io.File; +import java.io.IOException; import java.util.List; import static org.testng.Assert.assertEquals; @@ -60,6 +64,28 @@ public void testSimpleCompiletimeReal() { " testSuccess()"); } + /** Java prints a float from 10^7 on and below 10^-3 with an exponent, which Jass does not parse. */ + @Test + public void compiletimeRealsBecomeDecimalLiterals() throws IOException { + test().executeProg(true) + .runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "function compiletime(real r) returns real", + " return r", + "@noinline function w(real x) returns real", + " return x", + "constant big = compiletime(w(10000.) * w(1000.))", + "constant huge = compiletime(w(30000.) * w(100000.) * w(1000.))", + "constant small = compiletime(w(1.) / w(1024.))", + "init", + " if not (big != w(10000.) * w(1000.)) and not (huge != w(30000.) * w(100000.) * w(1000.))", + " and not (small != w(1.) / w(1024.))", + " testSuccess()"); + String jass = Files.toString(new File(TEST_OUTPUT_PATH + "CompiletimeTests_compiletimeRealsBecomeDecimalLiterals_no_opts.j"), Charsets.UTF_8); + assertFalse(java.util.regex.Pattern.compile("\\d[eE][-+]?\\d").matcher(jass).find(), "no exponent:\n" + jass); + } + @Test public void testCompiletimeArray() { test().executeProg(true) 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 4fd3d00a2..447c41ce3 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 @@ -408,8 +408,8 @@ public void derivedAndLessCommonConstants() { /** * 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. + * folding), as it was in the measurement: Jass reads such a literal wrapped as written (the literal + * 3000000000. is negative), which is why the Jass backend prints it otherwise. */ @Test public void stringNativesAsMeasured() { 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 2e385e589..94f235d8a 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 @@ -7669,6 +7669,45 @@ public void r2iSaturatesInEveryRun() { " testSuccess()"); } + /** + * Jass reads a real literal's integer part as a 32-bit integer (measured on the 3.0.0 client: the + * literal 3000000000. is negative, 10000000000. reads as 1410065408), and truncates one past 2^24 + * (16777219. reads as 16777218). So the Jass backend prints the float the literal means: its exact + * digits below 2^31, and above that a product of exact factors, which Jass multiplies exactly. + * The test Lua computes in double, where 16777219. is exact, so only the Jass shows that one. + */ + @Test + public void largeRealLiteralsMeanTheSameFloatInEveryRun() throws IOException { + test().testLua(true).luaOnly(false).executeProg().lines( + "package Test", + "native testSuccess()", + "@extern native R2I(real r) returns int", + "@noinline function w(real x) returns real", + " return x", + "function same(real a, real b) returns boolean", + " return not (a != b)", + "init", + " let big = w(30000.) * w(100000.)", + " let tenTo10 = w(100000.) * w(100000.)", + " if same(3000000000., big) and same(-3000000000., -big) and 3000000000. > 0.", + " and same(w(6000000000.) / 3000000000., 2.) and same(w(1.) - 3000000000., 1. - big)", + " and same(10000000000., tenTo10) and w(16777219.) > 0.", + " and same(100000000000000000000., tenTo10 * tenTo10)", + // the stdlib's REAL_MAX: past the largest float, so infinity to a float, and finite to the test Lua + " and 340282366920938000000000000000000000000. > w(3000.) * w(100000.) * tenTo10 * tenTo10 * tenTo10", + " and -340282366920938000000000000000000000000. < -(w(3000.) * w(100000.) * tenTo10 * tenTo10 * tenTo10)", + " and R2I(3000000000.) == 2147483647 and R2I(-3000000000.) == -2147483648", + " testSuccess()"); + String jass = compiledJass("largeRealLiteralsMeanTheSameFloatInEveryRun"); + java.util.regex.Matcher literal = java.util.regex.Pattern.compile("(?