From 9ddbb08f785c82751506bdcbb59836606f90d548 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 15:55:53 +0200 Subject: [PATCH 01/22] Optimize Lua type assurance boundaries --- .../imtranslation/ExprTranslation.java | 77 +++++++++- .../imtranslation/LuaEnsureFunctions.java | 11 +- .../imtranslation/LuaNativeLowering.java | 88 +++++++----- .../tests/LuaBackendAuditTests.java | 134 +++++++++++++++--- .../tests/LuaTranslationTests.java | 10 +- 5 files changed, 250 insertions(+), 70 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 5420899d0..3a843797b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -104,8 +104,10 @@ private static ImExpr wrapTranslation(Expr e, ImTranslator t, ImExpr translated) } static ImExpr wrapLua(Element trace, ImTranslator t, ImExpr translated, WurstType actualType) { - // use ensureType functions for lua - // these functions convert nil to the default value for primitive types (int, string, bool, real) + // Erased generic values are the one kind of Wurst value which can lose + // its primitive default when represented in Lua. Keep the + // normalization available to callers which explicitly cross an + // external boundary; ordinary Wurst expressions must not pay for it. if (t.isLuaTarget() && actualType instanceof WurstTypeBoundTypeParam) { WurstTypeBoundTypeParam wtb = (WurstTypeBoundTypeParam) actualType; @@ -125,6 +127,13 @@ static ImExpr wrapLua(Element trace, ImTranslator t, ImExpr translated, WurstTyp break; } if(ensureType != null) { + // Lua already has the exact cheap operation needed for the + // boolean case. Besides being faster than a helper call, + // this also turns every non-nil value into a real boolean. + if (ensureType == t.ensureBoolFunc) { + return ImOperatorCall(WurstOperator.NOTEQ, ImExprs( + translated, ImNull(ImAnyType()))); + } return ImFunctionCall(trace, ensureType, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL); } } @@ -168,7 +177,10 @@ static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, // System.out.println(" --> toIndex"); return wrapLua(trace, t, ImFunctionCall(trace, toIndex, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL), actualType); } - return wrapLua(trace, t, translated, actualType); + // Do not normalize every generic expression. The Lua backend only + // needs this at an external/native boundary (or before the legacy + // index conversion handled above). + return translated; } public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) { @@ -659,8 +671,14 @@ && isCalledOnDynamicRef(e) + " -> dynamicDispatch=" + dynamicDispatch); } + ImFunction directFunc = null; + if (!dynamicDispatch && !(calledFunc instanceof TupleDef)) { + directFunc = t.getFuncFor(calledFunc); + } + ImExpr receiver = leftExpr == null ? null : leftExpr.imTranslateExpr(t, f); - ImExprs imArgs = translateExprs(arguments, t, f); + boolean normalizeAtBoundary = directFunc != null && isLuaExternalBoundary(directFunc); + ImExprs imArgs = translateExprs(arguments, t, f, normalizeAtBoundary); if (calledFunc instanceof TupleDef) { // creating a new tuple... @@ -686,7 +704,7 @@ && isCalledOnDynamicRef(e) t, e.attrFunctionSignature(), e, method.getImplementation().getTypeVariables()); call = ImMethodCall(e, method, typeArguments, receiver, imArgs, false); } else { - ImFunction calledImFunc = t.getFuncFor(calledFunc); + ImFunction calledImFunc = directFunc; if (receiver != null) { imArgs.add(0, receiver); } @@ -784,13 +802,60 @@ private static boolean isCalledOnDynamicRef(FunctionCall e) { } private static ImExprs translateExprs(List arguments, ImTranslator t, ImFunction f) { + return translateExprs(arguments, t, f, false); + } + + private static ImExprs translateExprs(List arguments, ImTranslator t, ImFunction f, + boolean externalBoundary) { ImExprs result = ImExprs(); for (Expr e : arguments) { - result.add(e.imTranslateExpr(t, f)); + ImExpr translated = e.imTranslateExpr(t, f); + if (externalBoundary) { + translated = wrapLuaAtExternalBoundary(e, t, translated); + } + result.add(translated); } return result; } + private static boolean isLuaExternalBoundary(ImFunction function) { + return function.isNative() || function.isBj() || function.isExtern(); + } + + private static ImExpr wrapLuaAtExternalBoundary(Expr source, ImTranslator t, ImExpr translated) { + WurstType actualType = source.attrTypRaw(); + // Ordinary Wurst locals and literals already have their normal Lua + // representation. Only values which can lose their primitive default + // in Lua need normalization: erased generic values and raw array + // reads crossing into untyped code. + if (!(actualType instanceof WurstTypeBoundTypeParam) + && !(translated instanceof ImVarArrayAccess)) { + return translated; + } + if (actualType instanceof WurstTypeBoundTypeParam) { + return wrapLua(source, t, translated, actualType); + } + WurstType normalized = actualType.normalize(); + ImFunction ensureType = null; + if (normalized instanceof WurstTypeInt) { + ensureType = t.ensureIntFunc; + } else if (normalized instanceof WurstTypeBool) { + ensureType = t.ensureBoolFunc; + } else if (normalized instanceof WurstTypeReal) { + ensureType = t.ensureRealFunc; + } else if (normalized instanceof WurstTypeString) { + ensureType = t.ensureStrFunc; + } + if (ensureType == null) { + return translated; + } + if (ensureType == t.ensureBoolFunc) { + return ImOperatorCall(WurstOperator.NOTEQ, ImExprs( + translated, ImNull(ImAnyType()))); + } + return ImFunctionCall(source, ensureType, ImTypeArguments(), ImExprs(translated), false, CallType.NORMAL); + } + public static ImExpr translateIntern(ExprIncomplete e, ImTranslator t, ImFunction f) { throw new CompileError(e.getSource(), "Incomplete expression."); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java index a7467b44e..bc67fa6bf 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java @@ -71,21 +71,16 @@ static ImFunction buildEnsureInt(List out) { return f; } - /** local result = false; if x ~= nil then result = x end; return result */ + /** return x ~= nil; this is emitted as a cheap boolean expression in Lua. */ static ImFunction buildEnsureBool(List out) { ImType boolType = TypesHelper.imBool(); ImVar x = JassIm.ImVar(TRACE, boolType.copy(), "x", false); - ImVar result = JassIm.ImVar(TRACE, boolType.copy(), "result", false); ImStmts body = JassIm.ImStmts( - JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImBoolVal(false)), - JassIm.ImIf(TRACE, notNull(x), - JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(x))), - JassIm.ImStmts()), - JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result)) + JassIm.ImReturn(TRACE, notNull(x)) ); ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureBool", JassIm.ImTypeVars(), JassIm.ImVars(x), boolType.copy(), - JassIm.ImVars(result), body, Collections.emptyList()); + JassIm.ImVars(), body, Collections.emptyList()); out.add(f); return f; } 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 5116190de..f5a5bed97 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 @@ -127,7 +127,7 @@ public static void transform(ImProg prog, ImTranslator translator) { lowerStringConcatenation(prog, translator); lowerDivMod(prog); - lowerPrimitiveArrayEnsure(prog, translator); + lowerPrimitiveArrayBoundaryEnsure(prog, translator); // Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper). // Populated lazily during the traversal. @@ -334,40 +334,6 @@ private static boolean isIntentionalThreadAbortDivByZero(ImOperatorCall call) { && "I2S".equals(parentCall.getFunc().getName()); } - /** - * Rewrites reads (never writes - see {@link LValues#isUsedAsLValue}) of - * primitive-typed ({@code int}/{@code bool}/{@code real}/{@code string}) - * array slots into calls against the portable {@code ensureXxx} IM - * functions ({@link ImTranslator#ensureIntFunc} and friends), instead of - * that normalization being applied later as opaque, always-emitted Lua - * source at Lua-emission time. Same treatment as {@link #lowerDivMod}: - * this makes a hot read optimizable (inlinable, foldable) instead of a - * fixed per-read function-call cost, and lets the helper disappear - * entirely from programs whose arrays are never read this way. - * - *

The shared per-type array-default metatable (see {@code - * LuaTranslator#newDefaultArray}) already guarantees a typed, non-nil - * default on every miss, so this remains defensive hardening against - * values written from outside typed Wurst code, not a correctness - * requirement for pure Wurst-authored programs. - */ - private static void lowerPrimitiveArrayEnsure(ImProg prog, ImTranslator translator) { - prog.accept(new Element.DefaultVisitor() { - @Override - public void visit(ImVarArrayAccess access) { - super.visit(access); - if (LValues.isUsedAsLValue(access)) { - return; - } - ImFunction ensureFunc = ensureFunctionFor(access.attrTyp(), translator); - if (ensureFunc == null) { - return; - } - access.replaceBy(callWithStacktrace(access.attrTrace(), ensureFunc, JassIm.ImExprs(access.copy()))); - } - }); - } - private static ImFunctionCall callWithStacktrace(de.peeeq.wurstscript.ast.Element trace, ImFunction f, ImExprs args) { int stacktraceIndex = stacktraceParamIndex(f); if (stacktraceIndex >= 0) { @@ -386,6 +352,58 @@ private static int stacktraceParamIndex(ImFunction f) { return -1; } + /** + * Normalizes primitive array reads only when they enter code outside the + * typed Wurst world. Lua's array metatables already provide Wurst + * defaults for ordinary reads, so doing this at every read is redundant; + * a native/BJ/extern call is the point where an untyped value must be + * made safe for the callee. + */ + private static void lowerPrimitiveArrayBoundaryEnsure(ImProg prog, ImTranslator translator) { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + ImFunction function = call.getFunc(); + if (!isExternalBoundary(function)) { + return; + } + for (ImExpr argument : new ArrayList<>(call.getArguments())) { + if (!(argument instanceof ImVarArrayAccess) + || isAlreadyNormalized(argument, translator)) { + continue; + } + ImFunction ensure = ensureFunctionFor(argument.attrTyp(), translator); + if (ensure == null) { + continue; + } + ImExpr normalized; + if (ensure == translator.ensureBoolFunc) { + normalized = JassIm.ImOperatorCall(WurstOperator.NOTEQ, + JassIm.ImExprs(argument.copy(), JassIm.ImNull(JassIm.ImAnyType()))); + } else { + normalized = JassIm.ImFunctionCall(call.attrTrace(), ensure, + JassIm.ImTypeArguments(), JassIm.ImExprs(argument.copy()), false, CallType.NORMAL); + } + argument.replaceBy(normalized); + } + } + }); + } + + private static boolean isExternalBoundary(ImFunction function) { + return !function.getName().startsWith("__wurst_") + && (function.isNative() || function.isBj() || function.isExtern()); + } + + private static boolean isAlreadyNormalized(ImExpr argument, ImTranslator translator) { + return argument instanceof ImFunctionCall + && (((ImFunctionCall) argument).getFunc() == translator.ensureIntFunc + || ((ImFunctionCall) argument).getFunc() == translator.ensureBoolFunc + || ((ImFunctionCall) argument).getFunc() == translator.ensureRealFunc + || ((ImFunctionCall) argument).getFunc() == translator.ensureStrFunc); + } + private static ImFunction ensureFunctionFor(ImType type, ImTranslator translator) { if (TypesHelper.isIntType(type)) { return translator.ensureIntFunc; 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 7d0a3a15d..269d5d686 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 @@ -2143,7 +2143,7 @@ public void integerDivModReferenceSemanticsInInterpreter() { * ImStringVal(""). If their own "x ~= nil" checks were tagged with the * string type, that rewrite would silently turn them into "x ~= \"\"", * so a genuinely nil Lua value (e.g. an unset bound-generic string - * field) would read as "not nil", skip normalization, and come out as + * array slot) would read as "not nil", skip normalization, and come out as * the literal string "nil" via tostring() instead of "" - or, for * stringConcat, get passed straight into raw ".." concatenation. * LuaEnsureFunctions#notNull tags its ImNull sentinel with ImAnyType @@ -2154,11 +2154,13 @@ public void ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes() throws test().testLua(true).executeProg().lines( "package Test", "native testSuccess()", + "native print(string value)", "string array names", "function join(string a, string b) returns string", " return a + b", "init", " if names[5] == \"\" and join(\"a\", \"b\") == \"ab\"", + " print(names[5])", " testSuccess()" ); String compiled = compiledLua("ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes"); @@ -2166,6 +2168,109 @@ public void ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes() throws assertNilCheckNotCorruptedToEmptyStringCheck(compiled, "__wurst_stringConcat("); } + @Test + public void genericNormalizationIsKeptAtNativeBoundaryOnly() { + String compiled = compileLuaWithRunArgs( + "LuaBackendAuditTests_genericNormalizationIsKeptAtNativeBoundaryOnly", + new RunArgs().with("-lua"), + "package Test", + "native print(string value)", + "native consumeBool(bool value)", + "string array values", + "function identity(T value) returns T", + " return value", + "function forward(T value) returns T", + " return identity(value)", + "init", + " print(forward(\"value\"))", + " consumeBool(forward(false))", + " print(values[1])" + ); + + assertFunctionBodyContains(compiled, "forward", "__wurst_ensure", false); + assertTrue("boolean normalization should be a direct nil comparison:\n" + compiled, + compiled.contains("consumeBool(not((forward(false) == nil)))")); + assertFalse("boolean normalization must not call the ensure helper", + compiled.contains("__wurst_ensureBool(forward(false))")); + assertTrue("primitive array reads crossing a native boundary must be normalized:\n" + compiled, + compiled.contains("__wurst_ensureStr(Test_values[1])")); + } + + /** + * Seeded boundary corpus for the type-assurance change. Each case varies + * the primitive type, literal value, and array slot while checking the two + * unsafe paths independently: erased generic propagation and a raw array + * read. The intermediate generic functions and an internal array reader + * must stay free of assurance calls, while the native call sites must have + * the appropriate normalization. This is intentionally compile-only: the + * generated native sinks have no Warcraft runtime implementation. + */ + @Test + public void seededTypeAssuranceBoundaryFuzz() { + Random random = new Random(0x7A55_BA5EL); + String[] types = {"int", "bool", "real", "string"}; + String[] suffixes = {"Int", "Bool", "Real", "Str"}; + for (int caseIndex = 0; caseIndex < 32; caseIndex++) { + int typeIndex = (caseIndex + random.nextInt(types.length)) % types.length; + String type = types[typeIndex]; + String suffix = suffixes[typeIndex]; + int arrayIndex = random.nextInt(16) + 1; + String literal = switch (type) { + case "int" -> Integer.toString(random.nextInt(51)); + case "bool" -> random.nextBoolean() ? "true" : "false"; + case "real" -> random.nextInt(51) + ".5"; + case "string" -> "\"fuzz_" + caseIndex + "\""; + default -> throw new AssertionError(type); + }; + String sink = "consume" + suffix; + String testName = "LuaBackendAuditTests_seededTypeAssuranceBoundaryFuzz_" + caseIndex; + String compiled = compileLuaWithRunArgs( + testName, + new RunArgs().with("-lua"), + "package TypeAssuranceFuzz", + "native " + sink + "(" + type + " value)", + type + " array values", + "function identity(T value) returns T", + " return value", + "function forward(T value) returns T", + " return identity(value)", + "function read() returns " + type, + " return values[" + arrayIndex + "]", + "init", + " " + sink + "(forward<" + type + ">(" + literal + "))", + " " + sink + "(values[" + arrayIndex + "])", + " " + sink + "(read())", + " " + sink + "(" + literal + ")" + ); + + assertFunctionBodyContains(compiled, "forward", "__wurst_ensure", false); + assertFunctionBodyContains(compiled, "read", "__wurst_ensure", false); + String genericArgument = type.equals("bool") + ? "not((forward(" + literal + ") == nil))" + : "__wurst_ensure" + suffix + "(forward(" + literal + "))"; + assertTrue("generic boundary case " + caseIndex + " was not normalized:\n" + compiled, + compiled.contains(sink + "(" + genericArgument + ")")); + String arrayArgument = type.equals("bool") + ? "not((TypeAssuranceFuzz_values[" + arrayIndex + "] == nil))" + : "__wurst_ensure" + suffix + "(TypeAssuranceFuzz_values[" + arrayIndex + "])"; + assertTrue("array boundary case " + caseIndex + " was not normalized:\n" + compiled, + compiled.contains(sink + "(" + arrayArgument + ")")); + assertTrue("ordinary typed values must not be normalized at the boundary:\n" + compiled, + compiled.contains(sink + "(" + literal + ")")); + } + } + + private static void assertFunctionBodyContains(String compiled, String functionName, + String text, boolean expected) { + int start = compiled.indexOf("function " + functionName + "("); + assertTrue("expected function " + functionName, start >= 0); + int end = compiled.indexOf("\nend", start); + assertTrue("unterminated function " + functionName, end >= 0); + boolean found = compiled.substring(start, end).contains(text); + assertEquals("unexpected occurrence of " + text + " in " + functionName, + expected, found); + } + private void assertNilCheckNotCorruptedToEmptyStringCheck(String compiled, String functionNamePrefix) { int fnStart = compiled.indexOf("function " + functionNamePrefix); assertTrue("expected " + functionNamePrefix + " to be present", fnStart >= 0); @@ -2368,18 +2473,14 @@ public void optimizedMovedImHelpersHaveNoDanglingReferences() { "native print(string message)", "native I2S(int value) returns string", "native R2S(real value) returns string", + "native consumeInt(int value)", + "native consumeBool(bool value)", + "native consumeReal(real value)", + "native consumeString(string value)", "int array ints", "bool array bools", "real array reals", "string array strings", - "function readInt(int index) returns int", - " return ints[index]", - "function readBool(int index) returns bool", - " return bools[index]", - "function readReal(int index) returns real", - " return reals[index]", - "function readString(int index) returns string", - " return strings[index]", "function intDiv(int a, int b) returns int", " return a div b", "function intMod(int a, int b) returns int", @@ -2387,14 +2488,13 @@ public void optimizedMovedImHelpersHaveNoDanglingReferences() { "function realMod(real a, real b) returns real", " return a % b", "init", - " ints[1] = 7", - " bools[1] = true", - " reals[1] = 7.5", - " strings[1] = \"value=\"", - " if readBool(1)", - " print(readString(1) + I2S(intDiv(readInt(1), 2)))", - " print(I2S(intMod(readInt(1), 2)))", - " print(R2S(realMod(readReal(1), 2.)))" + " consumeInt(ints[1])", + " consumeBool(bools[1])", + " consumeReal(reals[1])", + " consumeString(strings[1])", + " print(\"value=\" + I2S(intDiv(7, 2)))", + " print(I2S(intMod(7, 2)))", + " print(R2S(realMod(7.5, 2.)))" ); String[] helperNames = { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index 1ca1e2183..20728204d 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -503,7 +503,7 @@ public void lazyGenericClosureDispatchWorksInLua() throws IOException { } @Test - public void stringArrayReadIsEnsured() throws IOException { + public void stringArrayReadIsEnsuredAtNativeBoundary() throws IOException { test().testLua(true).withStdLib().lines( "package Test", "string array playerName", @@ -511,8 +511,9 @@ public void stringArrayReadIsEnsured() throws IOException { " let i = 0", " SetPlayerName(Player(i), playerName[i])" ); - String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsEnsured.lua"), Charsets.UTF_8); - assertContainsRegex(compiled, "SetPlayerName\\(Player\\([^\\)]*\\),\\s*__wurst_ensureStr\\("); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsEnsuredAtNativeBoundary.lua"), Charsets.UTF_8); + assertTrue("native boundary must normalize an array read", + compiled.contains("__wurst_ensureStr(Test_playerName[")); } @Test @@ -1534,7 +1535,8 @@ public void newGenericsStringFieldAssignmentRoundTripsInLua() throws IOException ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsStringFieldAssignmentRoundTripsInLua.lua"), Charsets.UTF_8); assertFunctionBodyContains(compiled, "testGenericStringField", "C_x_storage[c] = \"42\"", true); - assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr(C_x_storage[c])", true); + assertFunctionBodyContains(compiled, "testGenericStringField", "C_x_storage[c]", true); + assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr", false); assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringToIndex", false); assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringFromIndex", false); } From d58a6d2cec519806990f40c4c5a7f86b7318ad39 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 16:13:28 +0200 Subject: [PATCH 02/22] Preserve generic defaults and boolean values --- .../imtranslation/ExprTranslation.java | 44 ++++++++++++------- .../imtranslation/LuaEnsureFunctions.java | 9 +++- .../tests/LuaBackendAuditTests.java | 27 ++++++++++-- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 3a843797b..18806d252 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -128,11 +128,11 @@ static ImExpr wrapLua(Element trace, ImTranslator t, ImExpr translated, WurstTyp } if(ensureType != null) { // Lua already has the exact cheap operation needed for the - // boolean case. Besides being faster than a helper call, - // this also turns every non-nil value into a real boolean. + // boolean case. Equality with true preserves false while + // mapping nil (and other non-true values) to false. if (ensureType == t.ensureBoolFunc) { - return ImOperatorCall(WurstOperator.NOTEQ, ImExprs( - translated, ImNull(ImAnyType()))); + return ImOperatorCall(WurstOperator.EQ, ImExprs( + translated, ImBoolVal(true))); } return ImFunctionCall(trace, ensureType, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL); } @@ -177,9 +177,16 @@ static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, // System.out.println(" --> toIndex"); return wrapLua(trace, t, ImFunctionCall(trace, toIndex, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL), actualType); } - // Do not normalize every generic expression. The Lua backend only - // needs this at an external/native boundary (or before the legacy - // index conversion handled above). + // Preserve Wurst's primitive defaults when an erased generic value is + // consumed by a concrete primitive expression. Generic-to-generic + // propagation remains raw and is normalized only at its eventual + // concrete/native boundary. + if (actualType instanceof WurstTypeBoundTypeParam + && !(expectedTypRaw instanceof WurstTypeBoundTypeParam) + && !(expectedTypRaw instanceof WurstTypeTypeParam) + && isPrimitiveType(expectedTypRaw)) { + return wrapLua(trace, t, translated, actualType); + } return translated; } @@ -826,15 +833,12 @@ private static ImExpr wrapLuaAtExternalBoundary(Expr source, ImTranslator t, ImE WurstType actualType = source.attrTypRaw(); // Ordinary Wurst locals and literals already have their normal Lua // representation. Only values which can lose their primitive default - // in Lua need normalization: erased generic values and raw array - // reads crossing into untyped code. - if (!(actualType instanceof WurstTypeBoundTypeParam) - && !(translated instanceof ImVarArrayAccess)) { + // in Lua need normalization: raw array reads crossing into untyped + // code. Erased generic values are normalized by wrapTranslation when + // a concrete primitive context consumes them. + if (!(translated instanceof ImVarArrayAccess)) { return translated; } - if (actualType instanceof WurstTypeBoundTypeParam) { - return wrapLua(source, t, translated, actualType); - } WurstType normalized = actualType.normalize(); ImFunction ensureType = null; if (normalized instanceof WurstTypeInt) { @@ -850,12 +854,20 @@ private static ImExpr wrapLuaAtExternalBoundary(Expr source, ImTranslator t, ImE return translated; } if (ensureType == t.ensureBoolFunc) { - return ImOperatorCall(WurstOperator.NOTEQ, ImExprs( - translated, ImNull(ImAnyType()))); + return ImOperatorCall(WurstOperator.EQ, ImExprs( + translated, ImBoolVal(true))); } return ImFunctionCall(source, ensureType, ImTypeArguments(), ImExprs(translated), false, CallType.NORMAL); } + private static boolean isPrimitiveType(WurstType type) { + WurstType normalized = type.normalize(); + return normalized instanceof WurstTypeInt + || normalized instanceof WurstTypeBool + || normalized instanceof WurstTypeReal + || normalized instanceof WurstTypeString; + } + public static ImExpr translateIntern(ExprIncomplete e, ImTranslator t, ImFunction f) { throw new CompileError(e.getSource(), "Incomplete expression."); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java index bc67fa6bf..22101c872 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java @@ -71,13 +71,13 @@ static ImFunction buildEnsureInt(List out) { return f; } - /** return x ~= nil; this is emitted as a cheap boolean expression in Lua. */ + /** return x == true; this preserves false and maps nil to false. */ static ImFunction buildEnsureBool(List out) { ImType boolType = TypesHelper.imBool(); ImVar x = JassIm.ImVar(TRACE, boolType.copy(), "x", false); ImStmts body = JassIm.ImStmts( - JassIm.ImReturn(TRACE, notNull(x)) + JassIm.ImReturn(TRACE, isTrue(x)) ); ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureBool", JassIm.ImTypeVars(), JassIm.ImVars(x), boolType.copy(), JassIm.ImVars(), body, Collections.emptyList()); @@ -191,6 +191,11 @@ private static ImExpr notNull(ImVar v) { return JassIm.ImOperatorCall(WurstOperator.NOTEQ, JassIm.ImExprs(JassIm.ImVarAccess(v), JassIm.ImNull(JassIm.ImAnyType()))); } + private static ImExpr isTrue(ImVar v) { + return JassIm.ImOperatorCall(WurstOperator.EQ, + JassIm.ImExprs(JassIm.ImVarAccess(v), JassIm.ImBoolVal(true))); + } + private static ImFunctionCall call(ImFunction f, ImExpr... args) { return JassIm.ImFunctionCall(TRACE, f, JassIm.ImTypeArguments(), JassIm.ImExprs(args), false, CallType.NORMAL); } 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 269d5d686..f390ff6d9 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 @@ -2188,14 +2188,33 @@ public void genericNormalizationIsKeptAtNativeBoundaryOnly() { ); assertFunctionBodyContains(compiled, "forward", "__wurst_ensure", false); - assertTrue("boolean normalization should be a direct nil comparison:\n" + compiled, - compiled.contains("consumeBool(not((forward(false) == nil)))")); + assertTrue("boolean normalization should be a direct true comparison:\n" + compiled, + compiled.contains("consumeBool((forward(false) == true))")); assertFalse("boolean normalization must not call the ensure helper", compiled.contains("__wurst_ensureBool(forward(false))")); assertTrue("primitive array reads crossing a native boundary must be normalized:\n" + compiled, compiled.contains("__wurst_ensureStr(Test_values[1])")); } + @Test + public void erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Box", + " T value", + " function get() returns T", + " return value", + "init", + " let box = new Box", + " if box.get() + 1 == 1", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse"); + assertTrue("concrete generic use must normalize an erased integer", + compiled.contains("__wurst_ensureInt")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two @@ -2246,12 +2265,12 @@ public void seededTypeAssuranceBoundaryFuzz() { assertFunctionBodyContains(compiled, "forward", "__wurst_ensure", false); assertFunctionBodyContains(compiled, "read", "__wurst_ensure", false); String genericArgument = type.equals("bool") - ? "not((forward(" + literal + ") == nil))" + ? "(forward(" + literal + ") == true)" : "__wurst_ensure" + suffix + "(forward(" + literal + "))"; assertTrue("generic boundary case " + caseIndex + " was not normalized:\n" + compiled, compiled.contains(sink + "(" + genericArgument + ")")); String arrayArgument = type.equals("bool") - ? "not((TypeAssuranceFuzz_values[" + arrayIndex + "] == nil))" + ? "(TypeAssuranceFuzz_values[" + arrayIndex + "] == true)" : "__wurst_ensure" + suffix + "(TypeAssuranceFuzz_values[" + arrayIndex + "])"; assertTrue("array boundary case " + caseIndex + " was not normalized:\n" + compiled, compiled.contains(sink + "(" + arrayArgument + ")")); From 2cf295e36a97d6dd87cf74d68cd3213f53eae342 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 16:14:47 +0200 Subject: [PATCH 03/22] Fix Lua boolean and generic default normalization --- .../test/java/tests/wurstscript/tests/LuaTranslationTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index 20728204d..ccd7b311f 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -1535,8 +1535,7 @@ public void newGenericsStringFieldAssignmentRoundTripsInLua() throws IOException ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsStringFieldAssignmentRoundTripsInLua.lua"), Charsets.UTF_8); assertFunctionBodyContains(compiled, "testGenericStringField", "C_x_storage[c] = \"42\"", true); - assertFunctionBodyContains(compiled, "testGenericStringField", "C_x_storage[c]", true); - assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr", false); + assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr(C_x_storage[c])", true); assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringToIndex", false); assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringFromIndex", false); } From ea8b239c1043f3b14fbd00bf304375ab238ac76e Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 16:34:21 +0200 Subject: [PATCH 04/22] Normalize generic values used as Lua indices --- .../imtranslation/ExprTranslation.java | 16 ++++++++++++++-- .../imtranslation/LuaNativeLowering.java | 4 ++-- .../wurstscript/tests/LuaBackendAuditTests.java | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 18806d252..6eaa544af 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -140,7 +140,19 @@ static ImExpr wrapLua(Element trace, ImTranslator t, ImExpr translated, WurstTyp return translated; } - static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, WurstType actualType, WurstType expectedTypRaw) { + static ImExpr wrapTranslation(Expr e, ImTranslator t, ImExpr translated, WurstType actualType, WurstType expectedTypRaw) { + return wrapTranslation(e, t, translated, actualType, expectedTypRaw, + e.getParent() instanceof Indexes); + } + + static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, + WurstType actualType, WurstType expectedTypRaw) { + return wrapTranslation(trace, t, translated, actualType, expectedTypRaw, false); + } + + private static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, + WurstType actualType, WurstType expectedTypRaw, + boolean indexContext) { ImFunction toIndex = null; ImFunction fromIndex = null; if (actualType instanceof WurstTypeBoundTypeParam) { @@ -184,7 +196,7 @@ static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, if (actualType instanceof WurstTypeBoundTypeParam && !(expectedTypRaw instanceof WurstTypeBoundTypeParam) && !(expectedTypRaw instanceof WurstTypeTypeParam) - && isPrimitiveType(expectedTypRaw)) { + && (isPrimitiveType(expectedTypRaw) || indexContext)) { return wrapLua(trace, t, translated, actualType); } return translated; 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 f5a5bed97..d4672d0fa 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 @@ -379,8 +379,8 @@ public void visit(ImFunctionCall call) { } ImExpr normalized; if (ensure == translator.ensureBoolFunc) { - normalized = JassIm.ImOperatorCall(WurstOperator.NOTEQ, - JassIm.ImExprs(argument.copy(), JassIm.ImNull(JassIm.ImAnyType()))); + normalized = JassIm.ImOperatorCall(WurstOperator.EQ, + JassIm.ImExprs(argument.copy(), JassIm.ImBoolVal(true))); } else { normalized = JassIm.ImFunctionCall(call.attrTrace(), ensure, JassIm.ImTypeArguments(), JassIm.ImExprs(argument.copy()), false, CallType.NORMAL); 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 f390ff6d9..97ef9bc74 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 @@ -2201,12 +2201,14 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse() throws IO test().testLua(true).executeProg().lines( "package Test", "native testSuccess()", + "int array values", "class Box", " T value", " function get() returns T", " return value", "init", " let box = new Box", + " values[box.get()] = 7", " if box.get() + 1 == 1", " testSuccess()" ); From caf7a4eb918c93d4ce2950234d5af9c738318f19 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 16:45:14 +0200 Subject: [PATCH 05/22] Normalize erased generic range bounds --- .../imtranslation/StmtTranslation.java | 17 +++++++++++++++-- .../wurstscript/tests/LuaBackendAuditTests.java | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java index c59c9ce2f..be0fd82b9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java @@ -13,6 +13,7 @@ import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeArray; +import de.peeeq.wurstscript.types.WurstTypeInt; import de.peeeq.wurstscript.types.WurstTypeVararg; import org.eclipse.jdt.annotation.Nullable; @@ -293,8 +294,8 @@ private static ImStmt case_StmtForRange(ImTranslator t, ImFunction f, LocalVarDe List result = Lists.newArrayList(); result.add(ImSet(loopVar, ImVarAccess(imLoopVar), fromExpr)); - ImExpr toExpr = addCacheVariableSmart(t, f, result, to, TypesHelper.imInt()); - ImExpr stepExpr = addCacheVariableSmart(t, f, result, step, TypesHelper.imInt()); + ImExpr toExpr = addCacheVariableSmart(t, f, result, to, TypesHelper.imInt(), WurstTypeInt.instance()); + ImExpr stepExpr = addCacheVariableSmart(t, f, result, step, TypesHelper.imInt(), WurstTypeInt.instance()); ImStmts imBody = ImStmts(); // exitwhen imLoopVar > toExpr @@ -310,6 +311,18 @@ private static ImStmt case_StmtForRange(ImTranslator t, ImFunction f, LocalVarDe private static ImExpr addCacheVariableSmart(ImTranslator t, ImFunction f, List result, Expr toCache, ImType type) { ImExpr r = toCache.imTranslateExpr(t, f); + return addCacheVariableSmart(t, f, result, toCache, type, r); + } + + private static ImExpr addCacheVariableSmart(ImTranslator t, ImFunction f, List result, + Expr toCache, ImType type, WurstType expectedType) { + ImExpr r = toCache.imTranslateExpr(t, f); + r = ExprTranslation.wrapTranslation(toCache, t, r, toCache.attrTypRaw(), expectedType); + return addCacheVariableSmart(t, f, result, toCache, type, r); + } + + private static ImExpr addCacheVariableSmart(ImTranslator t, ImFunction f, List result, + Expr toCache, ImType type, ImExpr r) { if (r instanceof ImConst) { return r; } 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 97ef9bc74..9d36d35a3 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 @@ -2210,7 +2210,10 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse() throws IO " let box = new Box", " values[box.get()] = 7", " if box.get() + 1 == 1", - " testSuccess()" + " testSuccess()", + " for i = 1 to box.get()", + " testSuccess()", + " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse"); assertTrue("concrete generic use must normalize an erased integer", From dcf8da584d38525fc420ef507efb9980f5c5c95e Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 16:57:11 +0200 Subject: [PATCH 06/22] Normalize erased generic switch values --- .../imtranslation/StmtTranslation.java | 16 +++++++++++++++- .../wurstscript/tests/LuaBackendAuditTests.java | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java index be0fd82b9..ba36a3926 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java @@ -14,6 +14,7 @@ import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeArray; import de.peeeq.wurstscript.types.WurstTypeInt; +import de.peeeq.wurstscript.types.WurstTypeIntLiteral; import de.peeeq.wurstscript.types.WurstTypeVararg; import org.eclipse.jdt.annotation.Nullable; @@ -484,7 +485,10 @@ public static ImStmt translate(StmtSkip s, ImTranslator translator, ImFunction f public static ImStmt translate(SwitchStmt switchStmt, ImTranslator t, ImFunction f) { List result = Lists.newArrayList(); ImType type = switchStmt.getExpr().attrTyp().imTranslateType(t); - ImExpr tempVar = addCacheVariableSmart(t, f, result, switchStmt.getExpr(), type); + WurstType expectedType = switchExpectedType(switchStmt); + ImExpr tempVar = expectedType == null + ? addCacheVariableSmart(t, f, result, switchStmt.getExpr(), type) + : addCacheVariableSmart(t, f, result, switchStmt.getExpr(), type, expectedType); // generate ifs // leerer Block: //ImStmts(); @@ -533,6 +537,16 @@ public static ImStmt translate(SwitchStmt switchStmt, ImTranslator t, ImFunction return ImHelper.statementExprVoid(ImStmts(result)); } + private static @Nullable WurstType switchExpectedType(SwitchStmt switchStmt) { + for (SwitchCase switchCase : switchStmt.getCases()) { + for (Expr expression : switchCase.getExpressions()) { + WurstType type = expression.attrTyp(); + return type instanceof WurstTypeIntLiteral ? WurstTypeInt.instance() : type; + } + } + return null; + } + /** * translate the expressions of a switch case to *

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 9d36d35a3..7689adc99 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 @@ -2201,6 +2201,7 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse() throws IO test().testLua(true).executeProg().lines( "package Test", "native testSuccess()", + "native testFail(string message)", "int array values", "class Box", " T value", @@ -2213,6 +2214,11 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse() throws IO " testSuccess()", " for i = 1 to box.get()", " testSuccess()", + " switch box.get()", + " case 0", + " testSuccess()", + " default", + " testFail(\"switch\")", " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse"); From f09ee161d6a9adf00305e41b68eac879ae0bbdf2 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 17:14:03 +0200 Subject: [PATCH 07/22] Preserve stacktraces in Lua assurance calls --- .../translation/imtranslation/LuaNativeLowering.java | 4 ++-- .../java/tests/wurstscript/tests/LuaBackendAuditTests.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) 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 d4672d0fa..5291e9721 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 @@ -382,8 +382,8 @@ public void visit(ImFunctionCall call) { normalized = JassIm.ImOperatorCall(WurstOperator.EQ, JassIm.ImExprs(argument.copy(), JassIm.ImBoolVal(true))); } else { - normalized = JassIm.ImFunctionCall(call.attrTrace(), ensure, - JassIm.ImTypeArguments(), JassIm.ImExprs(argument.copy()), false, CallType.NORMAL); + normalized = callWithStacktrace(call.attrTrace(), ensure, + JassIm.ImExprs(argument.copy())); } argument.replaceBy(normalized); } 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 7689adc99..9f90fc589 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 @@ -2559,13 +2559,18 @@ public void stacktracedLuaLoweringPassesHelperStacktraceArguments() { "package Test", "native print(string message)", "native I2S(int value) returns string", + "native consumeInt(int value)", + "native consumeString(string value)", "int array values", + "string array names", "function readValue(int index) returns int", " return values[index]", "function join(string left, string right) returns string", " return left + right", "init", " values[1] = 7", + " consumeInt(values[1])", + " consumeString(names[1])", " print(join(\"value=\", I2S(readValue(1))))" ); } From 3eb9de4ab8da4355a57eb06122c1ebba600f6b97 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 18:09:32 +0200 Subject: [PATCH 08/22] Normalize erased generic closure results --- .../imtranslation/ClosureTranslator.java | 2 ++ .../tests/LuaBackendAuditTests.java | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index d540afdc7..e30aff0ae 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -202,6 +202,8 @@ private ImClass createClass() { ImExpr translated = e.getImplementation().imTranslateExpr(tr, impl); + translated = ExprTranslation.wrapTranslation(e.getImplementation(), tr, translated, + e.getImplementation().attrTypRaw(), superMethod.attrReturnType()); if (e.getImplementation().attrTyp().isVoid()) { 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 9f90fc589..0bd9257ca 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 @@ -2226,6 +2226,28 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse() throws IO compiled.contains("__wurst_ensureInt")); } + @Test + public void erasedGenericPrimitiveDefaultsAreNormalizedInTypedClosures() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "interface IntSupplier", + " function get() returns int", + "class Box", + " T value", + " function get() returns T", + " return value", + "init", + " let box = new Box", + " IntSupplier supplier = () -> box.get()", + " if supplier.get() + 1 == 1", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedInTypedClosures"); + assertTrue("typed closure implementations must normalize erased primitive results", + compiled.contains("return __wurst_ensureInt(Box_Box_get(")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two From c99af949d2b81939be193ba869c3854652836159 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 18:21:24 +0200 Subject: [PATCH 09/22] Normalize erased generic statement block results --- .../imtranslation/ExprTranslation.java | 7 ++++-- .../tests/LuaBackendAuditTests.java | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 6eaa544af..c7687ad67 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -945,8 +945,11 @@ public static ImExpr translate(ExprStatementsBlock e, ImTranslator translator, I StmtReturn r = e.getReturnStmt(); if (r != null && r.getReturnedObj() instanceof Expr) { - ImExpr expr = ((Expr) r.getReturnedObj()).imTranslateExpr(translator, f); - return JassIm.ImStatementExpr(statements, expr); + Expr returnedExpr = (Expr) r.getReturnedObj(); + ImExpr expr = returnedExpr.imTranslateExpr(translator, f); + expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), e.attrExpectedTypRaw()); + return wrapTranslation(e, translator, JassIm.ImStatementExpr(statements, expr), + e.attrTypRaw(), e.attrExpectedTypRaw()); } else { return ImHelper.statementExprVoid(statements); } 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 0bd9257ca..80a1e7c29 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 @@ -2248,6 +2248,28 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedInTypedClosures() throws compiled.contains("return __wurst_ensureInt(Box_Box_get(")); } + @Test + public void erasedGenericPrimitiveDefaultsAreNormalizedInTypedStatementBlocks() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Box", + " T value", + " function get() returns T", + " return value", + "init", + " let box = new Box", + " int value = begin", + " return box.get()", + " end", + " if value + 1 == 1", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedInTypedStatementBlocks"); + assertTrue("typed statement blocks must normalize erased primitive results", + compiled.contains("value = __wurst_ensureInt(Box_Box_get(box))")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two From b94d7e622efd66f20fe3fb111f10f9baa5335161 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 18:37:09 +0200 Subject: [PATCH 10/22] Propagate range types into expression children --- .../attributes/AttrExprExpectedType.java | 5 ++++ .../tests/LuaBackendAuditTests.java | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java index 53e90a6d7..05da69943 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java @@ -76,6 +76,11 @@ public class AttrExprExpectedType { if (nearestFuncDef != null) { return nearestFuncDef.attrReturnTyp(); } + } else if (parent instanceof StmtForRange) { + StmtForRange forRange = (StmtForRange) parent; + if (forRange.getTo() == expr || forRange.getStep() == expr) { + return WurstTypeInt.instance(); + } } else if (parent instanceof SwitchCase) { SwitchCase sc = (SwitchCase) parent; SwitchStmt s = (SwitchStmt) sc.getParent().getParent(); 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 80a1e7c29..4ec720ed1 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 @@ -2270,6 +2270,29 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedInTypedStatementBlocks() compiled.contains("value = __wurst_ensureInt(Box_Box_get(box))")); } + @Test + public void erasedGenericPrimitiveDefaultsAreNormalizedInCompositeRangeBounds() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Box", + " T value", + " function get() returns T", + " return value", + "init", + " let box = new Box", + " bool useBox = true", + " int iterations = 0", + " for i = 1 to (useBox ? box.get() : 0)", + " iterations++", + " if iterations == 0", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedInCompositeRangeBounds"); + assertTrue("composite range bounds must normalize erased primitive branches", + compiled.contains("__wurst_ensureInt(Box_Box_get(box))")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two From e101e6397287bace7b9a3e8db86ee0da312511a8 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 18:47:53 +0200 Subject: [PATCH 11/22] Use selected overload type for erased arguments --- .../imtranslation/ExprTranslation.java | 6 ++++- .../tests/LuaBackendAuditTests.java | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index c7687ad67..be363e666 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -7,6 +7,7 @@ import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.ast.Element; import de.peeeq.wurstscript.attributes.AttrFuncDef; +import de.peeeq.wurstscript.attributes.AttrExprExpectedType; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.attributes.AttrImplicitParameter; import de.peeeq.wurstscript.attributes.names.FuncLink; @@ -99,7 +100,10 @@ public static ImExpr translate(ExprInstanceOf e, ImTranslator t, ImFunction f) { private static ImExpr wrapTranslation(Expr e, ImTranslator t, ImExpr translated) { WurstType actualType = e.attrTypRaw(); - WurstType expectedTypRaw = e.attrExpectedTypRaw(); + WurstType expectedTypRaw = actualType instanceof WurstTypeBoundTypeParam + && e.getParent() instanceof Arguments + ? AttrExprExpectedType.afterOverloading(e) + : e.attrExpectedTypRaw(); return wrapTranslation(e, t, translated, actualType, expectedTypRaw); } 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 4ec720ed1..0ed0da174 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 @@ -2293,6 +2293,28 @@ public void erasedGenericPrimitiveDefaultsAreNormalizedInCompositeRangeBounds() compiled.contains("__wurst_ensureInt(Box_Box_get(box))")); } + @Test + public void erasedGenericPrimitiveDefaultsUseTheSelectedOverload() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Box", + " T value", + " function get() returns T", + " return value", + "function consume(int value)", + " if value == 0", + " testSuccess()", + "function consume(string value)", + "init", + " let box = new Box", + " consume(box.get())" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsUseTheSelectedOverload"); + assertTrue("selected integer overload arguments must normalize erased primitive values", + compiled.contains("__wurst_ensureInt(Box_Box_get(box))")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two From 5920bf918d94e742ed644423015dab5daa6b3926 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 19:22:48 +0200 Subject: [PATCH 12/22] Fix erased generic propagation in composite Lua expressions --- .../attributes/AttrExprExpectedType.java | 45 +++++++++++++++++++ .../imtranslation/ClosureTranslator.java | 12 +++-- .../imtranslation/ExprTranslation.java | 41 ++++++++++++++--- .../tests/LuaBackendAuditTests.java | 39 ++++++++++++++++ 4 files changed, 129 insertions(+), 8 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java index 05da69943..d66f438ce 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java @@ -2,6 +2,7 @@ import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.*; +import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.types.*; import de.peeeq.wurstscript.utils.Utils; import org.eclipse.jdt.annotation.NonNull; @@ -46,6 +47,14 @@ public class AttrExprExpectedType { return varDef.attrTyp(); } else if (parent instanceof ExprBinary) { ExprBinary exprBinary = (ExprBinary) parent; + if (exprBinary.attrFuncLink() != null) { + FunctionSignature signature = FunctionSignature.fromNameLink(exprBinary.attrFuncLink()); + if (exprBinary.getLeft() == expr && signature.getReceiverType() != null) { + return signature.getReceiverType(); + } else if (exprBinary.getRight() == expr && !signature.getParamTypes().isEmpty()) { + return signature.getParamType(0); + } + } WurstType leftType = exprBinary.getLeft().attrTyp(); WurstType rightType = exprBinary.getRight().attrTyp(); if (leftType.equalsType(rightType, expr)) { @@ -72,6 +81,18 @@ public class AttrExprExpectedType { } } else if (parent instanceof StmtReturn) { StmtReturn stmtReturn = (StmtReturn) parent; + if (stmtReturn.getParent() instanceof ExprStatementsBlock) { + ExprStatementsBlock block = (ExprStatementsBlock) stmtReturn.getParent(); + WurstType expectedType = block.attrExpectedTypRaw(); + if (expectedType instanceof WurstTypeUnknown + && block.getParent() instanceof ExprClosure) { + FuncLink abstractMethod = ((ExprClosure) block.getParent()).attrClosureAbstractMethod(); + if (abstractMethod != null) { + return abstractMethod.getReturnType(); + } + } + return expectedType; + } FunctionImplementation nearestFuncDef = stmtReturn.attrNearestFuncDef(); if (nearestFuncDef != null) { return nearestFuncDef.attrReturnTyp(); @@ -81,6 +102,23 @@ public class AttrExprExpectedType { if (forRange.getTo() == expr || forRange.getStep() == expr) { return WurstTypeInt.instance(); } + } else if (parent instanceof ExprStatementsBlock) { + ExprStatementsBlock block = (ExprStatementsBlock) parent; + if (block.getReturnStmt() != null && block.getReturnStmt().getReturnedObj() == expr) { + return block.attrExpectedTypRaw(); + } + } else if (parent instanceof Indexes) { + return WurstTypeInt.instance(); + } else if (parent instanceof SwitchStmt) { + SwitchStmt switchStmt = (SwitchStmt) parent; + if (switchStmt.getExpr() == expr) { + for (SwitchCase switchCase : switchStmt.getCases()) { + for (Expr caseExpr : switchCase.getExpressions()) { + WurstType type = caseExpr.attrTyp(); + return type instanceof WurstTypeIntLiteral ? WurstTypeInt.instance() : type; + } + } + } } else if (parent instanceof SwitchCase) { SwitchCase sc = (SwitchCase) parent; SwitchStmt s = (SwitchStmt) sc.getParent().getParent(); @@ -115,6 +153,13 @@ public class AttrExprExpectedType { private static WurstType expectedTypeSuperCall(SuperConstructorCall sc, Expr expr) { ConstructorDef constr = (ConstructorDef) sc.getParent(); + ConstructorDef selected = constr.attrSuperConstructor(); + if (selected != null) { + int selectedIndex = SmallHelpers.superArgs(constr).indexOf(expr); + if (selectedIndex >= 0 && selectedIndex < selected.getParameters().size()) { + return selected.getParameters().get(selectedIndex).getTyp().attrTyp(); + } + } ClassDef c = constr.attrNearestClassDef(); if (c == null) { return WurstTypeUnknown.instance(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index e30aff0ae..66416f7b3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -201,9 +201,15 @@ private ImClass createClass() { OverrideUtils.addOverrideClosure(tr, superMethod, m, e); - ImExpr translated = e.getImplementation().imTranslateExpr(tr, impl); - translated = ExprTranslation.wrapTranslation(e.getImplementation(), tr, translated, - e.getImplementation().attrTypRaw(), superMethod.attrReturnType()); + ImExpr translated; + if (e.getImplementation() instanceof ExprIfElse) { + translated = ExprTranslation.translateWithExpectedType( + e.getImplementation(), tr, impl, superMethod.attrReturnType()); + } else { + translated = e.getImplementation().imTranslateExpr(tr, impl); + translated = ExprTranslation.wrapTranslation(e.getImplementation(), tr, translated, + e.getImplementation().attrTypRaw(), superMethod.attrReturnType()); + } if (e.getImplementation().attrTyp().isVoid()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index be363e666..ef9d9696e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -100,7 +100,8 @@ public static ImExpr translate(ExprInstanceOf e, ImTranslator t, ImFunction f) { private static ImExpr wrapTranslation(Expr e, ImTranslator t, ImExpr translated) { WurstType actualType = e.attrTypRaw(); - WurstType expectedTypRaw = actualType instanceof WurstTypeBoundTypeParam + WurstType expectedTypRaw = t.isLuaTarget() + && actualType instanceof WurstTypeBoundTypeParam && e.getParent() instanceof Arguments ? AttrExprExpectedType.afterOverloading(e) : e.attrExpectedTypRaw(); @@ -950,10 +951,16 @@ public static ImExpr translate(ExprStatementsBlock e, ImTranslator translator, I StmtReturn r = e.getReturnStmt(); if (r != null && r.getReturnedObj() instanceof Expr) { Expr returnedExpr = (Expr) r.getReturnedObj(); - ImExpr expr = returnedExpr.imTranslateExpr(translator, f); - expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), e.attrExpectedTypRaw()); - return wrapTranslation(e, translator, JassIm.ImStatementExpr(statements, expr), - e.attrTypRaw(), e.attrExpectedTypRaw()); + ImExpr expr = returnedExpr instanceof ExprIfElse + ? translateWithExpectedType(returnedExpr, translator, f, e.attrExpectedTypRaw()) + : returnedExpr.imTranslateExpr(translator, f); + if (!(returnedExpr instanceof ExprIfElse)) { + expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), e.attrExpectedTypRaw()); + } + ImExpr result = JassIm.ImStatementExpr(statements, expr); + return returnedExpr instanceof ExprIfElse + ? result + : wrapTranslation(e, translator, result, e.attrTypRaw(), e.attrExpectedTypRaw()); } else { return ImHelper.statementExprVoid(statements); } @@ -1006,6 +1013,30 @@ public static ImExpr translate(ExprIfElse e, ImTranslator t, ImFunction f) { ); } + static ImExpr translateWithExpectedType(Expr e, ImTranslator t, ImFunction f, WurstType expectedType) { + if (e instanceof ExprIfElse) { + return translateWithExpectedType((ExprIfElse) e, t, f, expectedType); + } + ImExpr translated = e.imTranslateExpr(t, f); + return wrapTranslation(e, t, translated, e.attrTypRaw(), expectedType); + } + + private static ImExpr translateWithExpectedType(ExprIfElse e, ImTranslator t, ImFunction f, + WurstType expectedType) { + ImExpr ifTrue = translateWithExpectedType(e.getIfTrue(), t, f, expectedType); + ImExpr ifFalse = translateWithExpectedType(e.getIfFalse(), t, f, expectedType); + ImVar res = JassIm.ImVar(e, ifTrue.attrTyp(), "cond_result", false); + f.getLocals().add(res); + return JassIm.ImStatementExpr( + ImStmts( + ImIf(e, e.getCond().imTranslateExpr(t, f), + ImStmts(ImSet(e.getIfTrue(), ImVarAccess(res), ifTrue)), + ImStmts(ImSet(e.getIfFalse(), ImVarAccess(res), ifFalse))) + ), + JassIm.ImVarAccess(res) + ); + } + public static ImLExpr translateLvalue(LExpr e, ImTranslator t, ImFunction f) { NameDef decl = e.attrNameDef(); if (decl == null) { 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 0ed0da174..151a60672 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 @@ -2315,6 +2315,45 @@ public void erasedGenericPrimitiveDefaultsUseTheSelectedOverload() throws IOExce compiled.contains("__wurst_ensureInt(Box_Box_get(box))")); } + @Test + public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Box", + " T value", + " function get() returns T", + " return value", + "class Addable", + " function op_plus(int value) returns int", + " return value", + "interface IntSupplier", + " function get() returns int", + "int array values", + "init", + " let box = new Box", + " let addable = new Addable()", + " bool useBox = true", + " values[0] = 7", + " let sum = addable + box.get()", + " let indexed = values[useBox ? box.get() : 0]", + " IntSupplier supplier = () -> (useBox ? box.get() : 0)", + " int blockValue = begin", + " return (useBox ? box.get() : 0)", + " end", + " int switchValue = -1", + " switch (useBox ? box.get() : 1)", + " case 0", + " switchValue = 0", + " if sum == 0 and indexed == 7 and supplier.get() == 0", + " and blockValue == 0 and switchValue == 0", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); + assertEquals("each concrete integer consumer must normalize its erased generic input", 5, + countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two From 5f218489a332c3a2d908caa305e2f5440b5c6a4f Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 19:32:26 +0200 Subject: [PATCH 13/22] Cover unary erased generic closure results --- .../imtranslation/ClosureTranslator.java | 4 +++- .../translation/imtranslation/ExprTranslation.java | 14 +++++++++++--- .../wurstscript/tests/LuaBackendAuditTests.java | 4 +++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index 66416f7b3..a719ef03b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -202,7 +202,9 @@ private ImClass createClass() { ImExpr translated; - if (e.getImplementation() instanceof ExprIfElse) { + boolean propagatesExpectedType = e.getImplementation() instanceof ExprIfElse + || e.getImplementation() instanceof ExprUnary; + if (propagatesExpectedType) { translated = ExprTranslation.translateWithExpectedType( e.getImplementation(), tr, impl, superMethod.attrReturnType()); } else { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index ef9d9696e..b117d49ee 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -252,6 +252,7 @@ private static ImExpr wrapImplicitToString(ExprBinary concat, Expr operand, ImEx FunctionDefinition calledFunc = toString.getDef().attrRealFuncDef(); FunctionSignature signature = FunctionSignature.fromNameLink(toString); + translated = wrapTranslation(operand, t, translated, operand.attrTypRaw(), signature.getReceiverType()); if (calledFunc instanceof FuncDef && !((FuncDef) calledFunc).attrIsStatic() && operand.attrTyp().allowsDynamicDispatch()) { @@ -951,14 +952,15 @@ public static ImExpr translate(ExprStatementsBlock e, ImTranslator translator, I StmtReturn r = e.getReturnStmt(); if (r != null && r.getReturnedObj() instanceof Expr) { Expr returnedExpr = (Expr) r.getReturnedObj(); - ImExpr expr = returnedExpr instanceof ExprIfElse + boolean propagatesExpectedType = returnedExpr instanceof ExprIfElse || returnedExpr instanceof ExprUnary; + ImExpr expr = propagatesExpectedType ? translateWithExpectedType(returnedExpr, translator, f, e.attrExpectedTypRaw()) : returnedExpr.imTranslateExpr(translator, f); - if (!(returnedExpr instanceof ExprIfElse)) { + if (!propagatesExpectedType) { expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), e.attrExpectedTypRaw()); } ImExpr result = JassIm.ImStatementExpr(statements, expr); - return returnedExpr instanceof ExprIfElse + return propagatesExpectedType ? result : wrapTranslation(e, translator, result, e.attrTypRaw(), e.attrExpectedTypRaw()); } else { @@ -1017,6 +1019,12 @@ static ImExpr translateWithExpectedType(Expr e, ImTranslator t, ImFunction f, Wu if (e instanceof ExprIfElse) { return translateWithExpectedType((ExprIfElse) e, t, f, expectedType); } + if (e instanceof ExprUnary) { + ExprUnary unary = (ExprUnary) e; + ImExpr right = translateWithExpectedType(unary.getRight(), t, f, expectedType); + ImExpr translated = ImOperatorCall(unary.getOpU(), ImExprs(right)); + return wrapTranslation(e, t, translated, e.attrTypRaw(), expectedType); + } ImExpr translated = e.imTranslateExpr(t, f); return wrapTranslation(e, t, translated, e.attrTypRaw(), expectedType); } 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 151a60672..719794988 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 @@ -2338,6 +2338,7 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " let sum = addable + box.get()", " let indexed = values[useBox ? box.get() : 0]", " IntSupplier supplier = () -> (useBox ? box.get() : 0)", + " IntSupplier unarySupplier = () -> -box.get()", " int blockValue = begin", " return (useBox ? box.get() : 0)", " end", @@ -2346,11 +2347,12 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " case 0", " switchValue = 0", " if sum == 0 and indexed == 7 and supplier.get() == 0", + " and unarySupplier.get() == 0", " and blockValue == 0 and switchValue == 0", " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); - assertEquals("each concrete integer consumer must normalize its erased generic input", 5, + assertEquals("each concrete integer consumer must normalize its erased generic input", 6, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); } From 24246be53ac184492e3083ffd473de75971beab5 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 19:43:18 +0200 Subject: [PATCH 14/22] Propagate selected types through composite Lua arguments --- .../imtranslation/ExprTranslation.java | 41 +++++++++++++++++-- .../tests/LuaBackendAuditTests.java | 9 +++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index b117d49ee..d0c4aa049 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -703,7 +703,8 @@ && isCalledOnDynamicRef(e) ImExpr receiver = leftExpr == null ? null : leftExpr.imTranslateExpr(t, f); boolean normalizeAtBoundary = directFunc != null && isLuaExternalBoundary(directFunc); - ImExprs imArgs = translateExprs(arguments, t, f, normalizeAtBoundary); + FunctionSignature selectedSignature = t.isLuaTarget() ? e.attrFunctionSignature() : null; + ImExprs imArgs = translateExprs(arguments, t, f, normalizeAtBoundary, selectedSignature); if (calledFunc instanceof TupleDef) { // creating a new tuple... @@ -832,9 +833,20 @@ private static ImExprs translateExprs(List arguments, ImTranslator t, ImFu private static ImExprs translateExprs(List arguments, ImTranslator t, ImFunction f, boolean externalBoundary) { + return translateExprs(arguments, t, f, externalBoundary, null); + } + + private static ImExprs translateExprs(List arguments, ImTranslator t, ImFunction f, + boolean externalBoundary, @Nullable FunctionSignature selectedSignature) { ImExprs result = ImExprs(); - for (Expr e : arguments) { - ImExpr translated = e.imTranslateExpr(t, f); + for (int i = 0; i < arguments.size(); i++) { + Expr e = arguments.get(i); + WurstType expectedType = selectedSignature != null && i < selectedSignature.getMaxNumParams() + ? selectedSignature.getParamType(i) + : null; + ImExpr translated = expectedType != null && isCompositeExpectedTypeExpression(e) + ? translateWithExpectedType(e, t, f, expectedType) + : e.imTranslateExpr(t, f); if (externalBoundary) { translated = wrapLuaAtExternalBoundary(e, t, translated); } @@ -843,6 +855,10 @@ private static ImExprs translateExprs(List arguments, ImTranslator t, ImFu return result; } + private static boolean isCompositeExpectedTypeExpression(Expr e) { + return e instanceof ExprIfElse || e instanceof ExprUnary; + } + private static boolean isLuaExternalBoundary(ImFunction function) { return function.isNative() || function.isBj() || function.isExtern(); } @@ -1026,9 +1042,28 @@ static ImExpr translateWithExpectedType(Expr e, ImTranslator t, ImFunction f, Wu return wrapTranslation(e, t, translated, e.attrTypRaw(), expectedType); } ImExpr translated = e.imTranslateExpr(t, f); + if (isAlreadyTypeAssured(translated, t)) { + return translated; + } return wrapTranslation(e, t, translated, e.attrTypRaw(), expectedType); } + private static boolean isAlreadyTypeAssured(ImExpr translated, ImTranslator t) { + if (translated instanceof ImFunctionCall) { + ImFunction function = ((ImFunctionCall) translated).getFunc(); + return function == t.ensureIntFunc || function == t.ensureRealFunc + || function == t.ensureStrFunc || function == t.ensureBoolFunc; + } + if (translated instanceof ImOperatorCall) { + ImOperatorCall operator = (ImOperatorCall) translated; + return operator.getOp() == WurstOperator.EQ + && operator.getArguments().size() == 2 + && operator.getArguments().get(1) instanceof ImBoolVal + && ((ImBoolVal) operator.getArguments().get(1)).getValB(); + } + return false; + } + private static ImExpr translateWithExpectedType(ExprIfElse e, ImTranslator t, ImFunction f, WurstType expectedType) { ImExpr ifTrue = translateWithExpectedType(e.getIfTrue(), t, f, expectedType); 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 719794988..2e8cf2ef3 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 @@ -2327,6 +2327,10 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th "class Addable", " function op_plus(int value) returns int", " return value", + "function consume(int value) returns int", + " return value", + "function consume(string value) returns int", + " return -1", "interface IntSupplier", " function get() returns int", "int array values", @@ -2336,6 +2340,7 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " bool useBox = true", " values[0] = 7", " let sum = addable + box.get()", + " let overloaded = consume(useBox ? box.get() : 0)", " let indexed = values[useBox ? box.get() : 0]", " IntSupplier supplier = () -> (useBox ? box.get() : 0)", " IntSupplier unarySupplier = () -> -box.get()", @@ -2346,13 +2351,13 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " switch (useBox ? box.get() : 1)", " case 0", " switchValue = 0", - " if sum == 0 and indexed == 7 and supplier.get() == 0", + " if sum == 0 and overloaded == 0 and indexed == 7 and supplier.get() == 0", " and unarySupplier.get() == 0", " and blockValue == 0 and switchValue == 0", " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); - assertEquals("each concrete integer consumer must normalize its erased generic input", 6, + assertEquals("each concrete integer consumer must normalize its erased generic input", 7, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); } From 5c72faed3ca9ccc1c576cc7511637c084be4a722 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 19:48:03 +0200 Subject: [PATCH 15/22] Propagate concrete types through Lua composite operands --- .../imtranslation/ExprTranslation.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index d0c4aa049..ea6268b6f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -208,10 +208,10 @@ private static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr tran } public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) { - ImExpr left = e.getLeft().imTranslateExpr(t, f); - ImExpr right = e.getRight().imTranslateExpr(t, f); WurstOperator op = e.getOp(); FuncLink overloadedOperator = e.attrFuncLink(); + ImExpr left = translateConcatOperand(e, e.getLeft(), t, f, overloadedOperator); + ImExpr right = translateConcatOperand(e, e.getRight(), t, f, overloadedOperator); if (op == WurstOperator.PLUS && overloadedOperator == null) { left = wrapImplicitToString(e, e.getLeft(), left, t); right = wrapImplicitToString(e, e.getRight(), right, t); @@ -243,6 +243,19 @@ public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) return ImOperatorCall(op, ImExprs(left, right)); } + private static ImExpr translateConcatOperand(ExprBinary concat, Expr operand, ImTranslator t, ImFunction f, + @Nullable FuncLink overloadedOperator) { + if (concat.getOp() == WurstOperator.PLUS && overloadedOperator == null + && isCompositeExpectedTypeExpression(operand)) { + FuncLink toString = AttrFuncDef.implicitToStringForConcatOperand(concat, operand); + if (toString != null) { + FunctionSignature signature = FunctionSignature.fromNameLink(toString); + return translateWithExpectedType(operand, t, f, signature.getReceiverType()); + } + } + return operand.imTranslateExpr(t, f); + } + private static ImExpr wrapImplicitToString(ExprBinary concat, Expr operand, ImExpr translated, ImTranslator t) { FuncLink toString = AttrFuncDef.implicitToStringForConcatOperand(concat, operand); From 510b152f5af87da81f08deb62211117a6375f738 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 20:02:04 +0200 Subject: [PATCH 16/22] Fix erased operands in Lua composite expressions --- .../imtranslation/ExprTranslation.java | 20 ++++++++++++++++++- .../tests/LuaBackendAuditTests.java | 16 ++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index ea6268b6f..1017def5a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -212,6 +212,14 @@ public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) FuncLink overloadedOperator = e.attrFuncLink(); ImExpr left = translateConcatOperand(e, e.getLeft(), t, f, overloadedOperator); ImExpr right = translateConcatOperand(e, e.getRight(), t, f, overloadedOperator); + if (overloadedOperator == null) { + // A built-in operator can leave both operands with the same erased + // generic type. In that case there is no concrete expected type to + // trigger wrapTranslation, but Lua still needs each operand's + // primitive default restored before applying the operator. + left = normalizeBuiltinOperand(e.getLeft(), left, t); + right = normalizeBuiltinOperand(e.getRight(), right, t); + } if (op == WurstOperator.PLUS && overloadedOperator == null) { left = wrapImplicitToString(e, e.getLeft(), left, t); right = wrapImplicitToString(e, e.getRight(), right, t); @@ -243,6 +251,14 @@ public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) return ImOperatorCall(op, ImExprs(left, right)); } + private static ImExpr normalizeBuiltinOperand(Expr operand, ImExpr translated, ImTranslator t) { + if (!t.isLuaTarget() || !(operand.attrTypRaw() instanceof WurstTypeBoundTypeParam) + || isAlreadyTypeAssured(translated, t)) { + return translated; + } + return wrapLua(operand, t, translated, operand.attrTypRaw()); + } + private static ImExpr translateConcatOperand(ExprBinary concat, Expr operand, ImTranslator t, ImFunction f, @Nullable FuncLink overloadedOperator) { if (concat.getOp() == WurstOperator.PLUS && overloadedOperator == null @@ -926,7 +942,9 @@ public static ImExpr translateIntern(ExprNewObject e, ImTranslator t, ImFunction WurstTypeClass wurstType = (WurstTypeClass) e.attrTyp(); ImClass imClass = t.getClassFor(wurstType.getClassDef()); ImTypeArguments typeArgs = getFunctionCallTypeArguments(t, sig, e, imClass.getTypeVariables()); - return ImFunctionCall(e, constructorImFunc, typeArgs, translateExprs(e.getArgs(), t, f), false, CallType.NORMAL); + FunctionSignature selectedSignature = t.isLuaTarget() ? sig : null; + return ImFunctionCall(e, constructorImFunc, typeArgs, + translateExprs(e.getArgs(), t, f, false, selectedSignature), false, CallType.NORMAL); } public static ImExprOpt translate(NoExpr e, ImTranslator translator, ImFunction f) { 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 2e8cf2ef3..681a7c3e7 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 @@ -2327,6 +2327,14 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th "class Addable", " function op_plus(int value) returns int", " return value", + "class Constructed", + " int value", + " construct(int value)", + " this.value = value", + " construct(string value)", + " this.value = -1", + " function get() returns int", + " return value", "function consume(int value) returns int", " return value", "function consume(string value) returns int", @@ -2340,10 +2348,12 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " bool useBox = true", " values[0] = 7", " let sum = addable + box.get()", + " let builtinSum = box.get() + box.get()", " let overloaded = consume(useBox ? box.get() : 0)", " let indexed = values[useBox ? box.get() : 0]", " IntSupplier supplier = () -> (useBox ? box.get() : 0)", " IntSupplier unarySupplier = () -> -box.get()", + " let constructed = new Constructed(useBox ? box.get() : 0)", " int blockValue = begin", " return (useBox ? box.get() : 0)", " end", @@ -2351,13 +2361,13 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " switch (useBox ? box.get() : 1)", " case 0", " switchValue = 0", - " if sum == 0 and overloaded == 0 and indexed == 7 and supplier.get() == 0", + " if sum == 0 and builtinSum == 0 and overloaded == 0 and indexed == 7 and supplier.get() == 0", " and unarySupplier.get() == 0", - " and blockValue == 0 and switchValue == 0", + " and blockValue == 0 and switchValue == 0 and constructed.get() == 0", " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); - assertEquals("each concrete integer consumer must normalize its erased generic input", 7, + assertEquals("each concrete integer consumer must normalize its erased generic input", 10, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); } From 72489bc8ee2a630282f2e6a69954a551719f517b Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 20:11:14 +0200 Subject: [PATCH 17/22] Propagate selected types into statement blocks --- .../imtranslation/ExprTranslation.java | 17 ++++++++++++----- .../wurstscript/tests/LuaBackendAuditTests.java | 8 ++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 1017def5a..24a5fca3a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -885,7 +885,7 @@ private static ImExprs translateExprs(List arguments, ImTranslator t, ImFu } private static boolean isCompositeExpectedTypeExpression(Expr e) { - return e instanceof ExprIfElse || e instanceof ExprUnary; + return e instanceof ExprIfElse || e instanceof ExprUnary || e instanceof ExprStatementsBlock; } private static boolean isLuaExternalBoundary(ImFunction function) { @@ -986,7 +986,11 @@ public static ImExpr translate(ExprClosure e, ImTranslator tr, ImFunction f) { } public static ImExpr translate(ExprStatementsBlock e, ImTranslator translator, ImFunction f) { + return translateStatementsBlock(e, translator, f, e.attrExpectedTypRaw()); + } + private static ImExpr translateStatementsBlock(ExprStatementsBlock e, ImTranslator translator, ImFunction f, + WurstType expectedType) { ImStmts statements = JassIm.ImStmts(); for (WStatement s : e.getBody()) { if (s instanceof StmtReturn) { @@ -999,17 +1003,17 @@ public static ImExpr translate(ExprStatementsBlock e, ImTranslator translator, I StmtReturn r = e.getReturnStmt(); if (r != null && r.getReturnedObj() instanceof Expr) { Expr returnedExpr = (Expr) r.getReturnedObj(); - boolean propagatesExpectedType = returnedExpr instanceof ExprIfElse || returnedExpr instanceof ExprUnary; + boolean propagatesExpectedType = isCompositeExpectedTypeExpression(returnedExpr); ImExpr expr = propagatesExpectedType - ? translateWithExpectedType(returnedExpr, translator, f, e.attrExpectedTypRaw()) + ? translateWithExpectedType(returnedExpr, translator, f, expectedType) : returnedExpr.imTranslateExpr(translator, f); if (!propagatesExpectedType) { - expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), e.attrExpectedTypRaw()); + expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), expectedType); } ImExpr result = JassIm.ImStatementExpr(statements, expr); return propagatesExpectedType ? result - : wrapTranslation(e, translator, result, e.attrTypRaw(), e.attrExpectedTypRaw()); + : wrapTranslation(e, translator, result, e.attrTypRaw(), expectedType); } else { return ImHelper.statementExprVoid(statements); } @@ -1066,6 +1070,9 @@ static ImExpr translateWithExpectedType(Expr e, ImTranslator t, ImFunction f, Wu if (e instanceof ExprIfElse) { return translateWithExpectedType((ExprIfElse) e, t, f, expectedType); } + if (e instanceof ExprStatementsBlock) { + return translateStatementsBlock((ExprStatementsBlock) e, t, f, expectedType); + } if (e instanceof ExprUnary) { ExprUnary unary = (ExprUnary) e; ImExpr right = translateWithExpectedType(unary.getRight(), t, f, expectedType); 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 681a7c3e7..c53364ca4 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 @@ -2350,6 +2350,9 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " let sum = addable + box.get()", " let builtinSum = box.get() + box.get()", " let overloaded = consume(useBox ? box.get() : 0)", + " let blockOverloaded = consume(begin", + " return (useBox ? box.get() : 0)", + " end)", " let indexed = values[useBox ? box.get() : 0]", " IntSupplier supplier = () -> (useBox ? box.get() : 0)", " IntSupplier unarySupplier = () -> -box.get()", @@ -2361,13 +2364,14 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " switch (useBox ? box.get() : 1)", " case 0", " switchValue = 0", - " if sum == 0 and builtinSum == 0 and overloaded == 0 and indexed == 7 and supplier.get() == 0", + " if sum == 0 and builtinSum == 0 and overloaded == 0 and blockOverloaded == 0", + " and indexed == 7 and supplier.get() == 0", " and unarySupplier.get() == 0", " and blockValue == 0 and switchValue == 0 and constructed.get() == 0", " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); - assertEquals("each concrete integer consumer must normalize its erased generic input", 10, + assertEquals("each concrete integer consumer must normalize its erased generic input", 11, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); } From dc1b2991e04639fa95bb06550d0bdfe7d3a576f3 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 20:29:01 +0200 Subject: [PATCH 18/22] Preserve normalization for primitive array reads --- .../imtranslation/LuaNativeLowering.java | 82 ++++++++++++++----- .../tests/LuaBackendAuditTests.java | 17 ++-- 2 files changed, 74 insertions(+), 25 deletions(-) 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 5291e9721..c4df7f309 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 @@ -353,14 +353,23 @@ private static int stacktraceParamIndex(ImFunction f) { } /** - * Normalizes primitive array reads only when they enter code outside the - * typed Wurst world. Lua's array metatables already provide Wurst - * defaults for ordinary reads, so doing this at every read is redundant; - * a native/BJ/extern call is the point where an untyped value must be - * made safe for the callee. + * Normalizes primitive array reads which can cross the Lua/Wurst boundary. + * Arrays can be visible to foreign Lua/Jass code, so a present value can + * be malformed even though the array metatable supplies defaults for + * missing keys. Lvalue writes remain raw; only rvalue reads are wrapped. */ private static void lowerPrimitiveArrayBoundaryEnsure(ImProg prog, ImTranslator translator) { prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImVarArrayAccess access) { + super.visit(access); + if (access.isUsedAsLValue() || isAlreadyNormalized(access, translator) + || isAlreadyNormalizedAccess(access, translator)) { + return; + } + replaceWithEnsure(access, access.attrTrace(), translator); + } + @Override public void visit(ImFunctionCall call) { super.visit(call); @@ -373,35 +382,68 @@ public void visit(ImFunctionCall call) { || isAlreadyNormalized(argument, translator)) { continue; } - ImFunction ensure = ensureFunctionFor(argument.attrTyp(), translator); - if (ensure == null) { - continue; - } - ImExpr normalized; - if (ensure == translator.ensureBoolFunc) { - normalized = JassIm.ImOperatorCall(WurstOperator.EQ, - JassIm.ImExprs(argument.copy(), JassIm.ImBoolVal(true))); - } else { - normalized = callWithStacktrace(call.attrTrace(), ensure, - JassIm.ImExprs(argument.copy())); - } - argument.replaceBy(normalized); + replaceWithEnsure((ImVarArrayAccess) argument, call.attrTrace(), translator); } } }); } + private static void replaceWithEnsure(ImVarArrayAccess access, de.peeeq.wurstscript.ast.Element trace, + ImTranslator translator) { + ImFunction ensure = ensureFunctionFor(access.attrTyp(), translator); + if (ensure == null) { + return; + } + ImExpr normalized; + if (ensure == translator.ensureBoolFunc) { + normalized = JassIm.ImOperatorCall(WurstOperator.EQ, + JassIm.ImExprs(access.copy(), JassIm.ImBoolVal(true))); + } else { + normalized = callWithStacktrace(trace, ensure, JassIm.ImExprs(access.copy())); + } + access.replaceBy(normalized); + } + private static boolean isExternalBoundary(ImFunction function) { return !function.getName().startsWith("__wurst_") && (function.isNative() || function.isBj() || function.isExtern()); } private static boolean isAlreadyNormalized(ImExpr argument, ImTranslator translator) { - return argument instanceof ImFunctionCall + if (argument instanceof ImFunctionCall && (((ImFunctionCall) argument).getFunc() == translator.ensureIntFunc || ((ImFunctionCall) argument).getFunc() == translator.ensureBoolFunc || ((ImFunctionCall) argument).getFunc() == translator.ensureRealFunc - || ((ImFunctionCall) argument).getFunc() == translator.ensureStrFunc); + || ((ImFunctionCall) argument).getFunc() == translator.ensureStrFunc)) { + return true; + } + if (argument instanceof ImOperatorCall) { + ImOperatorCall operator = (ImOperatorCall) argument; + return operator.getOp() == WurstOperator.EQ + && operator.getArguments().size() == 2 + && operator.getArguments().get(1) instanceof ImBoolVal + && ((ImBoolVal) operator.getArguments().get(1)).getValB(); + } + return false; + } + + private static boolean isAlreadyNormalizedAccess(ImVarArrayAccess access, ImTranslator translator) { + Element parent = access.getParent(); + Element owner = parent == null ? null : parent.getParent(); + if (owner instanceof ImFunctionCall) { + ImFunction function = ((ImFunctionCall) owner).getFunc(); + return function == translator.ensureIntFunc || function == translator.ensureBoolFunc + || function == translator.ensureRealFunc || function == translator.ensureStrFunc; + } + if (!(owner instanceof ImOperatorCall)) { + return false; + } + ImOperatorCall operator = (ImOperatorCall) owner; + return operator.getOp() == WurstOperator.EQ + && operator.getArguments().size() == 2 + && operator.getArguments().get(0) == access + && operator.getArguments().get(1) instanceof ImBoolVal + && ((ImBoolVal) operator.getArguments().get(1)).getValB(); } private static ImFunction ensureFunctionFor(ImType type, ImTranslator translator) { 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 c53364ca4..5bc50c560 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 @@ -2342,6 +2342,8 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th "interface IntSupplier", " function get() returns int", "int array values", + "function readArrayValue() returns int", + " return values[0]", "init", " let box = new Box", " let addable = new Addable()", @@ -2365,7 +2367,7 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " case 0", " switchValue = 0", " if sum == 0 and builtinSum == 0 and overloaded == 0 and blockOverloaded == 0", - " and indexed == 7 and supplier.get() == 0", + " and indexed == 7 and readArrayValue() == 7 and supplier.get() == 0", " and unarySupplier.get() == 0", " and blockValue == 0 and switchValue == 0 and constructed.get() == 0", " testSuccess()" @@ -2373,15 +2375,17 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); assertEquals("each concrete integer consumer must normalize its erased generic input", 11, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); + assertTrue("global primitive array reads must remain safe for foreign writes", + compiled.contains("__wurst_ensureInt(Test_values[0])")); } /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two * unsafe paths independently: erased generic propagation and a raw array - * read. The intermediate generic functions and an internal array reader - * must stay free of assurance calls, while the native call sites must have - * the appropriate normalization. This is intentionally compile-only: the + * read. The intermediate generic functions must stay free of assurance + * calls, while global array reads and native call sites must have the + * appropriate normalization. This is intentionally compile-only: the * generated native sinks have no Warcraft runtime implementation. */ @Test @@ -2423,7 +2427,10 @@ public void seededTypeAssuranceBoundaryFuzz() { ); assertFunctionBodyContains(compiled, "forward", "__wurst_ensure", false); - assertFunctionBodyContains(compiled, "read", "__wurst_ensure", false); + String readNormalization = type.equals("bool") + ? "(TypeAssuranceFuzz_values[" + arrayIndex + "] == true)" + : "__wurst_ensure" + suffix + "(TypeAssuranceFuzz_values[" + arrayIndex + "])"; + assertFunctionBodyContains(compiled, "read", readNormalization, true); String genericArgument = type.equals("bool") ? "(forward(" + literal + ") == true)" : "__wurst_ensure" + suffix + "(forward(" + literal + "))"; From 5afe445a5f482570229892cee29132a470917c55 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 22:27:42 +0200 Subject: [PATCH 19/22] Propagate closure types through statement blocks --- .../translation/imtranslation/ClosureTranslator.java | 3 +-- .../translation/imtranslation/ExprTranslation.java | 2 +- .../java/tests/wurstscript/tests/LuaBackendAuditTests.java | 7 +++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index a719ef03b..a24d5b63a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -202,8 +202,7 @@ private ImClass createClass() { ImExpr translated; - boolean propagatesExpectedType = e.getImplementation() instanceof ExprIfElse - || e.getImplementation() instanceof ExprUnary; + boolean propagatesExpectedType = ExprTranslation.isCompositeExpectedTypeExpression(e.getImplementation()); if (propagatesExpectedType) { translated = ExprTranslation.translateWithExpectedType( e.getImplementation(), tr, impl, superMethod.attrReturnType()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 24a5fca3a..8cb2a75bc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -884,7 +884,7 @@ private static ImExprs translateExprs(List arguments, ImTranslator t, ImFu return result; } - private static boolean isCompositeExpectedTypeExpression(Expr e) { + static boolean isCompositeExpectedTypeExpression(Expr e) { return e instanceof ExprIfElse || e instanceof ExprUnary || e instanceof ExprStatementsBlock; } 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 5bc50c560..42e013585 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 @@ -2358,6 +2358,9 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " let indexed = values[useBox ? box.get() : 0]", " IntSupplier supplier = () -> (useBox ? box.get() : 0)", " IntSupplier unarySupplier = () -> -box.get()", + " IntSupplier blockSupplier = () -> begin", + " return (useBox ? box.get() : 0)", + " end", " let constructed = new Constructed(useBox ? box.get() : 0)", " int blockValue = begin", " return (useBox ? box.get() : 0)", @@ -2368,12 +2371,12 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th " switchValue = 0", " if sum == 0 and builtinSum == 0 and overloaded == 0 and blockOverloaded == 0", " and indexed == 7 and readArrayValue() == 7 and supplier.get() == 0", - " and unarySupplier.get() == 0", + " and unarySupplier.get() == 0 and blockSupplier.get() == 0", " and blockValue == 0 and switchValue == 0 and constructed.get() == 0", " testSuccess()" ); String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); - assertEquals("each concrete integer consumer must normalize its erased generic input", 11, + assertEquals("each concrete integer consumer must normalize its erased generic input", 12, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); assertTrue("global primitive array reads must remain safe for foreign writes", compiled.contains("__wurst_ensureInt(Test_values[0])")); From 1d54bb40cd36f19b161d44b908ce2c98ac707761 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 23:34:10 +0200 Subject: [PATCH 20/22] Finish Lua type assurance boundaries --- .../imtranslation/ClassTranslator.java | 7 ++-- .../imtranslation/StmtTranslation.java | 6 ++-- .../tests/LuaBackendAuditTests.java | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java index f708c5285..248114a45 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java @@ -400,8 +400,11 @@ private void createConstructFunc(ConstructorDef constr) { if (calledConstr != null && calledConstr != constr) { ImFunction calledConstrFunc = translator.getConstructFunc(calledConstr); ImExprs arguments = ImExprs(ImVarAccess(thisVar)); - for (Expr a : thisCall.getArgs()) { - arguments.add(a.imTranslateExpr(translator, f)); + for (int i = 0; i < thisCall.getArgs().size(); i++) { + Expr argument = thisCall.getArgs().get(i); + WurstType expectedType = calledConstr.getParameters().get(i).getTyp().attrTyp(); + arguments.add(ExprTranslation.translateWithExpectedType( + argument, translator, f, expectedType)); } f.getBody().add(ImFunctionCall(trace, calledConstrFunc, classTypeArgs(), arguments, false, CallType.NORMAL)); bodyStartIndex = firstRelevantIndex + 1; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java index ba36a3926..630fd5562 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java @@ -392,8 +392,10 @@ public static ImStmt translate(StmtSet s, ImTranslator t, ImFunction f) { } else { receiver = ImVarAccess(receiverVar); } - ImExpr index = withIndexes.getIndexes().get(0).imTranslateExpr(t, f); - ImExpr value = s.getRight().imTranslateExpr(t, f); + ImExpr index = ExprTranslation.translateWithExpectedType( + withIndexes.getIndexes().get(0), t, f, setOverload.getParameterType(0)); + ImExpr value = ExprTranslation.translateWithExpectedType( + s.getRight(), t, f, setOverload.getParameterType(1)); ImFunction calledFunc = t.getFuncFor(setOverload.getDef()); return ImFunctionCall(s, calledFunc, ImTypeArguments(), ImExprs(receiver, index, value), false, CallType.NORMAL); } 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 42e013585..39741848f 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 @@ -2382,6 +2382,42 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th compiled.contains("__wurst_ensureInt(Test_values[0])")); } + @Test + public void erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Box", + " T value", + " function get() returns T", + " return value", + "class Delegating", + " int value", + " construct(int value)", + " this.value = value + 1", + " construct(Box box)", + " this(box.get())", + " function get() returns int", + " return value", + "class Indexed", + " bool assignedDefault", + " function op_index(int index) returns string", + " return \"\"", + " function op_indexAssign(int index, int value)", + " assignedDefault = value == 0", + "init", + " let box = new Box", + " let delegating = new Delegating(box)", + " let indexed = new Indexed", + " indexed[0] = box.get()", + " if delegating.get() == 1 and indexed.assignedDefault", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets"); + assertEquals("both resolved primitive consumers must normalize their erased generic input", 2, + countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); + } + /** * Seeded boundary corpus for the type-assurance change. Each case varies * the primitive type, literal value, and array slot while checking the two From 37453dacf413665be0bb96ce159ea88404445132 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 23:47:26 +0200 Subject: [PATCH 21/22] Handle varargs in constructor assurance --- .../translation/imtranslation/ClassTranslator.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java index 248114a45..c1adac74a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java @@ -402,7 +402,7 @@ private void createConstructFunc(ConstructorDef constr) { ImExprs arguments = ImExprs(ImVarAccess(thisVar)); for (int i = 0; i < thisCall.getArgs().size(); i++) { Expr argument = thisCall.getArgs().get(i); - WurstType expectedType = calledConstr.getParameters().get(i).getTyp().attrTyp(); + WurstType expectedType = constructorParameterType(calledConstr, i); arguments.add(ExprTranslation.translateWithExpectedType( argument, translator, f, expectedType)); } @@ -439,6 +439,16 @@ private void createConstructFunc(ConstructorDef constr) { f.getBody().addAll(translator.translateStatements(f, constr.getBody().subList(bodyStartIndex, constr.getBody().size()))); } + private static WurstType constructorParameterType(ConstructorDef constructor, int argumentIndex) { + int lastParameterIndex = constructor.getParameters().size() - 1; + WurstType parameterType = constructor.getParameters() + .get(Math.min(argumentIndex, lastParameterIndex)).getTyp().attrTyp(); + if (argumentIndex >= lastParameterIndex && parameterType instanceof WurstTypeVararg) { + return ((WurstTypeVararg) parameterType).getBaseType(); + } + return parameterType; + } + private int firstRelevantStatementIndex(ConstructorDef constr) { for (int i = 0; i < constr.getBody().size(); i++) { WStatement s = constr.getBody().get(i); From 053c8e09a844e91bb08bf91f9d1574dcd5cb2bf4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 2 Sep 2026 23:56:46 +0200 Subject: [PATCH 22/22] Unify constructor vararg assurance --- .../attributes/AttrExprExpectedType.java | 22 ++++++++++++++----- .../imtranslation/ClassTranslator.java | 13 ++--------- .../tests/LuaBackendAuditTests.java | 16 ++++++++++++-- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java index d66f438ce..d21c80f9d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprExpectedType.java @@ -153,11 +153,12 @@ public class AttrExprExpectedType { private static WurstType expectedTypeSuperCall(SuperConstructorCall sc, Expr expr) { ConstructorDef constr = (ConstructorDef) sc.getParent(); + int paramIndex = SmallHelpers.superArgs(constr).indexOf(expr); ConstructorDef selected = constr.attrSuperConstructor(); if (selected != null) { - int selectedIndex = SmallHelpers.superArgs(constr).indexOf(expr); - if (selectedIndex >= 0 && selectedIndex < selected.getParameters().size()) { - return selected.getParameters().get(selectedIndex).getTyp().attrTyp(); + WurstType selectedType = constructorParameterType(selected, paramIndex); + if (!(selectedType instanceof WurstTypeUnknown)) { + return selectedType; } } ClassDef c = constr.attrNearestClassDef(); @@ -175,8 +176,6 @@ private static WurstType expectedTypeSuperCall(SuperConstructorCall sc, Expr exp WurstType res = WurstTypeUnknown.instance(); - int paramIndex = SmallHelpers.superArgs(constr).indexOf(expr); - for (ConstructorDef superConstr : constructors) { if (superConstr.getParameters().size() == SmallHelpers.superArgs(constr).size()) { res = res.typeUnion(superConstr.getParameters().get(paramIndex).getTyp().attrTyp(), expr); @@ -186,6 +185,19 @@ private static WurstType expectedTypeSuperCall(SuperConstructorCall sc, Expr exp return res; } + public static WurstType constructorParameterType(ConstructorDef constructor, int argumentIndex) { + if (argumentIndex < 0 || constructor.getParameters().isEmpty()) { + return WurstTypeUnknown.instance(); + } + int lastParameterIndex = constructor.getParameters().size() - 1; + WurstType parameterType = constructor.getParameters() + .get(Math.min(argumentIndex, lastParameterIndex)).attrTyp(); + if (argumentIndex >= lastParameterIndex && parameterType instanceof WurstTypeVararg) { + return ((WurstTypeVararg) parameterType).getBaseType(); + } + return argumentIndex <= lastParameterIndex ? parameterType : WurstTypeUnknown.instance(); + } + private static WurstType expectedType(Expr expr, Arguments args, StmtCall stmtCall) { Collection sigs = stmtCall.attrPossibleFunctionSignatures(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java index c1adac74a..57f527f88 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java @@ -2,6 +2,7 @@ import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.attributes.AttrExprExpectedType; import de.peeeq.wurstscript.attributes.OverloadingResolver; import de.peeeq.wurstscript.jassIm.Element.DefaultVisitor; import de.peeeq.wurstscript.jassIm.*; @@ -402,7 +403,7 @@ private void createConstructFunc(ConstructorDef constr) { ImExprs arguments = ImExprs(ImVarAccess(thisVar)); for (int i = 0; i < thisCall.getArgs().size(); i++) { Expr argument = thisCall.getArgs().get(i); - WurstType expectedType = constructorParameterType(calledConstr, i); + WurstType expectedType = AttrExprExpectedType.constructorParameterType(calledConstr, i); arguments.add(ExprTranslation.translateWithExpectedType( argument, translator, f, expectedType)); } @@ -439,16 +440,6 @@ private void createConstructFunc(ConstructorDef constr) { f.getBody().addAll(translator.translateStatements(f, constr.getBody().subList(bodyStartIndex, constr.getBody().size()))); } - private static WurstType constructorParameterType(ConstructorDef constructor, int argumentIndex) { - int lastParameterIndex = constructor.getParameters().size() - 1; - WurstType parameterType = constructor.getParameters() - .get(Math.min(argumentIndex, lastParameterIndex)).getTyp().attrTyp(); - if (argumentIndex >= lastParameterIndex && parameterType instanceof WurstTypeVararg) { - return ((WurstTypeVararg) parameterType).getBaseType(); - } - return parameterType; - } - private int firstRelevantStatementIndex(ConstructorDef constr) { for (int i = 0; i < constr.getBody().size(); i++) { WStatement s = constr.getBody().get(i); 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 39741848f..c338b692c 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 @@ -2399,6 +2399,17 @@ public void erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets() thr " this(box.get())", " function get() returns int", " return value", + "class Parent", + " int sum", + " construct(int fixed, vararg int rest)", + " sum = fixed", + " for value in rest", + " sum += value", + "class Child extends Parent", + " construct(Box box)", + " super(1, box.get(), box.get())", + " function get() returns int", + " return sum", "class Indexed", " bool assignedDefault", " function op_index(int index) returns string", @@ -2408,13 +2419,14 @@ public void erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets() thr "init", " let box = new Box", " let delegating = new Delegating(box)", + " let child = new Child(box)", " let indexed = new Indexed", " indexed[0] = box.get()", - " if delegating.get() == 1 and indexed.assignedDefault", + " if delegating.get() == 1 and child.get() == 1 and indexed.assignedDefault", " testSuccess()" ); String compiled = compiledLua("erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets"); - assertEquals("both resolved primitive consumers must normalize their erased generic input", 2, + assertEquals("resolved primitive consumers must normalize their erased generic input", 4, countOccurrences(compiled, "__wurst_ensureInt(Box_Box_get(")); }