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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,7 @@ public <T extends Element> 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;
Expand Down Expand Up @@ -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
Expand All @@ -272,8 +269,8 @@ public List<ImFunction> pinnedFunctions() {
List<ImFunction> 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}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ public void visit(ImOperatorCall call) {
*/
private static void lowerRealToInt(ImProg prog, ImTranslator translator) {
ImFunction[] shim = {null};
List<ImFunction> added = new ArrayList<>();
prog.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImFunctionCall call) {
Expand All @@ -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<ImFunction> 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<ImFunction> 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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Comment thread
Frotty marked this conversation as resolved.
translator.luaModRealFunc = modReal;
created.add(modReal);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)))))),
Comment thread
Frotty marked this conversation as resolved.
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading