Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("(?<![\\w.])(\\d+)\\.").matcher(jass);
while (literal.find()) {
assertTrue("Jass reads " + literal.group() + " wrapped:\n" + jass,
new java.math.BigInteger(literal.group(1)).bitLength() <= 31);
}
assertFalse("Jass truncates 16777219.:\n" + jass, jass.contains("16777219."));
assertTrue("the float 16777219. means is printed:\n" + jass, jass.contains("16777220."));
}

@Test
public void realModIsModuloRealsFormulaInEveryRun() {
test().testLua(true).luaOnly(false).withStdLib().executeProg().lines(
Expand Down
2 changes: 1 addition & 1 deletion docs/WC3_RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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` 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** `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, `10000000000.` reads as 1410065408 and `1e20` written out as 1661992960, which is 10^20 modulo 2^32. Lua reads such a literal correctly. So the Jass backend prints the float a literal means wherever Jass would read it otherwise (`imtojass.ExprTranslation.realLiteral`): past 2^24 the float's own digits (`16777219.` becomes `16777220.`), and from 2^31 on a product of exact factors, its odd part and powers of two up to 2^30 (`3000000000.` becomes `5859375. * 512.`), which reads as the literal's float in the game. A literal past the largest float, such as the stdlib's `REAL_MAX` (2^128), is printed as the largest float: Jass has no product which gives infinity (2^150 compares below 3e38 and prints as `0.000`), and the largest float compares above 3e38 (its `R2S` is `inf`). A compile-time real is written as a plain decimal, since Jass parses no exponent.
* **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.

Expand Down
Loading