diff --git a/AGENTS.md b/AGENTS.md index 864d99705..4aec45f1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -292,3 +292,59 @@ 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. +* 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 + 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. +* 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. +* 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. + +### 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. 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..fe18491d1 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/CompilerIntrinsics.java @@ -0,0 +1,41 @@ +package de.peeeq.wurstscript; + +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 { + + 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) + && !AttrFuncDef.hasApplicableUserFunction(call); + } + + public static boolean isMapFields(ExprFunctionCall call) { + return MAP_FIELDS.equals(call.getFuncName()) + && hasClosureArgument(call) + && !AttrFuncDef.hasApplicableUserFunction(call); + } + + public static boolean isFieldIteration(ExprFunctionCall call) { + return isForFields(call) || isMapFields(call); + } + + public static boolean isNew(ExprFunctionCall call) { + return NEW.equals(call.getFuncName()) && !AttrFuncDef.hasApplicableUserFunction(call); + } + + 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..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 @@ -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,78 @@ 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); + 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); + 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, assignsResult); 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 +288,21 @@ 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, + boolean requireMutable) { 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, + requireMutable); } else if (owner instanceof ModuleDef moduleDef) { - addInstanceFields(moduleDef.getVars(), fields, null, List.of(), moduleDef); + addInstanceFields(moduleDef.getVars(), fields, null, List.of(), moduleDef, + accessSite, explicitTarget, requireMutable); } return fields; } @@ -260,24 +311,32 @@ private void collectInheritedFields(WurstTypeClass type, List fields, ClassDef concreteClass, Set visitedClasses, - Set visitedModules) { + Set visitedModules, + Element accessSite, + boolean explicitTarget, + boolean requireMutable) { 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, requireMutable); } addModuleFields(type.getClassDef().getModuleInstanciations(), fields, concreteClass, - visitedModules, List.of()); - addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null); + visitedModules, List.of(), accessSite, explicitTarget, requireMutable); + addInstanceFields(type.getClassDef().getVars(), fields, concreteClass, List.of(), null, + accessSite, explicitTarget, requireMutable); } private void addModuleFields(Iterable modules, List fields, ClassDef concreteClass, Set visited, - List parentPath) { + List parentPath, + Element accessSite, + boolean explicitTarget, + boolean requireMutable) { for (ModuleInstanciation module : modules) { if (!visited.add(module)) { continue; @@ -287,8 +346,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, requireMutable); + addInstanceFields(module.getVars(), fields, concreteClass, modulePath, module.attrModuleOrigin(), + accessSite, explicitTarget, requireMutable); } } @@ -296,18 +357,36 @@ private void addInstanceFields(Iterable declarations, List fields, ClassDef concreteClass, List modulePath, - ModuleDef declaringModule) { + ModuleDef declaringModule, + Element accessSite, + boolean explicitTarget, + boolean requireMutable) { 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) { + boolean inaccessibleProtected = explicitTarget && field.attrIsProtected() + && !canAccessProtectedField(field, declaringClass, accessSite); + boolean mutableEnough = !requireMutable || (!field.attrIsReadonly() && !field.attrIsConstant()); + if (!field.attrIsStatic() && mutableEnough + && !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() { @@ -328,13 +407,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 +516,20 @@ 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)); + for (String module : field.modulePath) { + left = Ast.ExprMemberVarDot(source, left, Ast.Identifier(source, module)); + } + } 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..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 @@ -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) { @@ -507,6 +511,30 @@ 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 (!node.getTypeArgs().isEmpty() + && node.getTypeArgs().size() != signature.getDefinitionTypeVariables().size()) { + continue; + } + 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/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/attributes/AttrImplicitParameter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java index e195e86e4..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,8 +59,13 @@ 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 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() // if we have an implicit parameter depends on whether sqrt is static or not @@ -66,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); @@ -74,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/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..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 @@ -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(); @@ -31,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 @@ -72,6 +78,9 @@ public void transform() { eliminateGenericUses(); dbg(summary("after eliminateGenericUses")); + eliminateRemainingGenericNewCalls(); + eliminateGenericUses(); + dbgMethodsByName("after eliminateGenericUses"); makeNullAssignmentsSafe(); @@ -82,12 +91,182 @@ 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; + collectUnspecializedGenericClassMethods(); + collectGenericNewRoots(); + eliminateGenericUses(); + eliminateRemainingGenericNewCalls(); + assertNoReachableGenericNewMarkers(); + } + + private void collectGenericNewRoots() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunction function) { + if (!function.getTypeVariables().isEmpty() + || unspecializedGenericClassMethods.contains(function)) { + return; + } + super.visit(function); + } + + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + collectGenericNewUse(call); + } + + @Override + public void visit(ImMethodCall 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); + } + + @Override + public void visit(ImMethodCall 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 void collectGenericNewUse(ImMethodCall call) { + ImMethod method = call.getMethod(); + if (!methodContainsGenericNew(method, + Collections.newSetFromMap(new IdentityHashMap<>()), + 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())) { + return true; + } + } + return false; + } + + private boolean functionContainsGenericNew(ImFunction function, Set visited) { + 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}; + function.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + if (translator.isGenericNewMarker(call.getFunc()) + || 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; + } + super.visit(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 + public void visit(ImFunction function) { + if (!function.getTypeVariables().isEmpty() + || unspecializedGenericClassMethods.contains(function)) { + 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 +284,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 @@ -300,45 +494,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()); + addMemberTypeArguments(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())); - } - }); } + 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; @@ -501,6 +684,51 @@ 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() + || unspecializedGenericClassMethods.contains(function)) { + 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 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() { @@ -510,6 +738,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 +801,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 +812,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; } @@ -609,21 +844,55 @@ 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(m.getName() + "⟪" + generics.makeName() + "⟫"); - newM.setImplementation(specializeFunction(newM.getImplementation(), generics)); - adaptSubmethods(m.getSubMethods(), newM); + 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, generics); return newM; } - private void adaptSubmethods(List oldSubMethods, ImMethod 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, GenericTypes generics) { newM.setSubMethods(new ArrayList<>()); ImClassType newClassT = newM.getMethodClass(); ImClass newMClass = newClassT.getClassDef(); @@ -631,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); @@ -752,7 +1034,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); @@ -961,6 +1245,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 +1567,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..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 @@ -1553,6 +1553,21 @@ 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; + } + 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..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 @@ -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" ); } @@ -124,6 +274,292 @@ 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 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 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 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 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() + .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() @@ -148,7 +584,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 +606,7 @@ public void rejectsExplicitFieldIterationParameterTypes() { " int value", "", " function save()", - " __wurst_forFields((NoSuch name, NoSuch value) -> value)", + " forFields((NoSuch name, NoSuch value) -> value)", "endpackage" ); } @@ -185,7 +621,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 +636,7 @@ public void rejectsDuplicateFieldIterationParameterNames() { " int value", "", " function save()", - " __wurst_mapFields((value, value) -> 42)", + " mapFields((value, value) -> 42)", "endpackage" ); } @@ -217,7 +653,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 +679,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 +704,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 +731,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 +756,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 +796,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 +833,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", @@ -408,6 +844,90 @@ 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 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() @@ -425,7 +945,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 +964,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");