From fa15fe8741aa18ee4b03f970d6874b281cbd20ec Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 31 Jul 2026 12:54:49 +0200 Subject: [PATCH] Fix plain Jass modulo semantics --- de.peeeq.wurstscript/parserspec/jass.parseq | 1 + .../antlr/de/peeeq/wurstscript/antlr/Jass.g4 | 2 +- .../jassinterpreter/JassInterpreter.java | 7 +++ .../providers/LuaEnsureTypeProvider.java | 3 +- .../de/peeeq/wurstscript/WurstOperator.java | 11 +++++ .../wurstscript/attributes/AttrExprType.java | 1 + .../attributes/prettyPrint/PrettyPrinter.java | 1 + .../optimizer/ConstantAndCopyPropagation.java | 1 + .../optimizer/SimpleRewrites.java | 6 +++ .../jass/AntlrJassParseTreeTransformer.java | 4 +- .../wurstscript/jassprinter/JassPrinter.java | 5 ++ .../wurstscript/jassprinter/OpPrinter.java | 4 ++ .../translation/imoptimizer/ImOptimizer.java | 3 +- .../translation/imtojass/ImAttrType.java | 1 + .../imtranslation/LuaNativeLowering.java | 8 +++- .../lua/translation/ExprTranslation.java | 5 +- .../tests/wurstscript/tests/BugTests.java | 48 +++++++++++++++++++ 17 files changed, 103 insertions(+), 8 deletions(-) diff --git a/de.peeeq.wurstscript/parserspec/jass.parseq b/de.peeeq.wurstscript/parserspec/jass.parseq index f24b6ebf1..67c179a43 100644 --- a/de.peeeq.wurstscript/parserspec/jass.parseq +++ b/de.peeeq.wurstscript/parserspec/jass.parseq @@ -75,6 +75,7 @@ JassOpBinary = | JassOpMinus() | JassOpMult() | JassOpDiv() + | JassOpMod() JassOpUnary = JassOpNot() diff --git a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Jass.g4 b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Jass.g4 index 11090aca7..c62e8c182 100644 --- a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Jass.g4 +++ b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Jass.g4 @@ -163,7 +163,7 @@ PLUS: '+'; MINUS: '-'; MULT: '*'; DIV_REAL: '/'; -MOD_REAL: '%'; +MOD_INT: '%'; PAREN_LEFT: '('; PAREN_RIGHT: ')'; BRACKET_LEFT: '['; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java index f0026501c..6e3c574a0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java @@ -2,6 +2,7 @@ import com.google.common.collect.Maps; import de.peeeq.wurstscript.WLogger; +import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.intermediatelang.*; import de.peeeq.wurstscript.intermediatelang.interpreter.AbstractInterpreter; import de.peeeq.wurstscript.intermediatelang.interpreter.TimerMockHandler; @@ -310,6 +311,12 @@ public ILconst case_JassOpDiv(JassOpDiv jassOpDiv) { return getLeftNum().div(getRightNum()); } + @Override + public ILconst case_JassOpMod(JassOpMod jassOpMod) { + return new ILconstInt(WurstOperator.jassModuloInteger( + ((ILconstInt) getLeft()).getVal(), ((ILconstInt) getRight()).getVal())); + } + @Override public ILconst case_JassOpLess(JassOpLess jassOpLess) { return getLeftNum().less(getRightNum()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java index 6fe1720ff..c4bab136b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java @@ -1,5 +1,6 @@ package de.peeeq.wurstio.jassinterpreter.providers; +import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.intermediatelang.ILconstInt; import de.peeeq.wurstscript.intermediatelang.ILconstReal; import de.peeeq.wurstscript.intermediatelang.ILconstString; @@ -50,7 +51,7 @@ public ILconstInt __wurst_rawFloorDivInt(ILconstInt a, ILconstInt b) { } public ILconstInt __wurst_rawFmodInt(ILconstInt a, ILconstInt b) { - return ILconstInt.create(a.getVal() % b.getVal()); + return ILconstInt.create(WurstOperator.jassModuloInteger(a.getVal(), b.getVal())); } public ILconstReal __wurst_rawFmodReal(ILconstReal a, ILconstReal b) { 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 5a641465a..d9e27d8b3 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 @@ -27,6 +27,7 @@ public enum WurstOperator { DIV_INT("div", 2), MOD_REAL("%", 2), MOD_INT("mod", 2), + JASS_MOD_INT("%", 2), NOT("not", 1), UNARY_MINUS("-", 1); @@ -75,6 +76,8 @@ public JassOpBinary jassTranslateBinary() { case MOD_INT: case MOD_REAL: throw new Error("Cannot translate modulo"); + case JASS_MOD_INT: + return JassAst.JassOpMod(); case MULT: return JassAst.JassOpMult(); case NOTEQ: @@ -110,6 +113,7 @@ public LuaOpBinary luaTranslateBinary() { case MOD_REAL: return LuaAst.LuaOpMod(); case MOD_INT: + case JASS_MOD_INT: throw new Error("Cannot translate modulo int"); case MULT: return LuaAst.LuaOpMult(); @@ -150,6 +154,8 @@ public ILconst evaluateBinaryOperator(ILconst left, return ((ILconstNum) left).sub((ILconstNum) right.get()); case MOD_INT: return new ILconstInt(moduloInteger(((ILconstInt) left).getVal(), ((ILconstInt) right.get()).getVal())); + case JASS_MOD_INT: + return new ILconstInt(jassModuloInteger(((ILconstInt) left).getVal(), ((ILconstInt) right.get()).getVal())); case MOD_REAL: return new ILconstReal(moduloReal(getReal(left), getReal(right.get()))); case MULT: @@ -180,6 +186,11 @@ public static int moduloInteger(int a, int b) { return r; } + /** Native Jass {@code %}: integer-only remainder truncated toward zero. */ + public static int jassModuloInteger(int a, int b) { + return a % b; + } + /** Reference semantics for Wurst's real {@code mod}; see {@link #moduloInteger}. */ public static float moduloReal(float a, float b) { float r = a % b; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java index 13a9f04fc..45d8148f3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java @@ -315,6 +315,7 @@ public static WurstType calculate(final ExprBinary term) { "operands " + leftType + " and " + rightType); return WurstTypeUnknown.instance(); case MOD_INT: + case JASS_MOD_INT: case DIV_INT: if (leftType.isSubtypeOf(WurstTypeInt.instance(), term) && rightType.isSubtypeOf(WurstTypeInt.instance(), term)) { return leftType; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java index 082854a15..995cc37db 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java @@ -236,6 +236,7 @@ public static int precedence(WurstOperator op) { case DIV_REAL: case MOD_INT: case MOD_REAL: + case JASS_MOD_INT: return 4; case PLUS: diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java index e0319732b..345f8385f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java @@ -486,6 +486,7 @@ private void analyzeComponent(List scc, Map knowledge) { case MULT: return JassIm.ImIntVal(l * r); case DIV_INT: if (r != 0) return JassIm.ImIntVal(l / r); break; case MOD_INT: if (r != 0) return JassIm.ImIntVal(WurstOperator.moduloInteger(l, r)); break; + case JASS_MOD_INT: if (r != 0) return JassIm.ImIntVal(WurstOperator.jassModuloInteger(l, r)); break; // IMPORTANT: Return ImBoolVal for comparisons, not ImIntVal! case EQ: return JassIm.ImBoolVal(l == r); case NOTEQ: return JassIm.ImBoolVal(l != r); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SimpleRewrites.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SimpleRewrites.java index 37d028906..3ef52b578 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SimpleRewrites.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SimpleRewrites.java @@ -551,6 +551,12 @@ private boolean optimizeIntInt(ImOperatorCall opc, boolean wasViable, ImIntVal l isArithmetic = true; } break; + case JASS_MOD_INT: + if (i2 != 0) { + resultVal = WurstOperator.jassModuloInteger(i1, i2); + isArithmetic = true; + } + break; case MOD_REAL: { float f1 = i1; float f2 = i2; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java index a3d4c8771..4e85892b6 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java @@ -368,8 +368,8 @@ private WurstOperator transformOp(Token op) { return WurstOperator.MULT; case JassParser.DIV_REAL: return WurstOperator.DIV_REAL; - case JassParser.MOD_REAL: - return WurstOperator.MOD_REAL; + case JassParser.MOD_INT: + return WurstOperator.JASS_MOD_INT; case JassParser.NOT: return WurstOperator.NOT; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/JassPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/JassPrinter.java index 220902c6e..abb48cf7e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/JassPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/JassPrinter.java @@ -244,6 +244,11 @@ public Integer case_JassOpDiv(@SuppressWarnings("null") JassOpDiv jassOpDiv) { return 4; } + @Override + public Integer case_JassOpMod(@SuppressWarnings("null") JassOpMod jassOpMod) { + return 4; + } + @Override public Integer case_JassOpLess(@SuppressWarnings("null") JassOpLess jassOpLess) { return 2; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/OpPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/OpPrinter.java index 9399c8571..f362ab378 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/OpPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jassprinter/OpPrinter.java @@ -77,6 +77,10 @@ public static String asString(JassOpDiv jassOpDiv) { return "/"; } + public static String asString(JassOpMod jassOpMod) { + return "%"; + } + public static String asString(JassOpAnd jassOpAnd) { return "and"; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index 5bc892cd9..f9c707d6e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -260,7 +260,8 @@ private boolean mayTrapAtRuntime(Element elem, Map function if (elem instanceof ImOperatorCall) { ImOperatorCall opCall = (ImOperatorCall) elem; WurstOperator op = opCall.getOp(); - if ((op == WurstOperator.DIV_INT || op == WurstOperator.MOD_INT) && opCall.getArguments().size() >= 2) { + if ((op == WurstOperator.DIV_INT || op == WurstOperator.MOD_INT || op == WurstOperator.JASS_MOD_INT) + && opCall.getArguments().size() >= 2) { ImExpr denominator = opCall.getArguments().get(1); // Preserve integer div/mod unless denominator is provably non-zero. if (!(denominator instanceof ImIntVal) || ((ImIntVal) denominator).getValI() == 0) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImAttrType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImAttrType.java index 1c485bb6c..5807e00ed 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImAttrType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImAttrType.java @@ -66,6 +66,7 @@ public static ImType getType(ImOperatorCall e) { return WurstTypeReal.instance().imTranslateType(); case DIV_INT: case MOD_INT: + case JASS_MOD_INT: return WurstTypeInt.instance().imTranslateType(); case AND: case OR: diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index b0736dfb3..5116190de 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -242,7 +242,7 @@ public void visit(ImOperatorCall call) { private static final de.peeeq.wurstscript.ast.Element SYNTHETIC_TRACE = de.peeeq.wurstscript.ast.Ast.NoExpr(); /** - * Rewrites {@code DIV_INT}/{@code MOD_INT}/{@code MOD_REAL} operator calls + * Rewrites {@code DIV_INT}/{@code MOD_INT}/{@code MOD_REAL}/{@code JASS_MOD_INT} operator calls * into calls against small, portable IM functions (not natives), instead * of them being lowered directly to opaque, always-emitted Lua helper * functions at Lua-emission time (after {@code ImOptimizer} has already @@ -287,6 +287,8 @@ public void visit(ImOperatorCall call) { target = funcs.modInt(); } else if (call.getOp() == WurstOperator.MOD_REAL) { target = funcs.modReal(); + } else if (call.getOp() == WurstOperator.JASS_MOD_INT) { + target = funcs.jassModInt(); } else { return; } @@ -439,6 +441,10 @@ ImFunction modReal() { return modReal; } + ImFunction jassModInt() { + return rawFmodInt(); + } + private ImFunction rawFloorDivInt() { if (rawFloorDivInt == null) { rawFloorDivInt = rawNative("__wurst_rawFloorDivInt", TypesHelper.imInt()); 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 61071e042..035e311d2 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 @@ -219,8 +219,9 @@ public static LuaExpr translate(ImOperatorCall e, LuaTranslator tr) { } else if (e.getOp() == WurstOperator.NOTEQ) { return LuaAst.LuaExprUnary(LuaAst.LuaOpNot(), translateEquals(left, right, tr)); } - if (e.getOp() == WurstOperator.MOD_INT || e.getOp() == WurstOperator.MOD_REAL || e.getOp() == WurstOperator.DIV_INT) { - // LuaNativeLowering.lowerDivMod rewrites every DIV_INT/MOD_INT/MOD_REAL + if (e.getOp() == WurstOperator.MOD_INT || e.getOp() == WurstOperator.MOD_REAL + || e.getOp() == WurstOperator.JASS_MOD_INT || e.getOp() == WurstOperator.DIV_INT) { + // LuaNativeLowering.lowerDivMod rewrites every DIV_INT/MOD_INT/MOD_REAL/JASS_MOD_INT // into a call against a portable IM function before the optimizer runs // (so it can be inlined/constant-folded there). It should never survive // to here - falling through to the default binary-op path below would diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java index 1c5454e87..8262bfa48 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java @@ -118,6 +118,54 @@ public void test_inline_jass_div() { "endpackage"); } + @Test + public void jassModuloUsesTruncatingIntegerRemainder() throws IOException { + test().executeProg().compilationUnits( + compilationUnit("input.j", + "function jassModulo takes integer a, integer b returns integer", + "\treturn a % b", + "endfunction"), + compilationUnit("test.wurst", + "package test", + "\tnative testSuccess()", + "\tinit", + "\t\tif jassModulo(5, 3) == 2 and jassModulo(-5, 3) == -2 and jassModulo(5, -3) == 2 and jassModulo(-5, -3) == -2", + "\t\t\ttestSuccess()", + "endpackage")); + String compiled = Files.toString(new File(TEST_OUTPUT_PATH + + "BugTests_jassModuloUsesTruncatingIntegerRemainder_no_opts.j"), Charsets.UTF_8); + Assert.assertTrue(compiled.contains("return a % b"), compiled); + } + + @Test + public void jassModuloUsesTruncatingIntegerRemainderInLua() throws IOException { + test().testLua(true).executeProg().compilationUnits( + compilationUnit("input.j", + "function jassModulo takes integer a, integer b returns integer", + "\treturn a % b", + "endfunction"), + compilationUnit("test.wurst", + "package test", + "\tnative testSuccess()", + "\tinit", + "\t\tif jassModulo(5, 3) == 2 and jassModulo(-5, 3) == -2 and jassModulo(5, -3) == 2 and jassModulo(-5, -3) == -2", + "\t\t\ttestSuccess()", + "endpackage")); + String compiled = Files.toString(new File(TEST_OUTPUT_PATH + + "lua/BugTests_jassModuloUsesTruncatingIntegerRemainderInLua.lua"), Charsets.UTF_8); + Assert.assertTrue(compiled.contains("return math.fmod(a, b)"), compiled); + } + + @Test + public void jassModuloRejectsRealOperands() { + test().executeProg(false) + .expectError("Operator % is not defined for operands real and real") + .compilationUnits(compilationUnit("input.j", + "function jassModulo takes real a, real b returns real", + "\treturn a % b", + "endfunction")); + } + @Test public void test_init_order_jass_warning() { testAssertErrorsLines(false, "Variable b may not have been initialized",