From d8fc3c2add3cca9b8634f36a0f5e67441604e753 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 13:31:11 +0200 Subject: [PATCH 1/9] Add generic construction and target field mapping --- .../peeeq/wurstio/WurstCompilerJassImpl.java | 20 ++ .../peeeq/wurstscript/CompilerIntrinsics.java | 36 +++ .../de/peeeq/wurstscript/SyntacticSugar.java | 163 ++++++++---- .../wurstscript/attributes/AttrFuncDef.java | 4 + .../attributes/AttrFunctionSignature.java | 9 + .../interpreter/EvaluateExpr.java | 40 +++ .../imtranslation/EliminateGenerics.java | 240 +++++++++++++++++- .../imtranslation/ExprTranslation.java | 9 + .../imtranslation/ImTranslator.java | 16 ++ .../validation/WurstValidator.java | 59 +++++ .../tests/FieldIterationTests.java | 190 ++++++++++++-- .../wurstscript/tests/ModelManagerTests.java | 2 +- 12 files changed, 713 insertions(+), 75 deletions(-) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index 5f38f5f97..ab10a27d8 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -877,6 +877,11 @@ public LuaCompilationUnit transformProgToLua() { ImAttrType.setWurstClassType(null); int stage; + if (containsGenericNewCall()) { + beginPhase(2, "Specialize generics for generic construction"); + new EliminateGenerics(getImTranslator(), getImProg()).transformGenericNewOnly(); + timeTaker.endPhase(); + } if (runArgs.isNoDebugMessages()) { beginPhase(3, "remove debug messages"); DebugMessageRemover.removeDebugMessages(imProg); @@ -963,4 +968,19 @@ public LuaCompilationUnit transformProgToLua() { timeTaker.endPhase(); return luaCode; } + + private boolean containsGenericNewCall() { + boolean[] found = {false}; + getImProg().accept(new de.peeeq.wurstscript.jassIm.Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + if (getImTranslator().isGenericNewMarker(call.getFunc())) { + found[0] = true; + return; + } + super.visit(call); + } + }); + return found[0]; + } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java new file mode 100644 index 000000000..8b4a2cb77 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -0,0 +1,36 @@ +package de.peeeq.wurstscript; + +import de.peeeq.wurstscript.ast.ExprClosure; +import de.peeeq.wurstscript.ast.ExprFunctionCall; + +/** Source-level compiler intrinsics which must be eliminated before backend emission. */ +public final class CompilerIntrinsics { + + public static final String FOR_FIELDS = "forFields"; + public static final String MAP_FIELDS = "mapFields"; + public static final String NEW = "newInstance"; + public static final String NEW_MARKER = "wurstNewMarker"; + + private CompilerIntrinsics() { + } + + public static boolean isForFields(ExprFunctionCall call) { + return FOR_FIELDS.equals(call.getFuncName()) && hasClosureArgument(call); + } + + public static boolean isMapFields(ExprFunctionCall call) { + return MAP_FIELDS.equals(call.getFuncName()) && hasClosureArgument(call); + } + + public static boolean isFieldIteration(ExprFunctionCall call) { + return isForFields(call) || isMapFields(call); + } + + public static boolean isNew(ExprFunctionCall call) { + return NEW.equals(call.getFuncName()); + } + + private static boolean hasClosureArgument(ExprFunctionCall call) { + return call.getArgs().stream().anyMatch(arg -> arg instanceof ExprClosure); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 63289239c..3610d7956 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -3,7 +3,9 @@ import com.google.common.collect.Maps; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.parser.WPos; +import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeClass; +import de.peeeq.wurstscript.types.WurstTypeTypeParam; import java.util.*; @@ -15,9 +17,7 @@ */ public class SyntacticSugar { - /** Compiler-reserved spelling keeps ordinary user functions named forFields/mapFields valid. */ - private static final String FOR_FIELDS = "__wurst_forFields"; - private static final String MAP_FIELDS = "__wurst_mapFields"; + private int generatedTargetCounter; public static final class DeferredModuleCall { private final WStatements statements; private final int index; @@ -63,11 +63,12 @@ private String key() { } public static boolean isFieldIterationIntrinsic(ExprFunctionCall call) { - return FOR_FIELDS.equals(call.getFuncName()) || MAP_FIELDS.equals(call.getFuncName()); + return CompilerIntrinsics.isFieldIteration(call); } public static boolean isUninstantiatedModuleFieldIteration(ExprFunctionCall call) { return isFieldIterationIntrinsic(call) + && call.getArgs().size() == 1 && call.attrNearestClassDef() == null && call.attrNearestClassOrModule() instanceof ModuleDef; } @@ -143,8 +144,8 @@ public void restoreModuleTemplateFieldIterations(List detach * field accesses. * *
-     * __wurst_forFields((name, value) -> writer.write(name, value))
-     * __wurst_mapFields((name, value) -> reader.read(name, value))
+     * forFields((name, value) -> writer.write(name, value))
+     * mapFields((name, value) -> reader.read(name, value))
      * 
*/ private List expandFieldIterationsInTree(CompilationUnit root) { @@ -161,7 +162,7 @@ public void visit(ExprFunctionCall call) { List detached = new ArrayList<>(); for (ExprFunctionCall call : calls) { - expandFieldIteration(call, MAP_FIELDS.equals(call.getFuncName()), detached); + expandFieldIteration(call, CompilerIntrinsics.isMapFields(call), detached); } return detached; } @@ -173,11 +174,13 @@ private void expandFieldIteration(ExprFunctionCall call, call.addError(call.getFuncName() + " can only be used as a statement."); return; } - ClassDef classDef = call.attrNearestClassDef(); - ClassOrModule owner = call.attrNearestClassOrModule(); - if (call.getArgs().size() != 1 || !(call.getArgs().get(0) instanceof ExprClosure closure) + boolean explicitTarget = call.getArgs().size() == 2; + int closureIndex = explicitTarget ? 1 : 0; + if ((!explicitTarget && call.getArgs().size() != 1) + || !(call.getArgs().get(closureIndex) instanceof ExprClosure closure) || closure.getShortParameters().size() != 2) { - call.addError(call.getFuncName() + " expects a closure with (fieldName, fieldValue) parameters."); + call.addError(call.getFuncName() + + " expects a closure with (fieldName, fieldValue) parameters, optionally preceded by a target."); return; } for (WShortParameter parameter : closure.getShortParameters()) { @@ -203,34 +206,76 @@ private void expandFieldIteration(ExprFunctionCall call, call.addError("forFields closure must produce a statement expression."); return; } - if (!call.attrIsDynamicContext()) { - call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); - return; - } - if (owner instanceof ModuleDef) { - // Module bodies are templates. Their copies were made by ModuleExpander; validate and - // expand those concrete copies instead of type-checking this uninstantiated template. - return; - } - if (classDef == null) { - call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); - return; + ClassDef classDef; + ClassOrModule owner; + Expr target = null; + String targetName = null; + LocalVarDef targetVariable = null; + int originalStatementIndex = statements.indexOf(call); + if (explicitTarget) { + target = call.getArgs().get(0); + targetName = "__wurstFieldTarget" + generatedTargetCounter++; + targetVariable = Ast.LocalVarDef(call.getSource(), Ast.Modifiers(), Ast.NoTypeExpr(), + Ast.Identifier(call.getSource(), targetName), target.copy()); + statements.add(originalStatementIndex, targetVariable); + statements.clearAttributes(); + WurstType targetType = target.attrTyp(); + if (targetType instanceof WurstTypeTypeParam) { + statements.remove(targetVariable); + statements.clearAttributes(); + call.addError(call.getFuncName() + " target type " + targetType + + " is not concrete here. Move field mapping into a callback with a concrete target type."); + return; + } + if (!(targetType instanceof WurstTypeClass targetClass) || targetClass.isStaticRef()) { + statements.remove(targetVariable); + statements.clearAttributes(); + call.addError(call.getFuncName() + " target must have a concrete class type, but found " + + targetType + "."); + return; + } + classDef = targetClass.getClassDef(); + owner = classDef; + } else { + classDef = call.attrNearestClassDef(); + owner = call.attrNearestClassOrModule(); + if (!call.attrIsDynamicContext()) { + call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); + return; + } + if (owner instanceof ModuleDef) { + // Module bodies are templates. Their copies were made by ModuleExpander; validate and + // expand those concrete copies instead of type-checking this uninstantiated template. + return; + } + if (classDef == null) { + call.addError(call.getFuncName() + " can only be used in an instance method or constructor."); + return; + } } - List fields = collectInstanceFields(classDef, owner); + List fields = collectInstanceFields(classDef, owner, call, explicitTarget); if (fields.isEmpty()) { - call.addError(call.getFuncName() + " requires at least one instance field."); + if (targetVariable != null) { + statements.remove(targetVariable); + statements.clearAttributes(); + } + call.addError(call.getFuncName() + + " requires at least one instance field; no accessible mutable instance fields were found."); return; } int statementIndex = statements.indexOf(call); - detached.add(new DeferredModuleCall(statements, statementIndex, call)); + detached.add(new DeferredModuleCall(statements, originalStatementIndex, call)); statements.remove(statementIndex); - List generatedStatements = new ArrayList<>(fields.size()); + List generatedStatements = new ArrayList<>(fields.size() + (explicitTarget ? 1 : 0)); + if (targetVariable != null) { + generatedStatements.add(targetVariable); + } for (FieldInfo field : fields) { String fieldKey = field.key(); - Expr fieldAccess = fieldAccess(call.getSource(), field); + Expr fieldAccess = fieldAccess(call.getSource(), field, targetName); Expr implementation = substituteFieldParameters( - closure.getImplementation().copy(), nameParameter, valueParameter, fieldKey, field); + closure.getImplementation().copy(), nameParameter, valueParameter, fieldKey, field, targetName); WStatement expanded; if (assignsResult) { expanded = Ast.StmtSet(call.getSource(), (LExpr) fieldAccess, implementation); @@ -241,17 +286,19 @@ private void expandFieldIteration(ExprFunctionCall call, statements.add(statementIndex++, expanded); } detached.set(detached.size() - 1, - new DeferredModuleCall(statements, statementIndex - fields.size(), call, generatedStatements)); + new DeferredModuleCall(statements, originalStatementIndex, call, generatedStatements)); } - private List collectInstanceFields(ClassDef classDef, ClassOrModule owner) { + private List collectInstanceFields(ClassDef classDef, ClassOrModule owner, + Element accessSite, boolean explicitTarget) { List fields = new ArrayList<>(); if (classDef != null) { collectInheritedFields(classDef.attrTypC(), fields, classDef, Collections.newSetFromMap(new IdentityHashMap<>()), - Collections.newSetFromMap(new IdentityHashMap<>())); + Collections.newSetFromMap(new IdentityHashMap<>()), accessSite, explicitTarget); } else if (owner instanceof ModuleDef moduleDef) { - addInstanceFields(moduleDef.getVars(), fields, null, List.of(), moduleDef); + addInstanceFields(moduleDef.getVars(), fields, null, List.of(), moduleDef, + accessSite, explicitTarget); } return fields; } @@ -260,24 +307,30 @@ private void collectInheritedFields(WurstTypeClass type, List fields, ClassDef concreteClass, Set visitedClasses, - Set visitedModules) { + Set visitedModules, + Element accessSite, + boolean explicitTarget) { if (!visitedClasses.add(type.getClassDef())) { return; } WurstTypeClass superType = type.extendedClass(); if (superType != null) { - collectInheritedFields(superType, fields, concreteClass, visitedClasses, visitedModules); + collectInheritedFields(superType, fields, concreteClass, visitedClasses, visitedModules, + accessSite, explicitTarget); } addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, - visitedModules, List.of()); - addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null); + visitedModules, List.of(), accessSite, explicitTarget); + addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null, + accessSite, explicitTarget); } private void addModuleFields(Iterable modules, List fields, ClassDef concreteClass, Set visited, - List parentPath) { + List parentPath, + Element accessSite, + boolean explicitTarget) { for (ModuleInstanciation module : modules) { if (!visited.add(module)) { continue; @@ -287,8 +340,10 @@ private void addModuleFields(Iterable modules, List modulePath = parentPath.isEmpty() ? List.of(module.getName()) : parentPath; - addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited, modulePath); - addInstanceFields(module.getVars(), fields, concreteClass, modulePath, module.attrModuleOrigin()); + addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited, modulePath, + accessSite, explicitTarget); + addInstanceFields(module.getVars(), fields, concreteClass, modulePath, module.attrModuleOrigin(), + accessSite, explicitTarget); } } @@ -296,13 +351,18 @@ private void addInstanceFields(Iterable declarations, List fields, ClassDef concreteClass, List modulePath, - ModuleDef declaringModule) { + ModuleDef declaringModule, + Element accessSite, + boolean explicitTarget) { for (GlobalVarDef field : declarations) { - boolean privateFromAnotherClass = field.attrIsPrivate() - && concreteClass != null - && field.attrNearestClassDef() != concreteClass; + ClassDef declaringClass = field.attrNearestClassDef(); + boolean privateFromAnotherClass = field.attrIsPrivate() && concreteClass != null + && (explicitTarget + ? declaringClass == null || !accessSite.isSubtreeOf(declaringClass) + : declaringClass != concreteClass); boolean privateFromAnotherModule = field.attrIsPrivate() && declaringModule != null; - if (!field.attrIsStatic() && !privateFromAnotherClass && !privateFromAnotherModule) { + if (!field.attrIsStatic() && !field.attrIsReadonly() && !field.attrIsConstant() + && !privateFromAnotherClass && !privateFromAnotherModule) { fields.add(new FieldInfo(field, modulePath)); } } @@ -328,13 +388,14 @@ public void visit(LocalVarDef localVarDef) { } private Expr substituteFieldParameters(Expr expression, String nameParameter, - String valueParameter, String fieldName, FieldInfo field) { + String valueParameter, String fieldName, FieldInfo field, + String targetName) { if (expression instanceof ExprVarAccess access) { if (access.getVarName().equals(nameParameter)) { return Ast.ExprStringVal(access.getSource(), fieldName); } if (access.getVarName().equals(valueParameter)) { - return fieldAccess(access.getSource(), field); + return fieldAccess(access.getSource(), field, targetName); } } @@ -436,15 +497,17 @@ public void visit(ExprVarAccess access) { for (ExprVarAccess access : accesses) { Expr replacement = access.getVarName().equals(nameParameter) ? Ast.ExprStringVal(access.getSource(), fieldName) - : fieldAccess(access.getSource(), field); + : fieldAccess(access.getSource(), field, targetName); access.replaceBy(replacement); } return expression; } - private ExprMemberVarDot fieldAccess(WPos source, FieldInfo field) { + private ExprMemberVarDot fieldAccess(WPos source, FieldInfo field, String targetName) { Expr left; - if (field.modulePath.isEmpty()) { + if (targetName != null) { + left = Ast.ExprVarAccess(source, Ast.Identifier(source, targetName)); + } else if (field.modulePath.isEmpty()) { left = Ast.ExprThis(source); } else { left = Ast.ExprVarAccess(source, Ast.Identifier(source, field.modulePath.get(0))); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 74d8940d3..87b8ef82a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.attributes.names.FuncLink; @@ -292,6 +293,9 @@ private ToStringConversionResolution(@Nullable FuncLink conversion, @Nullable St if (isConstructorThisCall(node)) { return null; } + if (CompilerIntrinsics.isNew(node)) { + return null; + } FuncLink result = searchFunction(node.getFuncName(), node, argumentTypes(node)); if (result == null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java index 1f777ea50..882bcd093 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java @@ -1,5 +1,6 @@ package de.peeeq.wurstscript.attributes; +import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.types.FunctionSignature; @@ -12,12 +13,20 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class AttrFunctionSignature { public static FunctionSignature calculate(StmtCall fc) { + if (fc instanceof ExprFunctionCall call && CompilerIntrinsics.isNew(call)) { + WurstType returnType = call.getTypeArgs().size() == 1 + ? call.getTypeArgs().get(0).attrTyp() + : WurstTypeUnknown.instance(); + return new FunctionSignature(null, VariableBinding.emptyMapping(), null, + CompilerIntrinsics.NEW, Collections.emptyList(), Collections.emptyList(), returnType); + } Collection sigs = fc.attrPossibleFunctionSignatures(); List at = argTypes(fc); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 3b3b54844..7f07951df 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -1,6 +1,7 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; import de.peeeq.wurstio.jassinterpreter.InterpreterException; +import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.PackageOrGlobal; @@ -8,6 +9,7 @@ import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.intermediatelang.*; import de.peeeq.wurstscript.jassIm.*; +import de.peeeq.wurstscript.translation.imtranslation.CallType; import de.peeeq.wurstscript.translation.imtranslation.ImPrinter; import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.Utils; @@ -38,6 +40,9 @@ public static ILconst eval(ImFuncRef e, ProgramState globalState, LocalState loc mark(e, globalState); ImFunction f = e.getFunc(); + if (CompilerIntrinsics.NEW_MARKER.equals(f.getName()) && f.getParent() == null) { + return evaluateGenericNew(e, globalState); + } ImExprs arguments = e.getArguments(); ILconst[] args = new ILconst[arguments.size()]; @@ -48,6 +53,41 @@ public static ILconst eval(ImFuncRef e, ProgramState globalState, LocalState loc return ILInterpreter.runFunc(globalState, f, e, args).getReturnVal(); } + private static ILconst evaluateGenericNew(ImFunctionCall markerCall, ProgramState globalState) { + if (markerCall.getTypeArguments().size() != 1) { + throw new InterpreterException(markerCall.attrTrace(), + CompilerIntrinsics.NEW + " expects exactly one type argument."); + } + ImType resolved = globalState.resolveType(markerCall.getTypeArguments().get(0).getType()); + if (!(resolved instanceof ImClassType classType)) { + throw new InterpreterException(markerCall.attrTrace(), + CompilerIntrinsics.NEW + " requires a concrete class type, but found " + resolved + "."); + } + + ImFunction constructor = null; + for (ImFunction candidate : classType.getClassDef().getFunctions()) { + if (candidate.getTrace() instanceof de.peeeq.wurstscript.ast.ConstructorDef + && candidate.getParameters().isEmpty() + && !(candidate.getReturnType() instanceof ImVoid)) { + constructor = candidate; + break; + } + } + if (constructor == null) { + throw new InterpreterException(markerCall.attrTrace(), + CompilerIntrinsics.NEW + " could not find the zero-argument constructor for " + + classType.getClassDef().getName() + "."); + } + + ImTypeArguments constructorTypeArguments = JassIm.ImTypeArguments(); + for (ImTypeArgument argument : classType.getTypeArguments()) { + constructorTypeArguments.add(argument.copy()); + } + ImFunctionCall constructorCall = JassIm.ImFunctionCall(markerCall.getTrace(), constructor, + constructorTypeArguments, JassIm.ImExprs(), false, CallType.NORMAL); + return ILInterpreter.runFunc(globalState, constructor, constructorCall, new ILconst[0]).getReturnVal(); + } + public static @Nullable ILconst evaluateFunc(ProgramState globalState, LocalState localState, ImFunction f, List args2, Element trace) { ILconst[] args = new ILconst[args2.size()]; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index da6b1615d..b57423688 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -1,8 +1,11 @@ package de.peeeq.wurstscript.translation.imtranslation; import com.google.common.collect.*; +import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.ClassDef; +import de.peeeq.wurstscript.ast.ConstructorDef; +import de.peeeq.wurstscript.ast.InterfaceDef; import de.peeeq.wurstscript.ast.PackageOrGlobal; import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.attributes.CompileError; @@ -23,6 +26,7 @@ public class EliminateGenerics { private final ImTranslator translator; private final ImProg prog; + private boolean genericNewOnly; private final Deque genericsUses = new ArrayDeque<>(); private final Table specializedFunctions = HashBasedTable.create(); private final Table specializedMethods = HashBasedTable.create(); @@ -72,6 +76,9 @@ public void transform() { eliminateGenericUses(); dbg(summary("after eliminateGenericUses")); + eliminateRemainingGenericNewCalls(); + eliminateGenericUses(); + dbgMethodsByName("after eliminateGenericUses"); makeNullAssignmentsSafe(); @@ -82,12 +89,119 @@ public void transform() { removeGenericConstructs(); dbg(summary("after removeGenericConstructs")); + assertNoGenericNewMarkers(); + dbg(checkDanglingMethodRefs("end")); // TODO fix or remove this check // assertNoUnspecializedGenericGlobals(); } + /** + * Lua normally erases new generics. Generic construction is the one operation which needs the + * concrete type, so only specialize functions on paths leading to {@code newInstance}. All other + * generic calls and classes keep the Lua backend's normal erased representation. + */ + public void transformGenericNewOnly() { + genericNewOnly = true; + collectGenericNewRoots(); + eliminateGenericUses(); + eliminateRemainingGenericNewCalls(); + assertNoReachableGenericNewMarkers(); + } + + private void collectGenericNewRoots() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunction function) { + if (!function.getTypeVariables().isEmpty()) { + return; + } + super.visit(function); + } + + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + collectGenericNewUse(call); + } + }); + } + + private void collectGenericNewUses(Element element) { + element.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + collectGenericNewUse(call); + } + }); + } + + private void collectGenericNewUse(ImFunctionCall call) { + if (translator.isGenericNewMarker(call.getFunc())) { + if (!typeArgumentsContainTypeVariable(call.getTypeArguments())) { + genericsUses.add(new GenericNewCall(call)); + } + return; + } + if (!call.getTypeArguments().isEmpty() + && functionContainsGenericNew(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>()))) { + if (!typeArgumentsContainTypeVariable(call.getTypeArguments())) { + genericsUses.add(new GenericImFunctionCall(call)); + } + } + } + + private boolean typeArgumentsContainTypeVariable(ImTypeArguments typeArguments) { + for (ImTypeArgument typeArgument : typeArguments) { + if (containsTypeVariable(typeArgument.getType())) { + return true; + } + } + return false; + } + + private boolean functionContainsGenericNew(ImFunction function, Set visited) { + if (!visited.add(function)) { + return false; + } + boolean[] found = {false}; + function.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + if (translator.isGenericNewMarker(call.getFunc()) + || functionContainsGenericNew(call.getFunc(), visited)) { + found[0] = true; + return; + } + super.visit(call); + } + }); + return found[0]; + } + + private void assertNoReachableGenericNewMarkers() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunction function) { + if (!function.getTypeVariables().isEmpty()) { + return; + } + super.visit(function); + } + + @Override + public void visit(ImFunctionCall call) { + if (translator.isGenericNewMarker(call.getFunc())) { + throw new CompileError(call, CompilerIntrinsics.NEW + + " requires its type argument to resolve to a concrete class."); + } + super.visit(call); + } + }); + } + private void assertNoUnspecializedGenericGlobals() { prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImVarAccess va) { @@ -105,6 +219,21 @@ private void assertNoUnspecializedGenericGlobals() { }); } + private void assertNoGenericNewMarkers() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + if (translator.isGenericNewMarker(call.getFunc())) { + ImFunction owner = enclosingFunction(call); + throw new CompileError(call, "Internal error: " + CompilerIntrinsics.NEW + + " was not lowered in " + (owner == null ? "" : owner.getName()) + + "."); + } + } + }); + } + private void makeNullAssignmentsSafe() { prog.accept(new Element.DefaultVisitor() { @Override @@ -501,6 +630,30 @@ private void eliminateGenericUses() { } } + private void eliminateRemainingGenericNewCalls() { + List calls = new ArrayList<>(); + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunction function) { + if (!function.getTypeVariables().isEmpty()) { + return; + } + super.visit(function); + } + + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + if (translator.isGenericNewMarker(call.getFunc())) { + calls.add(call); + } + } + }); + for (ImFunctionCall call : calls) { + new GenericNewCall(call).eliminate(); + } + } + private void fixCalleesInSpecializedFunction(ImFunction newF, GenericTypes generics) { newF.accept(new Element.DefaultVisitor() { @@ -510,6 +663,7 @@ public void visit(ImFunctionCall fc) { ImFunction callee = fc.getFunc(); if (callee == null) return; + if (translator.isGenericNewMarker(callee)) return; boolean calleeIsGeneric = !callee.getTypeVariables().isEmpty(); boolean calleeNeedsGlobals = needsGlobalSpecialization(callee); @@ -572,7 +726,9 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { // concrete clone => no type vars newF.getTypeVariables().removeAll(); - newF.setName(f.getName() + "⟪" + generics.makeName() + "⟫"); + newF.setName(genericNewOnly + ? f.getName() + "_specialized" + : f.getName() + "⟪" + generics.makeName() + "⟫"); // Only rewrite type variables if the function actually has them if (isGeneric) { @@ -581,10 +737,14 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { } // Fix calls inside this specialized function so they also point to specialized callees - fixCalleesInSpecializedFunction(newF, generics); + if (genericNewOnly) { + collectGenericNewUses(newF); + } else { + fixCalleesInSpecializedFunction(newF, generics); - // Then collect further generic uses inside the now-specialized body (incl. generic globals) - collectGenericUsages(newF); + // Then collect further generic uses inside the now-specialized body (incl. generic globals) + collectGenericUsages(newF); + } return newF; } @@ -961,6 +1121,10 @@ private void collectGenericUsages(Element element) { @Override public void visit(ImFunctionCall f) { super.visit(f); + if (translator.isGenericNewMarker(f.getFunc())) { + genericsUses.add(new GenericNewCall(f)); + return; + } if (!f.getTypeArguments().isEmpty()) { genericsUses.add(new GenericImFunctionCall(f)); } @@ -1279,6 +1443,74 @@ public void eliminate() { } } + class GenericNewCall implements GenericUse { + private final ImFunctionCall call; + + GenericNewCall(ImFunctionCall call) { + this.call = call; + } + + @Override + public void eliminate() { + if (call.getTypeArguments().size() != 1) { + throw new CompileError(call, CompilerIntrinsics.NEW + + " expects exactly one type argument and no value arguments."); + } + + ImType targetType = call.getTypeArguments().get(0).getType(); + if (containsTypeVariable(targetType)) { + throw new CompileError(call, CompilerIntrinsics.NEW + + " requires its type argument to resolve to a concrete class."); + } + if (!(targetType instanceof ImClassType classType)) { + throw new CompileError(call, CompilerIntrinsics.NEW + + " requires a concrete, non-abstract class type, but found " + targetType + "."); + } + + ImClass imClass = classType.getClassDef(); + if (!(imClass.getTrace() instanceof ClassDef classDef)) { + String kind = imClass.getTrace() instanceof InterfaceDef ? "interface" : "type"; + throw new CompileError(call, CompilerIntrinsics.NEW + " cannot construct " + kind + " " + + imClass.getName() + "."); + } + if (classDef.attrIsAbstract()) { + throw new CompileError(call, CompilerIntrinsics.NEW + " cannot construct abstract class " + + classDef.getName() + "."); + } + ConstructorDef constructor = zeroArgumentConstructor(classDef); + if (constructor == null) { + throw new CompileError(call, CompilerIntrinsics.NEW + " requires class " + classDef.getName() + + " to have a zero-argument constructor."); + } + de.peeeq.wurstscript.ast.Element source = call.getTrace(); + if (constructor.attrIsPrivate() && (source == null || !source.isSubtreeOf(classDef))) { + throw new CompileError(call, CompilerIntrinsics.NEW + + " cannot access the zero-argument constructor of class " + classDef.getName() + "."); + } + + ImFunction constructorFunction = translator.getConstructNewFunc(constructor); + ImTypeArguments constructorTypeArguments = JassIm.ImTypeArguments(); + for (ImTypeArgument argument : classType.getTypeArguments()) { + constructorTypeArguments.add(argument.copy()); + } + ImFunctionCall replacement = JassIm.ImFunctionCall(call.getTrace(), constructorFunction, + constructorTypeArguments, JassIm.ImExprs(), false, CallType.NORMAL); + call.replaceBy(replacement); + if (!genericNewOnly && !constructorTypeArguments.isEmpty()) { + genericsUses.addFirst(new GenericImFunctionCall(replacement)); + } + } + + private ConstructorDef zeroArgumentConstructor(ClassDef classDef) { + for (ConstructorDef constructor : classDef.getConstructors()) { + if (constructor.getParameters().isEmpty()) { + return constructor; + } + } + return null; + } + } + class GenericMethodCall implements GenericUse { private final ImMethodCall mc; 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 d9235248c..ea3a2580b 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 @@ -1,6 +1,7 @@ package de.peeeq.wurstscript.translation.imtranslation; import com.google.common.collect.Lists; +import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.*; @@ -513,6 +514,14 @@ public static ImExpr translateIntern(FunctionCall e, ImTranslator t, ImFunction private static ImExpr translateFunctionCall(FunctionCall e, ImTranslator t, ImFunction f, boolean returnReveiver, boolean nullSafe) { + if (e instanceof ExprFunctionCall call && CompilerIntrinsics.isNew(call)) { + ImType targetType = call.getTypeArgs().get(0).attrTyp().imTranslateType(t); + ImTypeArguments typeArguments = JassIm.ImTypeArguments( + JassIm.ImTypeArgument(targetType, new HashMap<>())); + return ImFunctionCall(call, t.getGenericNewMarker(), typeArguments, + JassIm.ImExprs(), false, CallType.NORMAL); + } + if (e.getFuncName().equals("getStackTraceString") && e.attrImplicitParameter() instanceof NoExpr && e.getArgs().size() == 0) { // special built-in error function 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 8551a3f8c..a8f1eee39 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 @@ -1553,6 +1553,22 @@ private String constructorName(ConstructorDef constr) { Map constrNewFuncs = Maps.newLinkedHashMap(); + private ImFunction genericNewMarker; + + public ImFunction getGenericNewMarker() { + if (genericNewMarker == null) { + ImTypeVar typeVar = JassIm.ImTypeVar("T"); + genericNewMarker = ImFunction(emptyTrace, de.peeeq.wurstscript.CompilerIntrinsics.NEW_MARKER, + ImTypeVars(typeVar), ImVars(), JassIm.ImTypeVarRef(typeVar), ImVars(), ImStmts(), flags()); + } + return genericNewMarker; + } + + public boolean isGenericNewMarker(ImFunction function) { + return function == genericNewMarker + || de.peeeq.wurstscript.CompilerIntrinsics.NEW_MARKER.equals(function.getName()); + } + public ImFunction getConstructNewFunc(ConstructorDef constr) { ImFunction f = constrNewFuncs.get(constr); if (f == null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 1b56d1ce5..6a65f1544 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1,6 +1,7 @@ package de.peeeq.wurstscript.validation; import com.google.common.collect.*; +import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.*; @@ -2006,6 +2007,10 @@ private void checkAnnotation(Annotation a) { private void visit(ExprFunctionCall stmtCall) { String funcName = stmtCall.getFuncName(); + if (CompilerIntrinsics.isNew(stmtCall)) { + checkGenericNew(stmtCall); + return; + } if (isConstructorThisCall(stmtCall)) { return; } @@ -2043,6 +2048,60 @@ private void visit(ExprFunctionCall stmtCall) { } + private void checkGenericNew(ExprFunctionCall call) { + if (!call.getArgs().isEmpty() || call.getTypeArgs().size() != 1) { + call.addError(CompilerIntrinsics.NEW + " expects exactly one type argument and no value arguments."); + return; + } + + WurstType targetType = call.getTypeArgs().get(0).attrTyp(); + if (targetType instanceof WurstTypeUnknown) { + return; + } + if (targetType instanceof WurstTypeTypeParam typeParam) { + if (!isTypeParamNewGeneric(typeParam.getDef())) { + call.addError(CompilerIntrinsics.NEW + " cannot construct unresolved type parameter " + + typeParam.getName() + "; use a new generic parameter such as ."); + } + return; + } + if (targetType instanceof WurstTypeInterface interfaceType) { + call.addError(CompilerIntrinsics.NEW + " cannot construct interface " + + interfaceType.getInterfaceDef().getName() + "."); + return; + } + if (!(targetType instanceof WurstTypeClass classType)) { + call.addError(CompilerIntrinsics.NEW + + " requires a concrete, non-abstract class type, but found " + targetType + "."); + return; + } + + ClassDef classDef = classType.getClassDef(); + if (classDef.attrIsAbstract()) { + call.addError(CompilerIntrinsics.NEW + " cannot construct abstract class " + classDef.getName() + "."); + return; + } + ConstructorDef constructor = zeroArgumentConstructor(classDef); + if (constructor == null) { + call.addError(CompilerIntrinsics.NEW + " requires class " + classDef.getName() + + " to have a zero-argument constructor."); + return; + } + if (constructor.attrIsPrivate() && !call.isSubtreeOf(classDef)) { + call.addError(CompilerIntrinsics.NEW + " cannot access the zero-argument constructor of class " + + classDef.getName() + "."); + } + } + + private ConstructorDef zeroArgumentConstructor(ClassDef classDef) { + for (ConstructorDef constructor : classDef.getConstructors()) { + if (constructor.getParameters().isEmpty()) { + return constructor; + } + } + return null; + } + // private void checkParams(Element where, List args, // FunctionDefinition calledFunc) { // if (calledFunc == null) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index bebe3e181..df296c752 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -11,6 +11,156 @@ public class FieldIterationTests extends WurstScriptTest { + @Test + public void explicitTargetAndGenericConstructionWorkForJassAndLua() throws IOException { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package MagicFunctions", + "endpackage", + "", + "package FieldIterationTest", + " import MagicFunctions", + " native testSuccess()", + "", + " interface FieldLoader", + " function apply(Reader reader, T target)", + "", + " class Reader", + " function read(string name, int oldValue) returns int", + " return oldValue + 1", + " function read(string name, real oldValue) returns real", + " return oldValue + 1.", + " function read(string name, string oldValue) returns string", + " return oldValue + \"!\"", + " function read(string name, boolean oldValue) returns boolean", + " return not oldValue", + "", + " function load(Reader reader, FieldLoader loader) returns T", + " let result = newInstance()", + " loader.apply(reader, result)", + " return result", + "", + " class FirstState", + " int score", + " real ratio", + " construct()", + " score = 10", + " ratio = 2.", + "", + " class SecondState", + " string name", + " boolean enabled", + " construct()", + " name = \"loaded\"", + " enabled = false", + "", + " init", + " let reader = new Reader", + " let first = load(reader, (Reader r, FirstState state) -> begin", + " mapFields(state, (name, oldValue) -> r.read(name, oldValue))", + " end)", + " let second = load(reader, (Reader r, SecondState state) -> begin", + " mapFields(state, (name, oldValue) -> r.read(name, oldValue))", + " end)", + " if first.score == 11 and first.ratio == 3. and second.name == \"loaded!\" and second.enabled", + " testSuccess()", + "endpackage" + ); + + String lua = Files.readString(new File(TEST_OUTPUT_PATH + + "lua/FieldIterationTests_explicitTargetAndGenericConstructionWorkForJassAndLua.lua").toPath()); + String jass = Files.readString(new File(TEST_OUTPUT_PATH + + "FieldIterationTests_explicitTargetAndGenericConstructionWorkForJassAndLua_opt.j").toPath()); + for (String generated : new String[]{lua, jass}) { + assertFalse(generated.contains("newInstance")); + assertFalse(generated.contains("mapFields")); + assertFalse(generated.contains("reflection")); + } + } + + @Test + public void explicitTargetIsEvaluatedOnceAndIncludesInheritedAndModuleFields() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package MagicFunctions", + "endpackage", + "", + "package FieldIterationTest", + " import MagicFunctions", + " native testSuccess()", + " int evaluations = 0", + " int total = 0", + "", + " module ExtraState", + " int moduleValue = 2", + "", + " class BaseState", + " int inheritedValue = 3", + "", + " class State extends BaseState", + " use ExtraState", + " int localValue = 4", + "", + " function evaluated(State state) returns State", + " evaluations++", + " return state", + " function add(int value)", + " total += value", + "", + " init", + " let state = new State", + " forFields(evaluated(state), (name, value) -> add(value))", + " mapFields(evaluated(state), (name, value) -> value + 1)", + " if evaluations == 2 and total == 9 and state.inheritedValue == 4 and state.moduleValue == 3 and state.localValue == 5", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericConstructionDiagnostics() { + expectIntrinsicError("requires a concrete, non-abstract class type, but found int", + "init", " newInstance()"); + expectIntrinsicError("requires a concrete, non-abstract class type, but found handle", + "init", " newInstance()"); + expectIntrinsicError("cannot construct interface State", + "interface State", " function unused()", "", "init", " newInstance()"); + expectIntrinsicError("cannot construct abstract class State", + "abstract class State", "", "init", " newInstance()"); + expectIntrinsicError("requires class State to have a zero-argument constructor", + "class State", " construct(int value)", "", "init", " newInstance()"); + expectIntrinsicError("cannot access the zero-argument constructor of class State", + "class State", " private construct()", "", "init", " newInstance()"); + expectIntrinsicError("cannot construct unresolved type parameter T", + "function make() returns T", " return newInstance()"); + } + + @Test + public void explicitTargetFieldIterationDiagnostics() { + expectIntrinsicError("target must have a concrete class type, but found int", + "function consume(string name, int value)", "", "init", " forFields(1, (name, value) -> consume(name, value))"); + expectIntrinsicError("expects a closure with (fieldName, fieldValue) parameters", + "class State", " int value", "", "init", " let state = new State", " mapFields(state, value -> value)"); + expectIntrinsicError("no accessible mutable instance fields were found", + "class State", " readonly int value = 1", "", "init", " let state = new State", " mapFields(state, (name, value) -> value)"); + expectIntrinsicError("no accessible mutable instance fields were found", + "function consume(string name, int value)", "", "class State", " static int value = 1", "", "init", " let state = new State", " forFields(state, (name, value) -> consume(name, value))"); + } + + private void expectIntrinsicError(String message, String... body) { + String[] lines = new String[body.length + 2]; + lines[0] = "package FieldIterationTest"; + System.arraycopy(body, 0, lines, 1, body.length); + lines[lines.length - 1] = "endpackage"; + test().expectError(message).lines(lines); + } + @Test public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOException { test() @@ -51,10 +201,10 @@ public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOE " static int schemaVersion = 1", "", " function save(Codec codec)", - " __wurst_forFields((fieldName, value) -> codec.write(fieldName, value))", + " forFields((fieldName, value) -> codec.write(fieldName, value))", "", " function load(Codec codec)", - " __wurst_mapFields((fieldName, value) -> codec.read(fieldName, value))", + " mapFields((fieldName, value) -> codec.read(fieldName, value))", "", " init", " let codec = new Codec", @@ -69,8 +219,8 @@ public void serializesAndDeserializesFieldsWithoutRuntimeReflection() throws IOE String lua = Files.readString(new File(TEST_OUTPUT_PATH + "lua/FieldIterationTests_serializesAndDeserializesFieldsWithoutRuntimeReflection.lua").toPath()); - assertFalse(lua.contains("__wurst_forFields")); - assertFalse(lua.contains("__wurst_mapFields")); + assertFalse(lua.contains("forFields")); + assertFalse(lua.contains("mapFields")); assertTrue(lua.contains("Codec_Codec_write(codec, \"score\", this")); assertTrue(lua.contains("Codec_Codec_write1(codec, \"name\", this")); assertTrue(lua.contains("Data_score = Codec_Codec_read(codec1, \"score\"")); @@ -86,7 +236,7 @@ public void rejectsFieldIterationOutsideInstanceContext() { " function consume(string name, int value)", "", " init", - " __wurst_forFields((name, value) -> consume(name, value))", + " forFields((name, value) -> consume(name, value))", "endpackage" ); } @@ -101,7 +251,7 @@ public void rejectsInvalidFieldIterationClosure() { " int value", "", " function save()", - " __wurst_forFields(value -> value)", + " forFields(value -> value)", "endpackage" ); } @@ -148,7 +298,7 @@ public void nestedClosureParametersAreNotSubstituted() { " int second = 2", "", " function save(Codec codec)", - " __wurst_forFields((fieldName, value) -> codec.write(fieldName, consume((int value) -> value)))", + " forFields((fieldName, value) -> codec.write(fieldName, consume((int value) -> value)))", "", " init", " let codec = new Codec", @@ -170,7 +320,7 @@ public void rejectsExplicitFieldIterationParameterTypes() { " int value", "", " function save()", - " __wurst_forFields((NoSuch name, NoSuch value) -> value)", + " forFields((NoSuch name, NoSuch value) -> value)", "endpackage" ); } @@ -185,7 +335,7 @@ public void rejectsFieldIterationWithoutInstanceFields() { " static int schemaVersion = 1", "", " function save()", - " __wurst_forFields((name, value) -> noSuchFunction(name, value))", + " forFields((name, value) -> noSuchFunction(name, value))", "endpackage" ); } @@ -200,7 +350,7 @@ public void rejectsDuplicateFieldIterationParameterNames() { " int value", "", " function save()", - " __wurst_mapFields((value, value) -> 42)", + " mapFields((value, value) -> 42)", "endpackage" ); } @@ -217,7 +367,7 @@ public void preservesLocalBindingsInBlockCallbacks() { " int second = 2", "", " function load()", - " __wurst_mapFields((name, value) -> begin", + " mapFields((name, value) -> begin", " let temporary = 42", " return temporary", " end)", @@ -243,7 +393,7 @@ public void keepsLoopBindingsInsideLoopBody() { " int second = 2", "", " function load()", - " __wurst_mapFields((name, value) -> begin", + " mapFields((name, value) -> begin", " for int index = 0 to 1", " continue", " return 42 + value - value", @@ -268,7 +418,7 @@ public void rejectsBlockLocalShadowingBeforeExpansion() { " int value", "", " function load()", - " __wurst_mapFields((name, value) -> begin", + " mapFields((name, value) -> begin", " let old = value", " let value = 42", " return old", @@ -295,7 +445,7 @@ public void includesInheritedInstanceFields() { " int local = 2", "", " function save(Acc acc)", - " __wurst_forFields((name, value) -> acc.add(value))", + " forFields((name, value) -> acc.add(value))", "", " init", " let acc = new Acc", @@ -320,8 +470,8 @@ public void expandsFieldIterationInModuleMethodsAfterInstantiation() { " total = total + value", " module Serializer", " function save(Acc acc)", - " __wurst_forFields((name, value) -> acc.add(value))", - " __wurst_forFields((name, value) -> acc.add(value))", + " forFields((name, value) -> acc.add(value))", + " forFields((name, value) -> acc.add(value))", "", " class Data", " use Serializer", @@ -360,7 +510,7 @@ public void includesInstanceFieldsFromModules() { " int local = 5", "", " function save(Acc acc)", - " __wurst_forFields((name, value) -> acc.add(value))", + " forFields((name, value) -> acc.add(value))", "", " init", " let acc = new Acc", @@ -397,7 +547,7 @@ public void qualifiesFieldsFromSiblingModules() { " use Left", " use Right", " function save(Acc acc)", - " __wurst_forFields((name, value) -> acc.add(name, value))", + " forFields((name, value) -> acc.add(name, value))", " init", " let acc = new Acc", " let data = new Data", @@ -425,7 +575,7 @@ public void excludesPrivateModuleFields() { " class Data", " use State", " function save(Acc acc)", - " __wurst_forFields((name, value) -> acc.add(value))", + " forFields((name, value) -> acc.add(value))", " init", " let acc = new Acc", " let data = new Data", @@ -444,7 +594,7 @@ public void validatesUnusedModuleFieldIteration() { "package FieldIterationTest", " module Unused", " function save()", - " __wurst_forFields(value -> value)", + " forFields(value -> value)", "endpackage" ); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java index 97418849f..bbc90db9b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java @@ -144,7 +144,7 @@ public void incrementalRecheckRestoresFieldIterationIntrinsic() throws IOExcepti "native consume(string name, int value)", "class Data extends Base", " function save()", - " __wurst_forFields((name, value) -> consume(name, value))" + " forFields((name, value) -> consume(name, value))" )); writeFile(fileWurst, "package Wurst\n"); From a56533fc869260a195dc57d73a6b8a3b641a8514 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 14:35:08 +0200 Subject: [PATCH 2/9] Address generic serialization review feedback --- .../peeeq/wurstscript/CompilerIntrinsics.java | 8 +- .../de/peeeq/wurstscript/SyntacticSugar.java | 3 + .../attributes/AttrImplicitParameter.java | 5 + .../imtranslation/EliminateGenerics.java | 146 +++++++++++++----- .../tests/FieldIterationTests.java | 81 ++++++++++ 5 files changed, 205 insertions(+), 38 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java index 8b4a2cb77..227ba97d5 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -15,11 +15,15 @@ private CompilerIntrinsics() { } public static boolean isForFields(ExprFunctionCall call) { - return FOR_FIELDS.equals(call.getFuncName()) && hasClosureArgument(call); + return FOR_FIELDS.equals(call.getFuncName()) + && hasClosureArgument(call) + && call.lookupFuncs(FOR_FIELDS).isEmpty(); } public static boolean isMapFields(ExprFunctionCall call) { - return MAP_FIELDS.equals(call.getFuncName()) && hasClosureArgument(call); + return MAP_FIELDS.equals(call.getFuncName()) + && hasClosureArgument(call) + && call.lookupFuncs(MAP_FIELDS).isEmpty(); } public static boolean isFieldIteration(ExprFunctionCall call) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index 3610d7956..b1e470e1e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -507,6 +507,9 @@ private ExprMemberVarDot fieldAccess(WPos source, FieldInfo field, String target Expr left; if (targetName != null) { left = Ast.ExprVarAccess(source, Ast.Identifier(source, targetName)); + for (String module : field.modulePath) { + left = Ast.ExprMemberVarDot(source, left, Ast.Identifier(source, module)); + } } else if (field.modulePath.isEmpty()) { left = Ast.ExprThis(source); } else { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java index e195e86e4..1be644d50 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java @@ -58,6 +58,11 @@ public static OptExpr getImplicitParameter(ExprMemberMethod e) { private static @Nullable Expr getImplicitParameterUsingLeft(HasReceiver e) { if (e.getLeft().attrTyp().isStaticRef()) { + // Module-instance qualifiers are static references, but a qualified access such as + // object.Module.field still uses object as the dynamic receiver of field. + if (e.getLeft() instanceof HasReceiver qualifiedLeft) { + return getImplicitParameterUsingLeft(qualifiedLeft); + } // we have a static ref like Math.sqrt() // this will be handled like if we just have sqrt() // if we have an implicit parameter depends on whether sqrt is static or not diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index b57423688..962be4101 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -35,6 +35,8 @@ public class EliminateGenerics { // Track concrete generic arguments for specialized functions to simplify later lookups private final Map specializedFunctionGenerics = new IdentityHashMap<>(); + private final Set unspecializedGenericClassMethods = + Collections.newSetFromMap(new IdentityHashMap<>()); // NEW: Track specialized global variables for generic static fields // Key: (original generic global var, concrete type instantiation) -> specialized var @@ -104,6 +106,7 @@ public void transform() { */ public void transformGenericNewOnly() { genericNewOnly = true; + collectUnspecializedGenericClassMethods(); collectGenericNewRoots(); eliminateGenericUses(); eliminateRemainingGenericNewCalls(); @@ -114,7 +117,8 @@ private void collectGenericNewRoots() { prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImFunction function) { - if (!function.getTypeVariables().isEmpty()) { + if (!function.getTypeVariables().isEmpty() + || unspecializedGenericClassMethods.contains(function)) { return; } super.visit(function); @@ -125,6 +129,12 @@ public void visit(ImFunctionCall call) { super.visit(call); collectGenericNewUse(call); } + + @Override + public void visit(ImMethodCall call) { + super.visit(call); + collectGenericNewUse(call); + } }); } @@ -135,6 +145,12 @@ public void visit(ImFunctionCall call) { super.visit(call); collectGenericNewUse(call); } + + @Override + public void visit(ImMethodCall call) { + super.visit(call); + collectGenericNewUse(call); + } }); } @@ -153,6 +169,22 @@ && functionContainsGenericNew(call.getFunc(), Collections.newSetFromMap(new Iden } } + private void collectGenericNewUse(ImMethodCall call) { + ImMethod method = call.getMethod(); + if (method.getImplementation() == null + || !functionContainsGenericNew(method.getImplementation(), + Collections.newSetFromMap(new IdentityHashMap<>()))) { + return; + } + if (call.getTypeArguments().isEmpty()) { + addMemberTypeArguments(call, method.attrClass()); + } + if (!call.getTypeArguments().isEmpty() + && !typeArgumentsContainTypeVariable(call.getTypeArguments())) { + genericsUses.add(new GenericMethodCall(call)); + } + } + private boolean typeArgumentsContainTypeVariable(ImTypeArguments typeArguments) { for (ImTypeArgument typeArgument : typeArguments) { if (containsTypeVariable(typeArgument.getType())) { @@ -185,7 +217,8 @@ private void assertNoReachableGenericNewMarkers() { prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImFunction function) { - if (!function.getTypeVariables().isEmpty()) { + if (!function.getTypeVariables().isEmpty() + || unspecializedGenericClassMethods.contains(function)) { return; } super.visit(function); @@ -429,45 +462,34 @@ private void addMemberTypeArguments() { @Override public void visit(ImMethodCall mc) { super.visit(mc); - handle(mc, mc.getMethod().attrClass()); + addMemberTypeArguments(mc, mc.getMethod().attrClass()); } @Override public void visit(ImMemberAccess ma) { super.visit(ma); - handle(ma, (ImClass) ma.getVar().getParent().getParent()); - } - - private void handle(ImMemberOrMethodAccess ma, ImClass owningClass) { - ImType receiverType = ma.getReceiver().attrTyp(); - if (!(receiverType instanceof ImClassType)) return; - - ImClassType rt = (ImClassType) receiverType; - ImClassType ct = adaptToSuperclass(rt, owningClass); - if (ct == null) { -// dbg("addMemberTA FAIL: owning=" + owningClass.getName() + " recv=" + rt + " in " + ma); - throw new CompileError(ma, "Could not adapt receiver " + rt + " to superclass " + owningClass + " in member access " + ma); - } - -// dbg("addMemberTA: kind=" + ma.getClass().getSimpleName() -// + " owning=" + owningClass.getName() + " " + id(owningClass) -// + " recvType=" + rt -// + " adapted=" + ct -// + " beforeTA=" + shortTypeArgs(ma.getTypeArguments())); - - // existing code... - List typeArgs = new ArrayList<>(); - for (ImTypeArgument imTypeArgument : ct.getTypeArguments()) { - typeArgs.add(imTypeArgument.copy()); - } - ma.getTypeArguments().addAll(0, typeArgs); - -// dbg("addMemberTA: afterTA=" + shortTypeArgs(ma.getTypeArguments())); + addMemberTypeArguments(ma, (ImClass) ma.getVar().getParent().getParent()); } - }); } + private void addMemberTypeArguments(ImMemberOrMethodAccess access, ImClass owningClass) { + ImType receiverType = access.getReceiver().attrTyp(); + if (!(receiverType instanceof ImClassType rt)) { + return; + } + ImClassType classType = adaptToSuperclass(rt, owningClass); + if (classType == null) { + throw new CompileError(access, "Could not adapt receiver " + rt + " to superclass " + + owningClass + " in member access " + access); + } + List typeArgs = new ArrayList<>(); + for (ImTypeArgument typeArgument : classType.getTypeArguments()) { + typeArgs.add(typeArgument.copy()); + } + access.getTypeArguments().addAll(0, typeArgs); + } + private static ImClassType adaptToSuperclass(ImClassType ct, ImClass owningClass) { if (ct.getClassDef() == owningClass) { return ct; @@ -635,7 +657,8 @@ private void eliminateRemainingGenericNewCalls() { prog.accept(new Element.DefaultVisitor() { @Override public void visit(ImFunction function) { - if (!function.getTypeVariables().isEmpty()) { + if (!function.getTypeVariables().isEmpty() + || unspecializedGenericClassMethods.contains(function)) { return; } super.visit(function); @@ -654,6 +677,26 @@ public void visit(ImFunctionCall call) { } } + private void collectUnspecializedGenericClassMethods() { + for (ImMethod method : prog.getMethods()) { + collectUnspecializedGenericClassMethod(method); + } + for (ImClass imClass : prog.getClasses()) { + if (!imClass.getTypeVariables().isEmpty()) { + for (ImMethod method : imClass.getMethods()) { + collectUnspecializedGenericClassMethod(method); + } + } + } + } + + private void collectUnspecializedGenericClassMethod(ImMethod method) { + if (method.getImplementation() != null + && !method.getMethodClass().getClassDef().getTypeVariables().isEmpty()) { + unspecializedGenericClassMethods.add(method.getImplementation()); + } + } + private void fixCalleesInSpecializedFunction(ImFunction newF, GenericTypes generics) { newF.accept(new Element.DefaultVisitor() { @@ -777,12 +820,41 @@ private ImMethod specializeMethod(ImMethod m, GenericTypes generics) { } newM.setMethodClass(specializeType(newClassType)); - newM.setName(m.getName() + "⟪" + generics.makeName() + "⟫"); - newM.setImplementation(specializeFunction(newM.getImplementation(), generics)); + newM.setName(genericNewOnly + ? m.getName() + "_specialized_" + generics.makeName() + : m.getName() + "⟪" + generics.makeName() + "⟫"); + newM.setImplementation(genericNewOnly + ? specializeMethodImplementation(m, generics) + : specializeFunction(newM.getImplementation(), generics)); adaptSubmethods(m.getSubMethods(), newM); return newM; } + private ImFunction specializeMethodImplementation(ImMethod method, GenericTypes generics) { + ImFunction implementation = method.getImplementation(); + ImFunction specialized = specializedFunctions.get(implementation, generics); + if (specialized != null) { + return specialized; + } + + List typeVariables = new ArrayList<>( + method.getMethodClass().getClassDef().getTypeVariables()); + typeVariables.addAll(implementation.getTypeVariables()); + if (typeVariables.size() != generics.getTypeArguments().size()) { + throw new CompileError(method, "Generics should match class method type variables."); + } + + ImFunction newImplementation = implementation.copyWithRefs(); + specializedFunctions.put(implementation, generics, newImplementation); + specializedFunctionGenerics.put(newImplementation, generics); + prog.getFunctions().add(newImplementation); + newImplementation.getTypeVariables().removeAll(); + newImplementation.setName(implementation.getName() + "_specialized"); + rewriteGenerics(newImplementation, generics, typeVariables); + collectGenericNewUses(newImplementation); + return newImplementation; + } + private void adaptSubmethods(List oldSubMethods, ImMethod newM) { newM.setSubMethods(new ArrayList<>()); ImClassType newClassT = newM.getMethodClass(); @@ -912,7 +984,9 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { prog.getClasses().add(newC); newC.getTypeVariables().removeAll(); - newC.setName(c.getName() + "⟪" + generics.makeName() + "⟫"); + newC.setName(genericNewOnly + ? c.getName() + "_specialized_" + generics.makeName() + : c.getName() + "⟪" + generics.makeName() + "⟫"); List typeVars = c.getTypeVariables(); rewriteGenerics(newC, generics, typeVars); newC.getSuperClasses().replaceAll(this::specializeType); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index df296c752..c6b37027b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -274,6 +274,87 @@ public void ordinaryFieldHelperNamesRemainUserFunctions() { ); } + @Test + public void ordinaryFieldHelperWithClosureRemainsUserFunction() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " interface IntCallback", + " function apply(int value) returns int", + "", + " function forFields(IntCallback callback) returns int", + " return callback.apply(2)", + "", + " init", + " if forFields(value -> value + 1) == 3", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void explicitTargetPreservesSiblingModuleFieldQualifiers() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " int total = 0", + "", + " module Left", + " int value = 2", + "", + " module Right", + " int value = 5", + "", + " class State", + " use Left", + " use Right", + "", + " function add(int value)", + " total += value", + "", + " init", + " let state = new State", + " forFields(state, (name, value) -> add(value))", + " mapFields(state, (name, value) -> value + 10)", + " if total == 7 and state.Left.value == 12 and state.Right.value == 15", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericConstructionInGenericClassMethodWorksForLua() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + "", + " class Factory", + " function make() returns T", + " return newInstance()", + "", + " class State", + " int value", + " construct()", + " value = 7", + "", + " init", + " let factory = new Factory", + " if factory.make().value == 7", + " testSuccess()", + "endpackage" + ); + } + @Test public void nestedClosureParametersAreNotSubstituted() { test() From b860752e16f7265023b6c03beffb22b48c56a25b Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 16:13:01 +0200 Subject: [PATCH 3/9] Address second generic serialization review --- .../peeeq/wurstscript/CompilerIntrinsics.java | 2 +- .../de/peeeq/wurstscript/SyntacticSugar.java | 36 +++-- .../imtranslation/EliminateGenerics.java | 66 +++++++-- .../tests/FieldIterationTests.java | 131 ++++++++++++++++++ 4 files changed, 212 insertions(+), 23 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java index 227ba97d5..cd9ff1b95 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -31,7 +31,7 @@ public static boolean isFieldIteration(ExprFunctionCall call) { } public static boolean isNew(ExprFunctionCall call) { - return NEW.equals(call.getFuncName()); + return NEW.equals(call.getFuncName()) && call.lookupFuncs(NEW).isEmpty(); } private static boolean hasClosureArgument(ExprFunctionCall call) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index b1e470e1e..c07d4c23b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -214,7 +214,9 @@ private void expandFieldIteration(ExprFunctionCall call, int originalStatementIndex = statements.indexOf(call); if (explicitTarget) { target = call.getArgs().get(0); - targetName = "__wurstFieldTarget" + generatedTargetCounter++; + do { + targetName = "__wurstFieldTarget" + generatedTargetCounter++; + } while (call.lookupVar(targetName, false) != null); targetVariable = Ast.LocalVarDef(call.getSource(), Ast.Modifiers(), Ast.NoTypeExpr(), Ast.Identifier(call.getSource(), targetName), target.copy()); statements.add(originalStatementIndex, targetVariable); @@ -253,7 +255,7 @@ private void expandFieldIteration(ExprFunctionCall call, return; } } - List fields = collectInstanceFields(classDef, owner, call, explicitTarget); + List fields = collectInstanceFields(classDef, owner, call, explicitTarget, assignsResult); if (fields.isEmpty()) { if (targetVariable != null) { statements.remove(targetVariable); @@ -290,15 +292,17 @@ private void expandFieldIteration(ExprFunctionCall call, } private List collectInstanceFields(ClassDef classDef, ClassOrModule owner, - Element accessSite, boolean explicitTarget) { + Element accessSite, boolean explicitTarget, + boolean requireMutable) { List fields = new ArrayList<>(); if (classDef != null) { collectInheritedFields(classDef.attrTypC(), fields, classDef, Collections.newSetFromMap(new IdentityHashMap<>()), - Collections.newSetFromMap(new IdentityHashMap<>()), accessSite, explicitTarget); + Collections.newSetFromMap(new IdentityHashMap<>()), accessSite, explicitTarget, + requireMutable); } else if (owner instanceof ModuleDef moduleDef) { addInstanceFields(moduleDef.getVars(), fields, null, List.of(), moduleDef, - accessSite, explicitTarget); + accessSite, explicitTarget, requireMutable); } return fields; } @@ -309,19 +313,20 @@ private void collectInheritedFields(WurstTypeClass type, Set visitedClasses, Set visitedModules, Element accessSite, - boolean explicitTarget) { + boolean explicitTarget, + boolean requireMutable) { if (!visitedClasses.add(type.getClassDef())) { return; } WurstTypeClass superType = type.extendedClass(); if (superType != null) { collectInheritedFields(superType, fields, concreteClass, visitedClasses, visitedModules, - accessSite, explicitTarget); + accessSite, explicitTarget, requireMutable); } addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, - visitedModules, List.of(), accessSite, explicitTarget); + visitedModules, List.of(), accessSite, explicitTarget, requireMutable); addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null, - accessSite, explicitTarget); + accessSite, explicitTarget, requireMutable); } private void addModuleFields(Iterable modules, @@ -330,7 +335,8 @@ private void addModuleFields(Iterable modules, Set visited, List parentPath, Element accessSite, - boolean explicitTarget) { + boolean explicitTarget, + boolean requireMutable) { for (ModuleInstanciation module : modules) { if (!visited.add(module)) { continue; @@ -341,9 +347,9 @@ private void addModuleFields(Iterable modules, ? List.of(module.getName()) : parentPath; addModuleFields(module.getModuleInstanciations(), fields, concreteClass, visited, modulePath, - accessSite, explicitTarget); + accessSite, explicitTarget, requireMutable); addInstanceFields(module.getVars(), fields, concreteClass, modulePath, module.attrModuleOrigin(), - accessSite, explicitTarget); + accessSite, explicitTarget, requireMutable); } } @@ -353,7 +359,8 @@ private void addInstanceFields(Iterable declarations, List modulePath, ModuleDef declaringModule, Element accessSite, - boolean explicitTarget) { + boolean explicitTarget, + boolean requireMutable) { for (GlobalVarDef field : declarations) { ClassDef declaringClass = field.attrNearestClassDef(); boolean privateFromAnotherClass = field.attrIsPrivate() && concreteClass != null @@ -361,7 +368,8 @@ private void addInstanceFields(Iterable declarations, ? declaringClass == null || !accessSite.isSubtreeOf(declaringClass) : declaringClass != concreteClass); boolean privateFromAnotherModule = field.attrIsPrivate() && declaringModule != null; - if (!field.attrIsStatic() && !field.attrIsReadonly() && !field.attrIsConstant() + boolean mutableEnough = !requireMutable || (!field.attrIsReadonly() && !field.attrIsConstant()); + if (!field.attrIsStatic() && mutableEnough && !privateFromAnotherClass && !privateFromAnotherModule) { fields.add(new FieldInfo(field, modulePath)); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index 962be4101..7bd10f4e7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -171,9 +171,9 @@ && functionContainsGenericNew(call.getFunc(), Collections.newSetFromMap(new Iden private void collectGenericNewUse(ImMethodCall call) { ImMethod method = call.getMethod(); - if (method.getImplementation() == null - || !functionContainsGenericNew(method.getImplementation(), - Collections.newSetFromMap(new IdentityHashMap<>()))) { + if (!methodContainsGenericNew(method, + Collections.newSetFromMap(new IdentityHashMap<>()), + Collections.newSetFromMap(new IdentityHashMap<>()))) { return; } if (call.getTypeArguments().isEmpty()) { @@ -195,7 +195,13 @@ private boolean typeArgumentsContainTypeVariable(ImTypeArguments typeArguments) } private boolean functionContainsGenericNew(ImFunction function, Set visited) { - if (!visited.add(function)) { + return functionContainsGenericNew(function, visited, + Collections.newSetFromMap(new IdentityHashMap<>())); + } + + private boolean functionContainsGenericNew(ImFunction function, Set visitedFunctions, + Set visitedMethods) { + if (!visitedFunctions.add(function)) { return false; } boolean[] found = {false}; @@ -203,7 +209,16 @@ private boolean functionContainsGenericNew(ImFunction function, Set @Override public void visit(ImFunctionCall call) { if (translator.isGenericNewMarker(call.getFunc()) - || functionContainsGenericNew(call.getFunc(), visited)) { + || functionContainsGenericNew(call.getFunc(), visitedFunctions, visitedMethods)) { + found[0] = true; + return; + } + super.visit(call); + } + + @Override + public void visit(ImMethodCall call) { + if (methodContainsGenericNew(call.getMethod(), visitedFunctions, visitedMethods)) { found[0] = true; return; } @@ -213,6 +228,23 @@ public void visit(ImFunctionCall call) { return found[0]; } + private boolean methodContainsGenericNew(ImMethod method, Set visitedFunctions, + Set visitedMethods) { + if (!visitedMethods.add(method)) { + return false; + } + if (method.getImplementation() != null + && functionContainsGenericNew(method.getImplementation(), visitedFunctions, visitedMethods)) { + return true; + } + for (ImMethod subMethod : method.getSubMethods()) { + if (methodContainsGenericNew(subMethod, visitedFunctions, visitedMethods)) { + return true; + } + } + return false; + } + private void assertNoReachableGenericNewMarkers() { prog.accept(new Element.DefaultVisitor() { @Override @@ -812,13 +844,18 @@ private ImMethod specializeMethod(ImMethod m, GenericTypes generics) { ImMethod newM = m.copyWithRefs(); specializedMethods.put(m, generics, newM); - prog.getMethods().add(newM); + if (!genericNewOnly) { + prog.getMethods().add(newM); + } ImClassType newClassType = newM.getMethodClass().copy(); for (int i = 0; i < newClassType.getTypeArguments().size(); i++) { newClassType.getTypeArguments().set(i, generics.getTypeArguments().get(i).copy()); } newM.setMethodClass(specializeType(newClassType)); + if (genericNewOnly) { + newM.getMethodClass().getClassDef().getMethods().add(newM); + } newM.setName(genericNewOnly ? m.getName() + "_specialized_" + generics.makeName() @@ -826,7 +863,7 @@ private ImMethod specializeMethod(ImMethod m, GenericTypes generics) { newM.setImplementation(genericNewOnly ? specializeMethodImplementation(m, generics) : specializeFunction(newM.getImplementation(), generics)); - adaptSubmethods(m.getSubMethods(), newM); + adaptSubmethods(m.getSubMethods(), newM, generics); return newM; } @@ -855,7 +892,7 @@ private ImFunction specializeMethodImplementation(ImMethod method, GenericTypes return newImplementation; } - private void adaptSubmethods(List oldSubMethods, ImMethod newM) { + private void adaptSubmethods(List oldSubMethods, ImMethod newM, GenericTypes generics) { newM.setSubMethods(new ArrayList<>()); ImClassType newClassT = newM.getMethodClass(); ImClass newMClass = newClassT.getClassDef(); @@ -863,6 +900,19 @@ private void adaptSubmethods(List oldSubMethods, ImMethod newM) { ImClassType subClassT = subMethod.getMethodClass(); ImClass subClass = subClassT.getClassDef(); if (isGenericType(subClassT)) { + if (genericNewOnly + && subClass.getTypeVariables().size() == generics.getTypeArguments().size()) { + ImMethod specializedSubMethod = specializeMethod(subMethod, generics); + // Lua keeps ordinary generic objects erased. Bind the concrete implementation + // to that erased class so interface dispatch on an existing object can reach it. + specializedSubMethod.getMethodClass().getClassDef().getMethods() + .remove(specializedSubMethod); + specializedSubMethod.setMethodClass( + JassIm.ImClassType(subClass, JassIm.ImTypeArguments())); + subClass.getMethods().add(specializedSubMethod); + newM.getSubMethods().add(specializedSubMethod); + continue; + } onSpecializeClass(subClass, (subGenerics, specializedSubClass) -> { if (specializedSubClass.isSubclassOf(newMClass)) { ImMethod specializedSubMethod = specializeMethod(subMethod, subGenerics); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index c6b37027b..5a8a72887 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -355,6 +355,137 @@ public void genericConstructionInGenericClassMethodWorksForLua() { ); } + @Test + public void genericConstructionReachabilityCrossesMethodCallsForLua() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + "", + " class Box", + " function make() returns T", + " return newInstance()", + "", + " function build(Box box) returns T", + " return box.make()", + "", + " class State", + " int value = 9", + "", + " init", + " let state = build(new Box)", + " if state.value == 9", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericConstructionReachabilityCrossesInterfaceDispatchForLua() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + "", + " interface Maker", + " function make() returns T", + "", + " class Box implements Maker", + " function make() returns T", + " return newInstance()", + "", + " function build(Maker maker) returns T", + " return maker.make()", + "", + " class State", + " int value = 11", + "", + " init", + " let state = build(new Box)", + " if state.value == 11", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void forFieldsIncludesReadonlyInstanceFields() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " int total = 0", + "", + " class State", + " readonly int value = 7", + " constant int version = 5", + "", + " function add(int value)", + " total += value", + "", + " init", + " let state = new State", + " forFields(state, (name, value) -> add(value))", + " if total == 12", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void ordinaryNewInstanceNameRemainsUserFunction() { + test() + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + "", + " function newInstance() returns int", + " return 7", + "", + " init", + " if newInstance() == 7", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void explicitTargetTemporaryDoesNotCollideWithUserLocal() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " int total = 0", + "", + " class State", + " int value = 3", + "", + " function add(int value)", + " total += value", + "", + " init", + " let __wurstFieldTarget0 = 4", + " let state = new State", + " forFields(state, (name, value) -> add(value))", + " if total == 3 and __wurstFieldTarget0 == 4", + " testSuccess()", + "endpackage" + ); + } + @Test public void nestedClosureParametersAreNotSubstituted() { test() From 49464973eaed4d6c093377b10c67cc922dee74c8 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 16:17:07 +0200 Subject: [PATCH 4/9] Document generic serialization compiler guardrails --- AGENTS.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 864d99705..53fa8de0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -292,3 +292,43 @@ Recent regressions showed that virtual-slot binding can silently degrade to base * assert each concrete sibling class binds that slot to its own implementation, * assert no sibling binds that dispatched slot to `NoOpState_*`. * Add a compile-twice determinism assertion for the same repro input. + +--- + +## 9. Compiler-Assisted Field Iteration and Generic Construction + +The compiler surface used by serialization libraries is intentionally general-purpose and contains no knowledge +of save formats, `ChunkedString`, hashes, or `Serializable`. + +### Source-level contract + +* The public Wurst names are `forFields`, `mapFields`, and `newInstance()`; do not introduce underscore-prefixed + alternatives. Internal markers must never survive backend lowering. +* A visible ordinary function with one of these names must resolve normally. Compiler handling is only the fallback + when no user-visible function resolves. +* `forFields` includes accessible, non-static instance fields, including inherited, module-injected, readonly, and + constant fields. `mapFields` additionally requires each included field to be mutable. +* Explicit targets are evaluated exactly once. Generated temporaries must be proven fresh in the enclosing scope. +* Preserve module qualification in both field keys and generated accesses so sibling modules with equal field names + remain distinct. +* `newInstance()` must invoke the normal accessible zero-argument constructor of a concrete, non-abstract class. + Never replace it with uninitialized allocation or runtime type lookup. + +### Lowering and backend rules + +* Field iteration expands after module expansion, when inherited and injected fields are concrete. +* Jass may use normal generic elimination. Lua specialization remains targeted to paths that reach generic + construction; do not turn this into general Lua generic monomorphization. +* Lua reachability must traverse both `ImFunctionCall` and `ImMethodCall`, including dispatch submethods. +* Targeted specialized methods needed by Lua dispatch must remain attached to the IM classes consumed by + `LuaDispatchPreparation`. Concrete implementations for erased generic objects must bind the same root slot used + by the call site. +* Generate no runtime reflection registry, type-name lookup, type-id switch, or serialization-specific metadata. + +### Required regression coverage + +Use `FieldIterationTests` as the focused suite. Cover both Jass and Lua, direct and explicit targets, target +evaluation count, inherited/module fields, readonly versus mutable behavior, overload selection, constructor +execution and diagnostics, ordinary same-name functions, generic functions and class methods, transitive method +reachability, and generic interface dispatch. Assert generated output contains direct construction/accesses and no +source intrinsic names or runtime reflection machinery. From 57606ad4fce1c70efd8ba4a392f382b42045278e Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 16:55:15 +0200 Subject: [PATCH 5/9] Respect applicable overloads for compiler functions --- AGENTS.md | 7 ++-- .../peeeq/wurstscript/CompilerIntrinsics.java | 7 ++-- .../wurstscript/attributes/AttrFuncDef.java | 20 +++++++++++ .../tests/FieldIterationTests.java | 35 +++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 53fa8de0c..d427a574f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,8 +304,8 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. * The public Wurst names are `forFields`, `mapFields`, and `newInstance()`; do not introduce underscore-prefixed alternatives. Internal markers must never survive backend lowering. -* A visible ordinary function with one of these names must resolve normally. Compiler handling is only the fallback - when no user-visible function resolves. +* An applicable visible ordinary function with one of these names must resolve normally. Compiler handling is only + the fallback when no user-visible overload accepts the call. * `forFields` includes accessible, non-static instance fields, including inherited, module-injected, readonly, and constant fields. `mapFields` additionally requires each included field to be mutable. * Explicit targets are evaluated exactly once. Generated temporaries must be proven fresh in the enclosing scope. @@ -323,6 +323,9 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. * Targeted specialized methods needed by Lua dispatch must remain attached to the IM classes consumed by `LuaDispatchPreparation`. Concrete implementations for erased generic objects must bind the same root slot used by the call site. +* Do not promise Lua support for a method which combines type parameters from its owning generic class with + independent method type parameters. Serialization loaders should be free generic functions, or class methods + parameterized only by their owning class. * Generate no runtime reflection registry, type-name lookup, type-id switch, or serialization-specific metadata. ### Required regression coverage diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java index cd9ff1b95..fe18491d1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -2,6 +2,7 @@ import de.peeeq.wurstscript.ast.ExprClosure; import de.peeeq.wurstscript.ast.ExprFunctionCall; +import de.peeeq.wurstscript.attributes.AttrFuncDef; /** Source-level compiler intrinsics which must be eliminated before backend emission. */ public final class CompilerIntrinsics { @@ -17,13 +18,13 @@ private CompilerIntrinsics() { public static boolean isForFields(ExprFunctionCall call) { return FOR_FIELDS.equals(call.getFuncName()) && hasClosureArgument(call) - && call.lookupFuncs(FOR_FIELDS).isEmpty(); + && !AttrFuncDef.hasApplicableUserFunction(call); } public static boolean isMapFields(ExprFunctionCall call) { return MAP_FIELDS.equals(call.getFuncName()) && hasClosureArgument(call) - && call.lookupFuncs(MAP_FIELDS).isEmpty(); + && !AttrFuncDef.hasApplicableUserFunction(call); } public static boolean isFieldIteration(ExprFunctionCall call) { @@ -31,7 +32,7 @@ public static boolean isFieldIteration(ExprFunctionCall call) { } public static boolean isNew(ExprFunctionCall call) { - return NEW.equals(call.getFuncName()) && call.lookupFuncs(NEW).isEmpty(); + return NEW.equals(call.getFuncName()) && !AttrFuncDef.hasApplicableUserFunction(call); } private static boolean hasClosureArgument(ExprFunctionCall call) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 87b8ef82a..4b031def2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -511,6 +511,26 @@ public static List argumentTypesPre(StmtCall node) { return result; } + /** Checks whether normal overload resolution has an applicable visible function before a compiler fallback. */ + public static boolean hasApplicableUserFunction(ExprFunctionCall node) { + ImmutableCollection candidates = node.lookupFuncs(node.getFuncName()); + if (candidates.isEmpty()) { + return false; + } + List argumentTypes = argumentTypesPre(node); + for (FuncLink candidate : candidates) { + if (candidate.getVisibility() == Visibility.PRIVATE_OTHER + || candidate.getVisibility() == Visibility.PROTECTED_OTHER) { + continue; + } + FunctionSignature signature = FunctionSignature.fromNameLink(candidate); + if (signature.matchAgainstArgs(argumentTypes, node) != null) { + return true; + } + } + return false; + } + private static FuncLink searchFunction(String funcName, @Nullable FuncRef node, List argumentTypes) { if (node == null) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 5a8a72887..15958bf9b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -459,6 +459,41 @@ public void ordinaryNewInstanceNameRemainsUserFunction() { ); } + @Test + public void unrelatedOverloadsDoNotSuppressCompilerFunctions() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " int total = 0", + "", + " function forFields(int value) returns int", + " return value", + " function mapFields(int value) returns int", + " return value", + " function newInstance(int value) returns int", + " return value", + "", + " class State", + " int value = 3", + "", + " function add(int value)", + " total += value", + "", + " init", + " let state = new State", + " State freshState = newInstance()", + " forFields(state, (name, value) -> add(value))", + " mapFields(state, (name, value) -> value + 1)", + " if total == 3 and state.value == 4 and freshState.value == 3", + " testSuccess()", + "endpackage" + ); + } + @Test public void explicitTargetTemporaryDoesNotCollideWithUserLocal() { test() From 73ed649573ad661f066c778e2dd73de90436e1fa Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 17:27:43 +0200 Subject: [PATCH 6/9] Handle generic helper and module qualifier edges --- AGENTS.md | 2 + .../wurstscript/attributes/AttrFuncDef.java | 4 ++ .../attributes/AttrImplicitParameter.java | 33 +++++++++----- .../tests/FieldIterationTests.java | 44 +++++++++++++++++++ 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d427a574f..bd8ac73b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -326,6 +326,8 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. * Do not promise Lua support for a method which combines type parameters from its owning generic class with independent method type parameters. Serialization loaders should be free generic functions, or class methods parameterized only by their owning class. +* Do not call `newInstance()` from the constructor of a generic class. Construct the simple state object in the + generic loader, then initialize any nested state explicitly after construction. * Generate no runtime reflection registry, type-name lookup, type-id switch, or serialization-specific metadata. ### Required regression coverage diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 4b031def2..4509b56e3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -524,6 +524,10 @@ public static boolean hasApplicableUserFunction(ExprFunctionCall node) { continue; } FunctionSignature signature = FunctionSignature.fromNameLink(candidate); + if (!node.getTypeArgs().isEmpty() + && node.getTypeArgs().size() != signature.getDefinitionTypeVariables().size()) { + continue; + } if (signature.matchAgainstArgs(argumentTypes, node) != null) { return true; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java index 1be644d50..014bd7a1c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java @@ -10,7 +10,7 @@ public class AttrImplicitParameter { public static OptExpr getImplicitParameter(ExprMemberVar e) { - Expr result = getImplicitParameterUsingLeft(e); + Expr result = getImplicitParameterUsingLeft(e, isDynamicVariable(e)); if (result == null) { return getImplicitParamterCaseNormalVar(e); } else { @@ -19,7 +19,7 @@ public static OptExpr getImplicitParameter(ExprMemberVar e) { } public static OptExpr getImplicitParameter(ExprMemberArrayVar e) { - Expr result = getImplicitParameterUsingLeft(e); + Expr result = getImplicitParameterUsingLeft(e, isDynamicVariable(e)); if (result == null) { return getImplicitParamterCaseNormalVar(e); } else { @@ -41,11 +41,14 @@ public static OptExpr getImplicitParameter(ExprFunctionCall e) { } public static OptExpr getImplicitParameter(ExprMemberMethod e) { - Expr result = getImplicitParameterUsingLeft(e); + FuncLink calledFunc = e.attrFuncLink(); + boolean requiresReceiver = calledFunc != null + && (calledFunc.getDef().attrIsDynamicClassMember() + || calledFunc.getDef() instanceof ExtensionFuncDef); + Expr result = getImplicitParameterUsingLeft(e, requiresReceiver); if (result == null) { return getImplicitParameterCaseNormalFunctionCall(e); } else { - FuncLink calledFunc = e.attrFuncLink(); if (calledFunc != null && !calledFunc.getDef().attrIsDynamicClassMember() && !(calledFunc.getDef() instanceof ExtensionFuncDef)) { @@ -56,12 +59,12 @@ public static OptExpr getImplicitParameter(ExprMemberMethod e) { } } - private static @Nullable Expr getImplicitParameterUsingLeft(HasReceiver e) { + private static @Nullable Expr getImplicitParameterUsingLeft(HasReceiver e, boolean requiresReceiver) { if (e.getLeft().attrTyp().isStaticRef()) { - // Module-instance qualifiers are static references, but a qualified access such as - // object.Module.field still uses object as the dynamic receiver of field. - if (e.getLeft() instanceof HasReceiver qualifiedLeft) { - return getImplicitParameterUsingLeft(qualifiedLeft); + // Module-instance qualifiers are static references, but a final dynamic member in an + // access such as object.Module.member still uses object as its receiver. + if (requiresReceiver && e.getLeft() instanceof HasReceiver qualifiedLeft) { + return getImplicitParameterUsingLeft(qualifiedLeft, true); } // we have a static ref like Math.sqrt() // this will be handled like if we just have sqrt() @@ -71,6 +74,13 @@ public static OptExpr getImplicitParameter(ExprMemberMethod e) { return e.getLeft(); } + private static boolean isDynamicVariable(NameRef e) { + NameLink nameLink = e.attrNameLink(); + return nameLink != null + && nameLink.getDef() instanceof VarDef variable + && variable.attrIsDynamicClassMember(); + } + private static OptExpr getImplicitParameterCaseNormalFunctionCall(FunctionCall e) { FuncLink calledFunc = e.attrFuncLink(); return getFunctionCallImplicitParameter(e, calledFunc, true); @@ -79,7 +89,10 @@ private static OptExpr getImplicitParameterCaseNormalFunctionCall(FunctionCall e static OptExpr getFunctionCallImplicitParameter(FunctionCall e, FuncLink calledFunc, boolean showError) { if (e instanceof HasReceiver) { HasReceiver hasReceiver = (HasReceiver) e; - Expr res = getImplicitParameterUsingLeft(hasReceiver); + boolean requiresReceiver = calledFunc != null + && (calledFunc.getDef().attrIsDynamicClassMember() + || calledFunc.getDef() instanceof ExtensionFuncDef); + Expr res = getImplicitParameterUsingLeft(hasReceiver, requiresReceiver); if (res != null) { return res; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 15958bf9b..f47969255 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -494,6 +494,27 @@ public void unrelatedOverloadsDoNotSuppressCompilerFunctions() { ); } + @Test + public void unrelatedGenericArityDoesNotSuppressNewInstance() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " function newInstance() returns int", + " return 0", + " class State", + " int value = 7", + " init", + " State state = newInstance()", + " if state.value == 7", + " testSuccess()", + "endpackage" + ); + } + @Test public void explicitTargetTemporaryDoesNotCollideWithUserLocal() { test() @@ -805,6 +826,29 @@ public void qualifiesFieldsFromSiblingModules() { ); } + @Test + public void keepsQualifiedStaticModuleMembersStatic() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " module Shared", + " static int value = 7", + " static function read() returns int", + " return value", + " class State", + " use Shared", + " init", + " let state = new State", + " if state.Shared.value == 7 and state.Shared.read() == 7", + " testSuccess()", + "endpackage" + ); + } + @Test public void excludesPrivateModuleFields() { test() From f272c5a5fcb9b7d36c5cc42166867f46ab760dcc Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 18:06:22 +0200 Subject: [PATCH 7/9] Identify generic construction marker by identity --- AGENTS.md | 3 +++ .../imtranslation/ImTranslator.java | 3 +-- .../wurstscript/tests/FieldIterationTests.java | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bd8ac73b5..5d5402b20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,6 +304,9 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. * The public Wurst names are `forFields`, `mapFields`, and `newInstance()`; do not introduce underscore-prefixed alternatives. Internal markers must never survive backend lowering. +* Names beginning with the compiler-internal `__wurst` prefix are reserved. Generated temporaries must be fresh + against user-visible enclosing declarations, but nested callback locals deliberately using that prefix are not + supported. * An applicable visible ordinary function with one of these names must resolve normally. Compiler handling is only the fallback when no user-visible overload accepts the call. * `forFields` includes accessible, non-static instance fields, including inherited, module-injected, readonly, and 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 a8f1eee39..8797b7cc3 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 @@ -1565,8 +1565,7 @@ public ImFunction getGenericNewMarker() { } public boolean isGenericNewMarker(ImFunction function) { - return function == genericNewMarker - || de.peeeq.wurstscript.CompilerIntrinsics.NEW_MARKER.equals(function.getName()); + return function == genericNewMarker; } public ImFunction getConstructNewFunc(ConstructorDef constr) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index f47969255..8206a9371 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -515,6 +515,24 @@ public void unrelatedGenericArityDoesNotSuppressNewInstance() { ); } + @Test + public void userFunctionNamedLikeInternalNewMarkerRemainsOrdinary() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " function wurstNewMarker(int value) returns int", + " return value + 1", + " init", + " if wurstNewMarker(2) == 3", + " testSuccess()", + "endpackage" + ); + } + @Test public void explicitTargetTemporaryDoesNotCollideWithUserLocal() { test() From 97fc192c3c1ce88aabdb084e06f4f893b2f17148 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 18:55:24 +0200 Subject: [PATCH 8/9] Respect protected field accessibility in mapping --- AGENTS.md | 4 ++ .../de/peeeq/wurstscript/SyntacticSugar.java | 13 +++- .../tests/FieldIterationTests.java | 61 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 5d5402b20..9680553e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -329,8 +329,12 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. * Do not promise Lua support for a method which combines type parameters from its owning generic class with independent method type parameters. Serialization loaders should be free generic functions, or class methods parameterized only by their owning class. +* Do not require Lua specialization of generic-construction methods invoked directly on a freshly constructed + generic receiver. Use the free generic loader shape, or bind the receiver to a typed local first. * Do not call `newInstance()` from the constructor of a generic class. Construct the simple state object in the generic loader, then initialize any nested state explicitly after construction. +* Nested modules whose sibling submodules declare equal field names are outside the supported field-key model. + Dedicated state classes should use direct fields, ordinary inheritance, or non-conflicting shallow module fields. * Generate no runtime reflection registry, type-name lookup, type-id switch, or serialization-specific metadata. ### Required regression coverage diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java index c07d4c23b..ace3e1c3b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/SyntacticSugar.java @@ -368,14 +368,25 @@ private void addInstanceFields(Iterable declarations, ? declaringClass == null || !accessSite.isSubtreeOf(declaringClass) : declaringClass != concreteClass); boolean privateFromAnotherModule = field.attrIsPrivate() && declaringModule != null; + boolean inaccessibleProtected = explicitTarget && field.attrIsProtected() + && !canAccessProtectedField(field, declaringClass, accessSite); boolean mutableEnough = !requireMutable || (!field.attrIsReadonly() && !field.attrIsConstant()); if (!field.attrIsStatic() && mutableEnough - && !privateFromAnotherClass && !privateFromAnotherModule) { + && !privateFromAnotherClass && !privateFromAnotherModule && !inaccessibleProtected) { fields.add(new FieldInfo(field, modulePath)); } } } + private boolean canAccessProtectedField(GlobalVarDef field, ClassDef declaringClass, Element accessSite) { + if (accessSite.attrNearestPackage() == field.attrNearestPackage()) { + return true; + } + ClassDef accessClass = accessSite.attrNearestClassDef(); + return accessClass != null && declaringClass != null + && accessClass.attrTypC().isSubtypeOf(declaringClass.attrTypC(), accessSite); + } + private boolean hasShadowingLocal(ExprClosure closure, String nameParameter, String valueParameter) { final boolean[] result = {false}; closure.getImplementation().accept(new WurstModel.DefaultVisitor() { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 8206a9371..4e974c49c 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -867,6 +867,67 @@ public void keepsQualifiedStaticModuleMembersStatic() { ); } + @Test + public void explicitTargetExcludesInaccessibleProtectedFields() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .compilationUnits( + compilationUnit("state.wurst", + "package StateTypes", + " public class BaseState", + " protected int shared = 100", + " function getShared() returns int", + " return shared", + " public class State extends BaseState", + " int local = 2", + "endpackage"), + compilationUnit("consumer.wurst", + "package FieldIterationTest", + " import StateTypes", + " native testSuccess()", + " int total = 0", + " function add(int value)", + " total += value", + " init", + " let state = new State", + " forFields(state, (name, value) -> add(value))", + " mapFields(state, (name, value) -> value + 1)", + " if total == 2 and state.local == 3 and state.getShared() == 100", + " testSuccess()", + "endpackage") + ); + } + + @Test + public void explicitTargetIncludesPackageAccessibleProtectedFields() { + test() + .testLua(true) + .luaOnly(false) + .executeProg() + .lines( + "package FieldIterationTest", + " native testSuccess()", + " int total = 0", + " function add(int value)", + " total += value", + " class BaseState", + " protected int shared = 100", + " function getShared() returns int", + " return shared", + " class State extends BaseState", + " int local = 2", + " init", + " let state = new State", + " forFields(state, (name, value) -> add(value))", + " mapFields(state, (name, value) -> value + 1)", + " if total == 102 and state.local == 3 and state.getShared() == 101", + " testSuccess()", + "endpackage" + ); + } + @Test public void excludesPrivateModuleFields() { test() From 6fa565854726caeb0c76fbf38977eece65fa4f9d Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 19:11:10 +0200 Subject: [PATCH 9/9] Define final generic construction boundaries --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9680553e1..4aec45f1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -331,8 +331,12 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. parameterized only by their owning class. * Do not require Lua specialization of generic-construction methods invoked directly on a freshly constructed generic receiver. Use the free generic loader shape, or bind the receiver to a typed local first. +* Lua generic-construction dispatch through multi-parameter generic interfaces is outside the supported loader + shape. The supported generic loader has a single construction type parameter. * Do not call `newInstance()` from the constructor of a generic class. Construct the simple state object in the generic loader, then initialize any nested state explicitly after construction. +* `newInstance()` is a runtime Jass/Lua construction surface and is not supported inside `compiletime(...)` + evaluation. Do not expand interpreter behavior for compile-time construction. * Nested modules whose sibling submodules declare equal field names are outside the supported field-key model. Dedicated state classes should use direct fields, ordinary inheritance, or non-conflicting shallow module fields. * Generate no runtime reflection registry, type-name lookup, type-id switch, or serialization-specific metadata.