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 @@ -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
Expand All @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -229,19 +229,32 @@ 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;
}
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;
Comment thread
Frotty marked this conversation as resolved.
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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\")",
Expand All @@ -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), \"\")",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Comment thread
Frotty marked this conversation as resolved.
"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(
Expand Down
Loading
Loading