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..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 @@ -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,10 +81,44 @@ 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(); } + } else if (parent instanceof StmtForRange) { + StmtForRange forRange = (StmtForRange) parent; + 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(); @@ -110,6 +153,14 @@ 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) { + WurstType selectedType = constructorParameterType(selected, paramIndex); + if (!(selectedType instanceof WurstTypeUnknown)) { + return selectedType; + } + } ClassDef c = constr.attrNearestClassDef(); if (c == null) { return WurstTypeUnknown.instance(); @@ -125,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); @@ -136,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 f708c5285..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.*; @@ -400,8 +401,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 = AttrExprExpectedType.constructorParameterType(calledConstr, i); + 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/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index d540afdc7..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 @@ -201,7 +201,16 @@ private ImClass createClass() { OverrideUtils.addOverrideClosure(tr, superMethod, m, e); - ImExpr translated = e.getImplementation().imTranslateExpr(tr, impl); + ImExpr translated; + boolean propagatesExpectedType = ExprTranslation.isCompositeExpectedTypeExpression(e.getImplementation()); + if (propagatesExpectedType) { + 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 5420899d0..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 @@ -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,13 +100,19 @@ 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 = t.isLuaTarget() + && actualType instanceof WurstTypeBoundTypeParam + && e.getParent() instanceof Arguments + ? AttrExprExpectedType.afterOverloading(e) + : e.attrExpectedTypRaw(); return wrapTranslation(e, t, translated, actualType, expectedTypRaw); } 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,13 +132,32 @@ 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. Equality with true preserves false while + // mapping nil (and other non-true values) to false. + if (ensureType == t.ensureBoolFunc) { + return ImOperatorCall(WurstOperator.EQ, ImExprs( + translated, ImBoolVal(true))); + } return ImFunctionCall(trace, ensureType, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL); } } 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) { @@ -168,14 +194,32 @@ 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); + // 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) || indexContext)) { + return wrapLua(trace, t, translated, actualType); + } + return translated; } 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 (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); @@ -207,6 +251,27 @@ 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 + && 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); @@ -216,6 +281,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()) { @@ -659,8 +725,15 @@ && 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); + FunctionSignature selectedSignature = t.isLuaTarget() ? e.attrFunctionSignature() : null; + ImExprs imArgs = translateExprs(arguments, t, f, normalizeAtBoundary, selectedSignature); if (calledFunc instanceof TupleDef) { // creating a new tuple... @@ -686,7 +759,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 +857,80 @@ 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) { + 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) { - result.add(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); + } + result.add(translated); } return result; } + 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 + || 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."); } @@ -802,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) { @@ -844,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) { @@ -856,8 +1002,18 @@ 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(); + boolean propagatesExpectedType = isCompositeExpectedTypeExpression(returnedExpr); + ImExpr expr = propagatesExpectedType + ? translateWithExpectedType(returnedExpr, translator, f, expectedType) + : returnedExpr.imTranslateExpr(translator, f); + if (!propagatesExpectedType) { + expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), expectedType); + } + ImExpr result = JassIm.ImStatementExpr(statements, expr); + return propagatesExpectedType + ? result + : wrapTranslation(e, translator, result, e.attrTypRaw(), expectedType); } else { return ImHelper.statementExprVoid(statements); } @@ -910,6 +1066,58 @@ 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); + } + 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); + ImExpr translated = ImOperatorCall(unary.getOpU(), ImExprs(right)); + 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); + 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/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..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,21 +71,16 @@ static ImFunction buildEnsureInt(List out) { return f; } - /** local result = false; if x ~= nil then result = x end; return result */ + /** 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); - 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, isTrue(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; } @@ -196,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/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..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 @@ -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,56 +334,116 @@ private static boolean isIntentionalThreadAbortDivByZero(ImOperatorCall call) { && "I2S".equals(parentCall.getFunc().getName()); } + private static ImFunctionCall callWithStacktrace(de.peeeq.wurstscript.ast.Element trace, ImFunction f, ImExprs args) { + int stacktraceIndex = stacktraceParamIndex(f); + if (stacktraceIndex >= 0) { + args.add(stacktraceIndex, JassIm.ImStringVal("when calling " + f.getName() + + StackTraceInjector2.getCallPos(trace.attrErrorPos()))); + } + return JassIm.ImFunctionCall(trace, f, JassIm.ImTypeArguments(), args, false, CallType.NORMAL); + } + + private static int stacktraceParamIndex(ImFunction f) { + for (int i = 0; i < f.getParameters().size(); i++) { + if (StackTraceInjector2.STACK_POS_PARAM.equals(f.getParameters().get(i).getName())) { + return i; + } + } + return -1; + } + /** - * 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. + * 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 lowerPrimitiveArrayEnsure(ImProg prog, ImTranslator translator) { + private static void lowerPrimitiveArrayBoundaryEnsure(ImProg prog, ImTranslator translator) { prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImVarArrayAccess access) { super.visit(access); - if (LValues.isUsedAsLValue(access)) { + if (access.isUsedAsLValue() || isAlreadyNormalized(access, translator) + || isAlreadyNormalizedAccess(access, translator)) { return; } - ImFunction ensureFunc = ensureFunctionFor(access.attrTyp(), translator); - if (ensureFunc == null) { + replaceWithEnsure(access, access.attrTrace(), translator); + } + + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + ImFunction function = call.getFunc(); + if (!isExternalBoundary(function)) { return; } - access.replaceBy(callWithStacktrace(access.attrTrace(), ensureFunc, JassIm.ImExprs(access.copy()))); + for (ImExpr argument : new ArrayList<>(call.getArguments())) { + if (!(argument instanceof ImVarArrayAccess) + || isAlreadyNormalized(argument, translator)) { + continue; + } + replaceWithEnsure((ImVarArrayAccess) argument, call.attrTrace(), translator); + } } }); } - private static ImFunctionCall callWithStacktrace(de.peeeq.wurstscript.ast.Element trace, ImFunction f, ImExprs args) { - int stacktraceIndex = stacktraceParamIndex(f); - if (stacktraceIndex >= 0) { - args.add(stacktraceIndex, JassIm.ImStringVal("when calling " + f.getName() - + StackTraceInjector2.getCallPos(trace.attrErrorPos()))); + private static void replaceWithEnsure(ImVarArrayAccess access, de.peeeq.wurstscript.ast.Element trace, + ImTranslator translator) { + ImFunction ensure = ensureFunctionFor(access.attrTyp(), translator); + if (ensure == null) { + return; } - return JassIm.ImFunctionCall(trace, f, JassIm.ImTypeArguments(), args, false, CallType.NORMAL); + 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 int stacktraceParamIndex(ImFunction f) { - for (int i = 0; i < f.getParameters().size(); i++) { - if (StackTraceInjector2.STACK_POS_PARAM.equals(f.getParameters().get(i).getName())) { - return i; - } + 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; } - return -1; + 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/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..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 @@ -13,6 +13,8 @@ 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.WurstTypeIntLiteral; import de.peeeq.wurstscript.types.WurstTypeVararg; import org.eclipse.jdt.annotation.Nullable; @@ -293,8 +295,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 +312,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; } @@ -378,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); } @@ -471,7 +487,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(); @@ -520,6 +539,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 7d0a3a15d..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 @@ -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,346 @@ 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 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()", + "native testFail(string message)", + "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()", + " for i = 1 to box.get()", + " testSuccess()", + " switch box.get()", + " case 0", + " testSuccess()", + " default", + " testFail(\"switch\")", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericPrimitiveDefaultsAreNormalizedAtConcreteUse"); + assertTrue("concrete generic use must normalize an erased integer", + 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(")); + } + + @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))")); + } + + @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))")); + } + + @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))")); + } + + @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", + "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", + " return -1", + "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()", + " 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 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()", + " 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)", + " end", + " int switchValue = -1", + " switch (useBox ? box.get() : 1)", + " case 0", + " 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 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", 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])")); + } + + @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 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", + " return \"\"", + " function op_indexAssign(int index, int value)", + " assignedDefault = value == 0", + "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 child.get() == 1 and indexed.assignedDefault", + " testSuccess()" + ); + String compiled = compiledLua("erasedGenericDefaultsUseResolvedAssignmentAndDelegationTargets"); + assertEquals("resolved primitive consumers must normalize their erased generic input", 4, + 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 + * 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 + * 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); + 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 + "))"; + 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("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 +2710,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 +2725,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 = { @@ -2429,13 +2766,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))))" ); } 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..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 @@ -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