diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java index 85a2b3fd5..1e4b35cd2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java @@ -56,10 +56,10 @@ private static void translateInnerClasses(ClassOrModuleOrModuleInstanciation mi, * translates the given classDef */ private void translate() { + imClass = translator.getClassFor(classDef); Map ov = translator.getTypeVarOverridesForClass(classDef); translator.pushTypeVarOverrides(ov); try { - imClass = translator.getClassFor(classDef); prog.getClasses().add(imClass); addSuperClasses(); @@ -193,6 +193,7 @@ private void addOnDestroyActions(ImFunction f, List addTo, ClassOrModule typeArgs.add(tp.imTranslateToTypeArgument(translator)); } } + typeArgs.addAll(translator.capturedTypeArguments(extended.getDef(), extended.getTypeArgBinding())); addTo.add(ImFunctionCall(c, onDestroy, typeArgs, ImExprs(ImVarAccess(thisVar)), false, CallType.NORMAL)); } } @@ -427,6 +428,8 @@ private void createConstructFunc(ConstructorDef constr) { typeArgs.add(bt.imTranslateToTypeArgument(translator)); } } + typeArgs.addAll(translator.capturedTypeArguments(extendedTypeC.getDef(), + extendedTypeC.getTypeArgBinding())); } f.getBody().add(ImFunctionCall(trace, superConstrFunc, typeArgs, arguments, false, CallType.NORMAL)); } 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 a018d2b5f..1077888f8 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 @@ -1569,21 +1569,6 @@ private void moveFunctionsOutOfClass(ImClass c) { typeArgs.add(JassIm.ImTypeArgument(JassIm.ImTypeVarRef(ta), Collections.emptyMap())); } rewriteGenerics(f, new GenericTypes(typeArgs), c.getTypeVariables()); - - // NEW: fill implicit type args for captured generics (Inner -> Inner) - Map scope = new HashMap<>(); - for (ImTypeVar tv : f.getTypeVariables()) { - scope.put(tv.getName(), tv); - } - - f.setReturnType(fillMissingTypeArgsFromScope(f.getReturnType(), scope)); - - for (ImVar p : f.getParameters()) { - p.setType(fillMissingTypeArgsFromScope(p.getType(), scope)); - } - for (ImVar l : f.getLocals()) { - l.setType(fillMissingTypeArgsFromScope(l.getType(), scope)); - } } } @@ -3261,63 +3246,6 @@ public void eliminate() { } } - private ImType fillMissingTypeArgsFromScope(ImType t, Map scope) { - return t.match(new ImType.Matcher() { - - @Override - public ImType case_ImClassType(ImClassType ct) { - int need = ct.getClassDef().getTypeVariables().size(); - int have = ct.getTypeArguments().size(); - if (need == 0 || have >= need) { - return ct; - } - - ImTypeArguments newArgs = JassIm.ImTypeArguments(); - // keep existing args - for (ImTypeArgument a : ct.getTypeArguments()) { - newArgs.add(a.copy()); - } - - // fill missing args by name from scope - for (int i = have; i < need; i++) { - ImTypeVar tv = ct.getClassDef().getTypeVariables().get(i); - ImTypeVar inScope = scope.get(tv.getName()); - if (inScope == null) { - // no suitable type var in scope -> cannot fill - return ct; - } - newArgs.add(JassIm.ImTypeArgument(JassIm.ImTypeVarRef(inScope), Collections.emptyMap())); - } - - return JassIm.ImClassType(ct.getClassDef(), newArgs); - } - - @Override - public ImType case_ImArrayType(ImArrayType at) { - return JassIm.ImArrayType(fillMissingTypeArgsFromScope(at.getEntryType(), scope)); - } - - @Override - public ImType case_ImArrayTypeMulti(ImArrayTypeMulti at) { - return JassIm.ImArrayTypeMulti(fillMissingTypeArgsFromScope(at.getEntryType(), scope), at.getArraySize()); - } - - @Override - public ImType case_ImTupleType(ImTupleType tt) { - List ts = new ArrayList<>(); - for (ImType x : tt.getTypes()) { - ts.add(fillMissingTypeArgsFromScope(x, scope)); - } - return JassIm.ImTupleType(ts, tt.getNames()); - } - - @Override public ImType case_ImVoid(ImVoid v) { return v; } - @Override public ImType case_ImAnyType(ImAnyType a) { return a; } - @Override public ImType case_ImSimpleType(ImSimpleType s) { return s; } - @Override public ImType case_ImTypeVarRef(ImTypeVarRef r) { return r; } - }); - } - private static String id(Object o) { return o == null ? "null" : (o.getClass().getSimpleName() + "@" + System.identityHashCode(o)); } 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 f183752e6..d5d8fbcc6 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 @@ -27,6 +27,7 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import io.vavr.control.Option; import org.eclipse.jdt.annotation.Nullable; import org.jetbrains.annotations.NotNull; @@ -350,9 +351,72 @@ private static String typeParamOwnerName(TypeParamDef tp) { return "type"; } - public Map getTypeVarOverridesForClass(ClassDef cd) { - Map m = capturedOwnerTypeVarsByStaticClass.get(cd); - return (m == null) ? Collections.emptyMap() : m; + /** + * The type variable each type parameter in scope of a member of {@code c} stands for: the class's own and, for a + * static class inside a generic class, the ones it captures from that class + * ({@link #addCapturedTypeVarsFromOwningGeneric}). A member is translated with these whatever the code which first + * asks for it, so the parameter of the enclosing class is the static class's captured variable everywhere in it. + */ + public Map getTypeVarOverridesForClass(ClassOrInterface c) { + Map result = new IdentityHashMap<>(); + for (ImTypeVar tv : getClassFor(c).getTypeVariables()) { + TypeParamDef tp = typeVariableReverse.get(tv); + if (tp != null) { + result.put(tp, tv); + } + } + return result; + } + + /** The overrides of {@link #getTypeVarOverridesForClass} for the class {@code member} belongs to, if any. */ + private Map getTypeVarOverridesForMember(de.peeeq.wurstscript.ast.Element member) { + ClassOrInterface c = member.attrNearestClassOrInterface(); + return c == null ? Collections.emptyMap() : getTypeVarOverridesForClass(c); + } + + /** + * The type arguments of a type of {@code c} for the type parameters it captures from the generic class around it + * ({@link #addCapturedTypeVarsFromOwningGeneric}), which follow its own in its type variables. Each is what + * {@code binding} binds the parameter to ({@code new Outer().make()} returns a {@code Base} with T bound to + * int), or else the parameter as the code being translated sees it: the enclosing class's own variable in its + * members, and the captured variable in the members of a static class. + */ + public List capturedTypeArguments(ClassOrInterface c, VariableBinding binding) { + List parameters = capturedTypeParameters(c); + if (parameters.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (TypeParamDef tp : parameters) { + Option bound = binding.get(tp); + result.add(bound.isDefined() + ? bound.get().imTranslateToTypeArgument(this) + : JassIm.ImTypeArgument(JassIm.ImTypeVarRef(getTypeVar(tp)), Collections.emptyMap())); + } + return result; + } + + /** + * The type parameters of the generic class around {@code c} which the static class captures + * ({@link #addCapturedTypeVarsFromOwningGeneric}), in the order of its type variables. + */ + private List capturedTypeParameters(ClassOrInterface c) { + if (!(c instanceof ClassDef cd)) { + return Collections.emptyList(); + } + ImClass imClass = getClassFor(cd); + Map captured = capturedOwnerTypeVarsByStaticClass.get(cd); + if (captured == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (ImTypeVar tv : imClass.getTypeVariables()) { + TypeParamDef tp = typeVariableReverse.get(tv); + if (tp != null && captured.get(tp) == tv) { + result.add(tp); + } + } + return result; } public void pushTypeVarOverrides(Map m) { @@ -1191,16 +1255,11 @@ public ImClassType selfType(StructureDef classDef) { return selfType(imClass); } + /** The type of this in the members of the class: the class with its own type variables, the captured ones too. */ public ImClassType selfType(ImClass imClass) { ImTypeArguments typeArgs = JassIm.ImTypeArguments(); for (ImTypeVar tv : imClass.getTypeVariables()) { - TypeParamDef tpd = typeVariableReverse.get(tv); - - // If this ImTypeVar corresponds to an owner TypeParamDef (captured case), - // resolve it through context so owner context uses owner vars, Iterator context uses captured vars. - ImTypeVar tvForContext = (tpd != null) ? getTypeVar(tpd) : tv; - - typeArgs.add(JassIm.ImTypeArgument(JassIm.ImTypeVarRef(tvForContext), Collections.emptyMap())); + typeArgs.add(JassIm.ImTypeArgument(JassIm.ImTypeVarRef(tv), Collections.emptyMap())); } return JassIm.ImClassType(imClass, typeArgs); } @@ -1291,12 +1350,17 @@ public ImFunction getFuncFor(TranslatedToImFunction funcDef) { ImTypeVars typeVars = collectTypeVarsForFunction(funcDef); ImFunction f = ImFunction(funcDef, name, typeVars, ImVars(), ImVoid(), ImVars(), ImStmts(), flags); + // The signature of a class member sees the type variables of its class, not those of the code asking for it. + Map classOv = funcDef instanceof ExprClosure + ? Collections.emptyMap() : getTypeVarOverridesForMember(funcDef); Map ov = getTypeVarOverridesForFunction(f); + pushTypeVarOverrides(classOv); pushTypeVarOverrides(ov); try { funcDef.imCreateFuncSkeleton(this, f); } finally { popTypeVarOverrides(ov); + popTypeVarOverrides(classOv); } addFunction(f, funcDef); @@ -1363,6 +1427,9 @@ public void case_FuncDef(FuncDef funcDef) { ClassOrInterface owner = funcDef.attrNearestClassOrInterface(); if (owner != null) { handleTypeParameters(owner.getTypeParameters()); + // A static function is not in its class, so it takes the parameters a static class captures + // from the generic class around it as its own too. + capturedTypeParameters(owner).forEach(this::handleTypeParameter); } } handleTypeParameters(funcDef.getTypeParameters()); @@ -1539,7 +1606,9 @@ public int getTupleIndex(TupleDef tupleDef, VarDef parameter) { public ImVar getVarFor(VarDef varDef) { ImVar v = varMap.get(varDef); if (v == null) { - Map ov = getOwnerTypeVarOverridesForStaticClassVar(varDef); + // A field sees the type variables of its class, not those of the code asking for it. + Map ov = varDef instanceof GlobalVarDef + ? getTypeVarOverridesForMember(varDef) : Collections.emptyMap(); pushTypeVarOverrides(ov); ImType type; try { @@ -1558,25 +1627,6 @@ public ImVar getVarFor(VarDef varDef) { return v; } - private Map getOwnerTypeVarOverridesForStaticClassVar(VarDef varDef) { - if (!(varDef instanceof GlobalVarDef) || !varDef.attrIsStatic()) { - return Collections.emptyMap(); - } - ClassOrInterface owner = varDef.attrNearestClassOrInterface(); - if (owner == null) { - return Collections.emptyMap(); - } - Map result = new IdentityHashMap<>(); - if (owner instanceof AstElementWithTypeParameters astElementWithTypeParameters) { - for (TypeParamDef tp : astElementWithTypeParameters.getTypeParameters()) { - if (tp.getTypeParamConstraints() instanceof TypeExprList) { - result.put(tp, typeVariable.getFor(tp)); - } - } - } - return result; - } - private boolean isNamedScopeVar(VarDef varDef) { if (varDef.getParent() == null) { return false; @@ -2813,7 +2863,8 @@ public ImClass getClassFor(ClassOrInterface s) { if (s1 instanceof AstElementWithTypeParameters astElementWithTypeParameters) { for (TypeParamDef tp : astElementWithTypeParameters.getTypeParameters()) { if (tp.getTypeParamConstraints() instanceof TypeExprList) { - ImTypeVar tv = getTypeVar(tp); // now context-aware (override stack) + // not getTypeVar: the class's variables must not depend on the code which first asks for it + ImTypeVar tv = typeVariable.getFor(tp); if (!hasTypeVarNamed(typeVariables, tv.getName())) { typeVariables.add(tv); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/InterfaceTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/InterfaceTranslator.java index da24b6d73..d51500a3f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/InterfaceTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/InterfaceTranslator.java @@ -188,35 +188,38 @@ private ImFunction implementationOfItsOwn(ImClass imClass, WurstTypeClass classT /** * The type arguments for {@code variables}, the type variables of {@code owner}, a class or interface above - * {@code classType}, as that class sees them. + * {@code classType}, as that class sees them: those of the type of {@code owner} among its supertypes, translated + * as in the class's own functions. So in a static class inside a generic class, the parameter of that class is + * the static class's captured variable, which no supertype binds. */ private List typeArgumentsAsSeenFrom(WurstTypeClass classType, ClassOrInterface owner, List variables) { - VariableBinding binding = VariableBinding.emptyMapping(); ArrayDeque queue = new ArrayDeque<>(); queue.add(classType); while (!queue.isEmpty()) { WurstTypeClassOrInterface type = queue.removeFirst(); if (type.getDef() == owner) { - binding = type.getTypeArgBinding(); + List arguments = translateAsIn(classType.getDef(), type).getTypeArguments().removeAll(); + if (arguments.size() == variables.size()) { + return arguments; + } break; } queue.addAll(type.directSupertypes()); } - List arguments = new java.util.ArrayList<>(); - for (ImTypeVar variable : variables) { - TypeParamDef parameter = translator.getTypeParamDef(variable); - if (parameter == null) { - throw new CompileError(classType.getDef(), "Could not find the type argument of " + owner.getName() - + " for " + variable.getName() + " as " + classType.getDef().getName() + " sees it."); - } - // A parameter no supertype binds is one of an enclosing generic class, which a static class inside it - // captures: the class sees it as it is, as its own functions do. - ImType type = binding.get(parameter).map(bound -> bound.imTranslateType(translator)) - .getOrElse(() -> JassIm.ImTypeVarRef(translator.getTypeVar(parameter))); - arguments.add(JassIm.ImTypeArgument(type, Collections.emptyMap())); + throw new CompileError(classType.getDef(), "Could not find the type arguments of " + owner.getName() + + " as " + classType.getDef().getName() + " sees it."); + } + + /** {@code type} translated as in the functions of {@code c}. */ + private ImClassType translateAsIn(ClassDef c, WurstTypeClassOrInterface type) { + Map overrides = translator.getTypeVarOverridesForClass(c); + translator.pushTypeVarOverrides(overrides); + try { + return (ImClassType) type.imTranslateType(translator); + } finally { + translator.popTypeVarOverrides(overrides); } - return arguments; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeClassOrInterface.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeClassOrInterface.java index c897e6b0e..29b328bf7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeClassOrInterface.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeClassOrInterface.java @@ -141,6 +141,7 @@ public final ImType imTranslateType(ImTranslator tr) { typeArgs.add(btp.imTranslateToTypeArgument(tr)); } } + typeArgs.addAll(tr.capturedTypeArguments(getDef(), getTypeArgBinding())); return JassIm.ImClassType(tr.getClassFor(getDef()), typeArgs); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java index 355a4294c..80181eb25 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java @@ -678,6 +678,50 @@ public void bridgesOfAGenericClassAreTheSameInAnyUnitOrder() throws IOException assertEquals(Files.toString(lua, Charsets.UTF_8), firstLua, "Lua must not depend on the unit order"); } + /** + * A static class inside a generic class inherits the implementation of an interface's method over the class's + * type parameter, and the interface is in a package of its own. Translating the interface first asks for Base's + * method before Outer is translated: the members of a static class see the parameter as its captured variable + * whichever code asks for them first, so the Jass and the Lua do not depend on the unit order. + */ + @Test + public void staticClassOfAGenericClassIsTheSameInAnyUnitOrder() throws IOException { + List units = List.of( + compilationUnit("OmegaLib.wurst", + "package OmegaLib", + "public interface Omega", + " function m(A a) returns A"), + compilationUnit("Lib.wurst", + "package Lib", + "import OmegaLib", + "public class Outer", + " function make() returns Omega", + " return new C()", + " static class Base", + " function m(T t) returns T", + " return t", + " static class C extends Base implements Omega"), + compilationUnit("Main.wurst", + "package Main", + "import OmegaLib", + "import Lib", + "native testSuccess()", + "init", + " if new Outer().make().m(5) == 5 and new Outer().make().m(\"a\") == \"a\"", + " testSuccess()")); + String name = "staticClassOfAGenericClass"; + File jass = new File("test-output/DeterministicChecks_" + name + "_no_opts.j"); + File lua = new File("test-output/lua/DeterministicChecks_" + name + ".lua"); + testNamed(name).testLua(true).luaOnly(false).executeProg() + .compilationUnits(units.get(0), units.get(1), units.get(2)); + String firstJass = Files.toString(jass, Charsets.UTF_8); + String firstLua = Files.toString(lua, Charsets.UTF_8); + testNamed(name).testLua(true).luaOnly(false).executeProg() + .compilationUnits(units.get(1), units.get(0), units.get(2)); + assertEquals(Files.toString(jass, Charsets.UTF_8), firstJass, "Jass must not depend on the unit order"); + assertEquals(Files.toString(lua, Charsets.UTF_8), firstLua, "Lua must not depend on the unit order"); + } + /** Compiles {@code units} to Lua, runs them, and returns the script, which is written under {@code name}. */ private String compileToLua(String name, List units) throws IOException { testNamed(name).testLua(true).executeProg().compilationUnits(units.toArray(new CU[0])); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java index 1bf2b5440..4a0121505 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java @@ -2507,4 +2507,203 @@ public void genericOverrideOverriddenAgainBySubclassWithInterface() { " testSuccess()"); } + /* + * A static class inside a generic class sees the class's type parameter as a type variable of its own, which each + * use of the static class binds: its members are translated with it whatever code first asks for them, and a type + * of the static class carries an argument for it (the enclosing class's parameter in the enclosing class's members). + */ + + /** A static class's own method uses the type parameter of the generic class around it. */ + @Test + public void staticClassMethodUsesTheEnclosingClassTypeParameter() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function roundTrip(T t) returns T", + " let h = new Holder()", + " return h.id(t)", + " static class Holder", + " function id(T t) returns T", + " return t", + "init", + " if new Outer().roundTrip(5) == 5 and new Outer().roundTrip(\"a\") == \"a\"", + " testSuccess()"); + } + + /** A static class extends a sibling static class inside a generic class. */ + @Test + public void staticClassExtendsASiblingStaticClass() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function make() returns Base", + " return new C()", + " static class Base", + " function m() returns int", + " return 1", + " static class C extends Base", + "init", + " if new Outer().make().m() == 1", + " testSuccess()"); + } + + /** A field of a static class has the enclosing class's type parameter, and the enclosing class uses it first. */ + @Test + public void staticClassFieldHasTheEnclosingClassTypeParameter() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function roundTrip(T t) returns T", + " let h = new Holder()", + " h.v = t", + " return h.v", + " static class Holder", + " T v", + "init", + " if new Outer().roundTrip(5) == 5 and new Outer().roundTrip(\"a\") == \"a\"", + " testSuccess()"); + } + + /** The constructor and the ondestroy block of a static class use the enclosing class's type parameter. */ + @Test + public void staticClassConstructorAndOnDestroyUseTheEnclosingClassTypeParameter() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function roundTrip(T t) returns T", + " let h = new Holder(t)", + " let r = h.v", + " destroy h", + " return r", + " static class Holder", + " T v", + " construct(T t)", + " v = t", + " ondestroy", + " T x = v", + "init", + " if new Outer().roundTrip(5) == 5 and new Outer().roundTrip(\"a\") == \"a\"", + " testSuccess()"); + } + + /** A closure in a method of a static class takes and returns the enclosing class's type parameter. */ + @Test + public void staticClassClosureUsesTheEnclosingClassTypeParameter() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface F", + " function apply(X x) returns X", + "class Outer", + " function run(T t) returns T", + " return new Holder().make().apply(t)", + " static class Holder", + " function make() returns F", + " return (T x) -> x", + "init", + " if new Outer().run(5) == 5 and new Outer().run(\"a\") == \"a\"", + " testSuccess()"); + } + + /** A generic static class uses its own type parameter and the enclosing class's. */ + @Test + public void genericStaticClassUsesItsOwnAndTheEnclosingClassTypeParameter() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function pair(T t, int i) returns T", + " let b = new Box(i)", + " return b.first(t)", + " static class Box", + " A a", + " construct(A a)", + " this.a = a", + " function first(T t) returns T", + " return t", + "init", + " if new Outer().pair(5, 3) == 5 and new Outer().pair(\"a\", 3) == \"a\"", + " testSuccess()"); + } + + /** + * A static function of a static class uses the enclosing class's type parameter. It is not in its class, so it + * takes the parameter as a type variable of its own, as a static function of the generic class does. + */ + @Test + public void staticFunctionOfAStaticClassUsesTheEnclosingClassTypeParameter() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function roundTrip(T t) returns T", + " return Holder.id(new Holder().viaStatic(t))", + " static function viaStaticOuter(T t) returns T", + " return Holder.id(t)", + " static class Holder", + " static function id(T t) returns T", + " return t", + " function viaStatic(T t) returns T", + " return id(t)", + "init", + " if new Outer().roundTrip(5) == 5 and Outer.viaStaticOuter(\"a\") == \"a\"", + " testSuccess()"); + } + + /** A static class extends a generic sibling static class: the constructor passes both type arguments up. */ + @Test + public void staticClassExtendsAGenericSiblingStaticClass() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " function make(T t) returns Base", + " return new C(t)", + " static class Base", + " function m() returns int", + " return 1", + " static class C extends Base", + " T t", + " construct(T t)", + " this.t = t", + "init", + " if new Outer().make(3).m() == 1 and new Outer().make(\"a\").m() == 1", + " testSuccess()"); + } + + /** A static class overrides methods of a sibling static class, one of them over the type parameter. */ + @Test + public void staticClassOverridesASiblingStaticClassMethod() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Outer", + " T last", + " function run(T t, bool sub) returns int", + " Base b = new Base()", + " if sub", + " b = new C()", + " last = b.m(t)", + " return b.k()", + " static class Base", + " function m(T t) returns T", + " return t", + " function k() returns int", + " return 1", + " static class C extends Base", + " override function m(T t) returns T", + " return t", + " override function k() returns int", + " return 2", + "init", + " let o = new Outer()", + " let s = new Outer()", + " if o.run(5, true) == 2 and o.last == 5 and s.run(\"a\", false) == 1 and s.last == \"a\" and s.run(\"b\", true) == 2", + " testSuccess()"); + } + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterfaceTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterfaceTests.java index 97e400d30..1d32234a8 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterfaceTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterfaceTests.java @@ -915,13 +915,11 @@ public void aGenericClassImplementingAVarargInterfaceMethodWithAnInheritedMethod /** * Static classes inside a generic class have its type parameter as a variable of their own, which no superclass - * binds: C sees Base's as the parameter itself. Lua only: on Jass a static class extending another inside a - * generic class does not compile yet, with or without an interface (the generic elimination specialises Base - * without the parameter). + * binds: C sees Base's as its own captured variable. */ @Test public void aStaticClassOfAGenericClassImplementingTheInterfaceWithAnInheritedMethodIsDispatched() { - test().testLua(true).executeProg().lines( + test().testLua(true).luaOnly(false).executeProg().lines( "package test", "native testSuccess()", "interface Omega", @@ -943,6 +941,38 @@ public void aStaticClassOfAGenericClassImplementingTheInterfaceWithAnInheritedMe " testSuccess()"); } + /** + * As above, with the enclosing class's parameter in the signature of the inherited method: the function C gets + * for it takes and returns C's captured variable, which each instantiation of Outer binds. + */ + @Test + public void aStaticClassOfAGenericClassInheritingAnImplementationOverTheTypeParameterIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Omega", + " function m(A a) returns A", + "class Outer", + " function make() returns Omega", + " return new C()", + " static class Base", + " function m(T t) returns T", + " return t", + " static class C extends Base implements Omega", + "class X implements Omega", + " function m(int a) returns int", + " return a + 1", + "function viaOmega(Omega a, int v) returns int", + " return a.m(v)", + "function viaOmegaString(Omega a, string v) returns string", + " return a.m(v)", + "init", + " let fromInt = viaOmega(new Outer().make(), 5)", + " let fromString = viaOmegaString(new Outer().make(), \"a\")", + " if fromInt == 5 and fromString == \"a\" and viaOmega(new X(), 5) == 6", + " testSuccess()"); + } + /** * The function a generic class gets for an inherited implementation only calls it, so an optimised Lua build * inlines the call and a dispatch through the interface runs Base's body directly.