From 82cbee136e181144abe35811e2312dd708a1f7f5 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 23:45:47 +0200 Subject: [PATCH] New generics: a static class sees the enclosing class's parameter as its own variable A static class inside a generic class (class Outer with static class Holder) has a type variable of its own for T, but did not use it consistently, so Jass failed to compile in several shapes which Lua accepted: - a member of the static class was translated with the type variables of whatever code first asked for it: a method Outer calls took and returned Outer's T while its body used the static class's ("Type variable T not eliminated"), and the same for fields, constructors and closures; - a type of the static class had no argument for that variable, so C extends Base gave C the superclass Base<> ("Rewrite generics with wrong sizes"); - a static function of the static class, which is not in its class, used the class's variable, which it does not have. The members of a class are now translated with the class's type variables whoever asks for them first, and the type of this is the class with its own variables. A type of a static class carries an argument for its variable: what the type binds it to (new Outer().make() returns a Base with T bound to int), or else the parameter as the code sees it, Outer's own in Outer and the captured one in the static class. Super constructor and ondestroy calls pass it, and a static function of a static class takes it as a type variable of its own, as a static function of the generic class does. EliminateGenerics no longer fills in missing type arguments by name. The function a class gets for an inherited interface implementation translates the superclass's type arguments the same way, with the class's variables, so a static class implementing an interface with an inherited method over T is dispatched on Jass too. --- .../imtranslation/ClassTranslator.java | 5 +- .../imtranslation/EliminateGenerics.java | 72 ------- .../imtranslation/ImTranslator.java | 113 +++++++--- .../imtranslation/InterfaceTranslator.java | 35 +-- .../types/WurstTypeClassOrInterface.java | 1 + .../tests/DeterministicChecks.java | 44 ++++ .../tests/GenericsWithTypeclassesTests.java | 199 ++++++++++++++++++ .../wurstscript/tests/InterfaceTests.java | 38 +++- 8 files changed, 383 insertions(+), 124 deletions(-) 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.