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 8cb2a75bc..66b7390db 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 @@ -731,9 +731,8 @@ && isCalledOnDynamicRef(e) } ImExpr receiver = leftExpr == null ? null : leftExpr.imTranslateExpr(t, f); - boolean normalizeAtBoundary = directFunc != null && isLuaExternalBoundary(directFunc); FunctionSignature selectedSignature = t.isLuaTarget() ? e.attrFunctionSignature() : null; - ImExprs imArgs = translateExprs(arguments, t, f, normalizeAtBoundary, selectedSignature); + ImExprs imArgs = translateExprs(arguments, t, f, selectedSignature); if (calledFunc instanceof TupleDef) { // creating a new tuple... @@ -857,16 +856,11 @@ private static boolean isCalledOnDynamicRef(FunctionCall e) { } private static ImExprs translateExprs(List arguments, ImTranslator t, ImFunction f) { - return translateExprs(arguments, t, f, false); + return translateExprs(arguments, t, f, null); } 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) { + @Nullable FunctionSignature selectedSignature) { ImExprs result = ImExprs(); for (int i = 0; i < arguments.size(); i++) { Expr e = arguments.get(i); @@ -876,9 +870,6 @@ private static ImExprs translateExprs(List arguments, ImTranslator t, ImFu ImExpr translated = expectedType != null && isCompositeExpectedTypeExpression(e) ? translateWithExpectedType(e, t, f, expectedType) : e.imTranslateExpr(t, f); - if (externalBoundary) { - translated = wrapLuaAtExternalBoundary(e, t, translated); - } result.add(translated); } return result; @@ -888,41 +879,6 @@ static boolean isCompositeExpectedTypeExpression(Expr e) { return e instanceof ExprIfElse || e instanceof ExprUnary || e instanceof ExprStatementsBlock; } - 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: 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; - } - 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.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 @@ -944,7 +900,7 @@ public static ImExpr translateIntern(ExprNewObject e, ImTranslator t, ImFunction ImTypeArguments typeArgs = getFunctionCallTypeArguments(t, sig, e, imClass.getTypeVariables()); FunctionSignature selectedSignature = t.isLuaTarget() ? sig : null; return ImFunctionCall(e, constructorImFunc, typeArgs, - translateExprs(e.getArgs(), t, f, false, selectedSignature), false, CallType.NORMAL); + translateExprs(e.getArgs(), t, f, selectedSignature), false, CallType.NORMAL); } public static ImExprOpt translate(NoExpr e, ImTranslator translator, ImFunction f) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index 0e38df8e8..c0f330026 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -191,6 +191,10 @@ public T canonical(T copy) { @Nullable public ImFunction ensureRealFunc = null; @Nullable public ImFunction ensureStrFunc = null; @Nullable public ImFunction stringConcatFunc = null; + // Exact synthetic nodes owned by LuaNativeLowering; backend intrinsic recognition must use identity. + @Nullable public ImFunction luaRawFloorDivIntFunc = null; + @Nullable public ImFunction luaRawFmodIntFunc = null; + @Nullable public ImFunction luaRawFmodRealFunc = null; private final Map varsForTupleVar = new Object2ObjectLinkedOpenHashMap<>(); 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 c4df7f309..7a733f9eb 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 @@ -126,8 +126,7 @@ public static void transform(ImProg prog, ImTranslator translator) { } lowerStringConcatenation(prog, translator); - lowerDivMod(prog); - lowerPrimitiveArrayBoundaryEnsure(prog, translator); + lowerDivMod(prog, translator); // Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper). // Populated lazily during the traversal. @@ -268,8 +267,8 @@ public void visit(ImOperatorCall call) { * handler's "was this an intentional abort" check. Leave that one * expression untouched so the existing recognition still fires. */ - private static void lowerDivMod(ImProg prog) { - DivModFunctions funcs = new DivModFunctions(); + private static void lowerDivMod(ImProg prog, ImTranslator translator) { + DivModFunctions funcs = new DivModFunctions(translator); prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImOperatorCall call) { @@ -352,119 +351,13 @@ private static int stacktraceParamIndex(ImFunction f) { return -1; } - /** - * 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); - ImFunction function = call.getFunc(); - if (!isExternalBoundary(function)) { - return; - } - for (ImExpr argument : new ArrayList<>(call.getArguments())) { - if (!(argument instanceof ImVarArrayAccess) - || isAlreadyNormalized(argument, translator)) { - continue; - } - 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) { - if (argument instanceof ImFunctionCall - && (((ImFunctionCall) argument).getFunc() == translator.ensureIntFunc - || ((ImFunctionCall) argument).getFunc() == translator.ensureBoolFunc - || ((ImFunctionCall) argument).getFunc() == translator.ensureRealFunc - || ((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) { - if (TypesHelper.isIntType(type)) { - return translator.ensureIntFunc; - } else if (TypesHelper.isBoolType(type)) { - return translator.ensureBoolFunc; - } else if (TypesHelper.isRealType(type)) { - return translator.ensureRealFunc; - } else if (TypesHelper.isStringType(type)) { - return translator.ensureStrFunc; - } - return null; - } - /** * Lazily builds (and memoizes) the div/mod helper functions and the tiny * raw-Lua-primitive natives they delegate to (Wurst's IM has no * floor-division/fmod operator of its own). */ private static final class DivModFunctions { + private final ImTranslator translator; private final List created = new ArrayList<>(); private ImFunction rawFloorDivInt; private ImFunction rawFmodInt; @@ -473,6 +366,10 @@ private static final class DivModFunctions { private ImFunction modInt; private ImFunction modReal; + private DivModFunctions(ImTranslator translator) { + this.translator = translator; + } + List createdFunctions() { return created; } @@ -508,6 +405,7 @@ ImFunction jassModInt() { private ImFunction rawFloorDivInt() { if (rawFloorDivInt == null) { rawFloorDivInt = rawNative("__wurst_rawFloorDivInt", TypesHelper.imInt()); + translator.luaRawFloorDivIntFunc = rawFloorDivInt; created.add(rawFloorDivInt); } return rawFloorDivInt; @@ -516,6 +414,7 @@ private ImFunction rawFloorDivInt() { private ImFunction rawFmodInt() { if (rawFmodInt == null) { rawFmodInt = rawNative("__wurst_rawFmodInt", TypesHelper.imInt()); + translator.luaRawFmodIntFunc = rawFmodInt; created.add(rawFmodInt); } return rawFmodInt; @@ -524,12 +423,13 @@ private ImFunction rawFmodInt() { private ImFunction rawFmodReal() { if (rawFmodReal == null) { rawFmodReal = rawNative("__wurst_rawFmodReal", TypesHelper.imReal()); + translator.luaRawFmodRealFunc = rawFmodReal; created.add(rawFmodReal); } return rawFmodReal; } - /** A native leaf with two params and a return, all of the same primitive type. Body supplied by LuaNatives. */ + /** A native leaf with two params and a return, translated as a Lua backend intrinsic. */ private static ImFunction rawNative(String name, ImType numType) { ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false); ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "b", false); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 796d6ad49..50f10fa97 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -136,12 +136,24 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { } } - LuaFunction f = tr.luaFunc.getFor(e.getFunc()); // Use the immutable ImFunction name rather than f.getName(), because f is a cached // LuaFunction object shared across all call sites of this native. The setName() calls // below mutate it, so f.getName() changes after the first translation and can no longer // be relied upon for sentinel checks. String imFuncName = e.getFunc().getName(); + if (isRawNumericIntrinsic(e.getFunc(), tr)) { + if (e.getArguments().size() != 2) { + throw new CompileError(e.attrTrace().attrSource(), + imFuncName + " expects exactly two arguments"); + } + LuaExpr left = e.getArguments().get(0).translateToLua(tr); + LuaExpr right = e.getArguments().get(1).translateToLua(tr); + if (e.getFunc() == tr.imTr.luaRawFloorDivIntFunc) { + return LuaAst.LuaExprBinary(left, LuaAst.LuaOpFloorDiv(), right); + } + return LuaAst.LuaExprFunctionCallByName("math.fmod", LuaAst.LuaExprlist(left, right)); + } + LuaFunction f = tr.luaFunc.getFor(e.getFunc()); if ("I2S".equals(imFuncName) && isIntentionalThreadAbortCall(e)) { return LuaAst.LuaExprFunctionCallByName("error", LuaAst.LuaExprlist( LuaAst.LuaExprStringVal(WURST_ABORT_THREAD_SENTINEL), @@ -156,6 +168,12 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { return LuaAst.LuaExprFunctionCall(f, tr.translateExprList(e.getArguments())); } + static boolean isRawNumericIntrinsic(ImFunction function, LuaTranslator tr) { + return function == tr.imTr.luaRawFloorDivIntFunc + || function == tr.imTr.luaRawFmodIntFunc + || function == tr.imTr.luaRawFmodRealFunc; + } + private static boolean isIntentionalThreadAbortCall(ImFunctionCall e) { if (e.getArguments().size() != 1) { return false; @@ -445,16 +463,7 @@ public static LuaExpr translate(ImVarAccess e, LuaTranslator tr) { return LuaAst.LuaExprVarAccess(tr.luaVar.getFor(e.getVar())); } - /** - * Primitive-typed array reads are wrapped in a type-normalizing helper - * call at the IM level, before the optimizer runs (see - * LuaNativeLowering#lowerPrimitiveArrayEnsure), by rewriting the read into - * a call against ImTranslator#ensureIntFunc and friends - so by the time - * an ImVarArrayAccess reaches this method, it is already either a - * genuine lvalue/raw access or an access whose type never needed - * wrapping (e.g. class/handle-typed arrays, which default to nil the - * same way an untouched Lua table key already does). - */ + /** Primitive-typed arrays carry their Wurst defaults through metatables, so every read is raw. */ public static LuaExpr translate(ImVarArrayAccess e, LuaTranslator tr) { return translateArrayAccessRaw(e, tr); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java index 4c1ed5298..ada9f0957 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java @@ -76,7 +76,11 @@ public void visit(LuaTableNamedField f) { @Override public void visit(LuaExprFunctionCallByName call) { super.visit(call); - check("call to", call.getFuncName()); + // Backend-owned qualified standard-library calls are valid Lua expressions, + // though they are deliberately not valid single identifiers. + if (!"math.fmod".equals(call.getFuncName())) { + check("call to", call.getFuncName()); + } } }); if (!invalid.isEmpty()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java index f4e9ef9c1..3e3797a04 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java @@ -138,24 +138,6 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("return math.ceil(x)")); }); - addNative("__wurst_rawFloorDivInt", f -> { - f.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("return a // b")); - }); - - addNative("__wurst_rawFmodInt", f -> { - f.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("return math.fmod(a, b)")); - }); - - addNative("__wurst_rawFmodReal", f -> { - f.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("return math.fmod(a, b)")); - }); - addNative(Arrays.asList("__wurst_rawToNumberInt", "__wurst_rawToNumberReal"), f -> { f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); f.getBody().add(LuaAst.LuaLiteral("return tonumber(x)")); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index ebe88d940..a7180712e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -725,6 +725,9 @@ private void translateFunc(ImFunction f) { // do not translate blizzard functions return; } + if (f.isNative() && ExprTranslation.isRawNumericIntrinsic(f, this)) { + return; + } LuaFunction lf = luaFunc.getFor(f); if (f.isNative()) { LuaNatives.get(lf); 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 ac17f6b75..b95c40e6f 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 @@ -2234,7 +2234,7 @@ public void integerDivModReferenceSemanticsInInterpreter() { * specifically to stay exempt from that rewrite. */ @Test - public void ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes() throws IOException { + public void stringConcatNilCheckSurvivesEliminateLocalTypes() throws IOException { test().testLua(true).executeProg().lines( "package Test", "native testSuccess()", @@ -2247,8 +2247,7 @@ public void ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes() throws " print(names[5])", " testSuccess()" ); - String compiled = compiledLua("ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes"); - assertNilCheckNotCorruptedToEmptyStringCheck(compiled, "__wurst_ensureStr("); + String compiled = compiledLua("stringConcatNilCheckSurvivesEliminateLocalTypes"); assertNilCheckNotCorruptedToEmptyStringCheck(compiled, "__wurst_stringConcat("); } @@ -2276,8 +2275,8 @@ public void genericNormalizationIsKeptAtNativeBoundaryOnly() { 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])")); + assertTrue("typed primitive arrays must cross native boundaries as raw reads:\n" + compiled, + compiled.contains("print(Test_values[1])")); } @Test @@ -2462,8 +2461,10 @@ public void erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts() th String compiled = compiledLua("erasedGenericPrimitiveDefaultsPropagateThroughCompositeContexts"); 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])")); + assertTrue("global primitive array reads must be raw table indexes", + compiled.contains("return Test_values[0]")); + assertFalse("typed array reads must not use erased-generic normalization", + compiled.contains("ensureInt(Test_values")); } @Test @@ -2518,9 +2519,9 @@ public void erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets() thr * 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 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 + * read. The intermediate generic functions and typed array reads must stay + * free of assurance calls, while erased generic values keep normalization + * at concrete uses. This is intentionally compile-only: the * generated native sinks have no Warcraft runtime implementation. */ @Test @@ -2562,25 +2563,98 @@ public void seededTypeAssuranceBoundaryFuzz() { ); assertFunctionBodyContains(compiled, "forward", "__wurst_ensure", false); - String readNormalization = type.equals("bool") - ? "(TypeAssuranceFuzz_values[" + arrayIndex + "] == true)" - : "__wurst_ensure" + suffix + "(TypeAssuranceFuzz_values[" + arrayIndex + "])"; - assertFunctionBodyContains(compiled, "read", readNormalization, true); + String rawArrayRead = "TypeAssuranceFuzz_values[" + arrayIndex + "]"; + assertFunctionBodyContains(compiled, "read", rawArrayRead, true); + assertFunctionBodyContains(compiled, "read", "__wurst_ensure", false); + assertFunctionBodyContains(compiled, "read", "== true", false); String genericArgument = type.equals("bool") ? "(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") - ? "(TypeAssuranceFuzz_values[" + arrayIndex + "] == true)" - : "__wurst_ensure" + suffix + "(TypeAssuranceFuzz_values[" + arrayIndex + "])"; - assertTrue("array boundary case " + caseIndex + " was not normalized:\n" + compiled, - compiled.contains(sink + "(" + arrayArgument + ")")); + assertTrue("array boundary case " + caseIndex + " was not emitted raw:\n" + compiled, + compiled.contains(sink + "(" + rawArrayRead + ")")); assertTrue("ordinary typed values must not be normalized at the boundary:\n" + compiled, compiled.contains(sink + "(" + literal + ")")); } } + @Test + public void typedPrimitiveArrayReadsAreRawInOptimizedLua() { + String compiled = compileOptimizedLua( + "LuaBackendAuditTests_typedPrimitiveArrayReadsAreRawInOptimizedLua", + primitiveArrayReadShapeLines() + ); + assertFalse("typed array reads must not call assurance helpers:\n" + compiled, + compiled.contains("__wurst_ensure")); + assertFalse("boolean array reads must not be normalized with a true comparison:\n" + compiled, + compiled.contains("== true)")); + } + + @Test + public void typedPrimitiveArrayDefaultsComeFromMetatables() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int array ints", + "real array reals", + "bool array bools", + "string array strings", + "function localDefault() returns int", + " int array[8] localInts", + " return localInts[7]", + "init", + " if ints[0] == 0 and ints[1000000] == 0", + " and reals[0] == 0. and reals[1000000] == 0.", + " and not bools[0] and not bools[1000000]", + " and strings[0] == \"\" and strings[1000000] == \"\"", + " and localDefault() == 0", + " ints[3] = 0", + " reals[3] = 0.", + " bools[3] = false", + " strings[3] = \"\"", + " if ints[3] == 0 and reals[3] == 0. and not bools[3] and strings[3] == \"\"", + " testSuccess()" + ); + } + + @Test + public void typedPrimitiveArrayReadsAreRawWithStacktraces() { + String compiled = compileLuaWithRunArgs( + "LuaBackendAuditTests_typedPrimitiveArrayReadsAreRawWithStacktraces", + new RunArgs().with("-lua", "-stacktraces"), + primitiveArrayReadShapeLines() + ); + assertFalse("stacktrace mode must not restore typed-array assurance calls:\n" + compiled, + compiled.contains("__wurst_ensure")); + assertFalse("stacktrace mode must not normalize boolean reads with a true comparison:\n" + compiled, + compiled.contains("== true)")); + } + + private static String[] primitiveArrayReadShapeLines() { + return new String[]{ + "package Test", + "native consumeInt(int value)", + "int array ints", + "real array reals", + "bool array bools", + "string array strings", + "function scan(int limit) returns real", + " int array[8] localInts", + " int i = 0", + " real sum = 0.", + " while i < limit", + " sum += ints[i] + reals[i] + localInts[i]", + " if bools[i] and strings[i] == \"\"", + " sum += 1", + " consumeInt(ints[i])", + " i++", + " return sum", + "init", + " scan(8)" + }; + } + private static void assertFunctionBodyContains(String compiled, String functionName, String text, boolean expected) { int start = compiled.indexOf("function " + functionName + "("); @@ -2608,22 +2682,20 @@ private void assertNilCheckNotCorruptedToEmptyStringCheck(String compiled, Strin * backend and the interpreter for negative operands * (e.g. -7 div 2 was -4 instead of -3, and 7 mod -2 was -1 instead of 1). * - * Div/mod are now lowered to portable IM functions before the optimizer - * runs (see LuaNativeLowering#lowerDivMod), so calls with constant - * arguments - like the ones below - may get inlined away entirely rather - * than showing up as a helper call in the output. The floor-div/fmod - * *native* they delegate to (Wurst has no such IM operator) always - * survives somewhere in the output, inlined or not, so checking for it - * is robust regardless of the inliner's decision. + * Div/mod are lowered to portable IM functions before the optimizer runs + * (see LuaNativeLowering#lowerDivMod). Their raw primitive calls are Lua + * backend intrinsics, so emitted code uses // and math.fmod directly. */ @Test public void integerDivModMatchJassSemanticsInLua() throws IOException { test().testLua(true).executeProg().lines(DIV_MOD_PROG); String compiled = compiledLua("integerDivModMatchJassSemanticsInLua"); - assertTrue("div must go through the truncating floor-div correction", - compiled.contains("__wurst_rawFloorDivInt(")); - assertTrue("mod must go through the ModuloInteger-compatible fmod correction", - compiled.contains("__wurst_rawFmodInt(")); + assertTrue("div must use Lua floor division inside the truncating correction", + compiled.contains(" // ")); + assertTrue("mod must use math.fmod inside the ModuloInteger-compatible correction", + compiled.contains("math.fmod(")); + assertFalse("raw numeric primitive calls must be intrinsic", + compiled.contains("__wurst_rawF")); assertFalse("mod/div must not use math.floor directly", compiled.contains("math.floor")); } @@ -2713,6 +2785,47 @@ public void nonConstantDivModCallsUseSharedHelper() throws IOException { 1, countOccurrences(compiled, "function __wurst_modInt(")); } + @Test + public void optimizedIntegerDivModUsesLuaPrimitivesInLoop() { + String compiled = compileOptimizedLua( + "LuaBackendAuditTests_optimizedIntegerDivModUsesLuaPrimitivesInLoop", + "package Test", + "native consumeInt(int value)", + "function run(int limit)", + " int x = limit", + " while x > 0", + " consumeInt(x div 8)", + " consumeInt(x mod 8)", + " x--", + "init", + " run(32)" + ); + assertTrue("optimized div must contain Lua floor division:\n" + compiled, + compiled.contains(" // 8")); + assertTrue("optimized mod must contain math.fmod:\n" + compiled, + compiled.contains("math.fmod(")); + assertFalse("optimized loop must not call raw numeric helpers:\n" + compiled, + compiled.contains("__wurst_raw")); + } + + @Test + public void numericIntrinsicRecognitionUsesFunctionIdentity() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "function __wurst_rawFmodInt(int a, int b) returns int", + " return 123", + "init", + " if __wurst_rawFmodInt(7, 2) == 123", + " testSuccess()" + ); + String compiled = compiledLua("numericIntrinsicRecognitionUsesFunctionIdentity"); + assertTrue("an ordinary same-named function must keep its definition:\n" + compiled, + compiled.contains("function __wurst_rawFmodInt(")); + assertFalse("an ordinary same-named call must not lower to fmod:\n" + compiled, + compiled.contains("math.fmod(")); + } + /** * String concatenation is lowered to a synthetic stringConcat IM function. * The polyfill and its call sites used to be linked only by both happening @@ -2819,22 +2932,16 @@ public void optimizedMovedImHelpersHaveNoDanglingReferences() { ); String[] helperNames = { - "__wurst_ensureInt", "__wurst_ensureBool", "__wurst_ensureReal", "__wurst_ensureStr", "__wurst_stringConcat", "__wurst_intDiv", "__wurst_modInt", "__wurst_modReal", - "__wurst_rawToNumberInt", "__wurst_rawToInteger", "__wurst_rawToNumberReal", - "__wurst_rawToString", "__wurst_rawConcat", "__wurst_rawFloorDivInt", - "__wurst_rawFmodInt", "__wurst_rawFmodReal" + "__wurst_rawConcat" }; for (String helperName : helperNames) { assertHelperDefinedWhenCalled(compiled, helperName); } - assertTrue("repro must exercise integer ensure lowering", compiled.contains("__wurst_rawToNumberInt")); - assertTrue("repro must exercise real ensure lowering", compiled.contains("__wurst_rawToNumberReal")); - assertTrue("repro must exercise string ensure lowering", compiled.contains("__wurst_rawToString")); assertTrue("repro must exercise string concat lowering", compiled.contains("__wurst_rawConcat")); - assertTrue("repro must exercise integer div lowering", compiled.contains("__wurst_rawFloorDivInt")); - assertTrue("repro must exercise integer mod lowering", compiled.contains("__wurst_rawFmodInt")); - assertTrue("repro must exercise real mod lowering", compiled.contains("__wurst_rawFmodReal")); + assertTrue("repro must exercise integer div lowering", compiled.contains(" // ")); + assertTrue("repro must exercise integer and real mod lowering", compiled.contains("math.fmod(")); + assertFalse("raw numeric primitive calls must not survive Lua emission", compiled.contains("__wurst_rawF")); } /** 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 ccd7b311f..1405c74ba 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 stringArrayReadIsEnsuredAtNativeBoundary() throws IOException { + public void stringArrayReadIsRawAtNativeBoundary() throws IOException { test().testLua(true).withStdLib().lines( "package Test", "string array playerName", @@ -511,8 +511,10 @@ public void stringArrayReadIsEnsuredAtNativeBoundary() throws IOException { " let i = 0", " SetPlayerName(Player(i), playerName[i])" ); - String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsEnsuredAtNativeBoundary.lua"), Charsets.UTF_8); - assertTrue("native boundary must normalize an array read", + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsRawAtNativeBoundary.lua"), Charsets.UTF_8); + assertTrue("native boundary must receive a raw typed array read", + compiled.contains(", Test_playerName[")); + assertFalse("typed array reads must not use erased-generic normalization", compiled.contains("__wurst_ensureStr(Test_playerName[")); }