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 10796d301..f183752e6 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 @@ -423,6 +423,7 @@ public ImProg translateProg() { translateCompilationUnit(cu); } linkBridgedOverrides(); + addBridgeFunctions(); if (mainFunc == null) { mainFunc = ImFunction(emptyTrace, "main", ImTypeVars(), ImVars(), ImVoid(), ImVars(), ImStmts(), flags()); @@ -483,6 +484,36 @@ private void linkBridgedOverrides() { } } + /** The functions {@link #bridgeFunction} made, by class and by the implementation each one runs. */ + private final Map> bridgeFunctions = new IdentityHashMap<>(); + + /** + * The function {@code c} has of its own to run {@code implementation}, an implementation it inherits + * ({@code InterfaceTranslator}), made by {@code make} the first time: a class implementing several interfaces with + * one inherited method gets one function for it. + */ + public ImFunction bridgeFunction(ImClass c, FuncDef implementation, java.util.function.Supplier make) { + return bridgeFunctions.computeIfAbsent(c, k -> new IdentityHashMap<>()) + .computeIfAbsent(implementation, k -> make.get()); + } + + /** + * Adds the functions of {@link #bridgeFunction} to their classes once every unit is translated. They are made as + * the interfaces are translated, in the order of the compilation units, and two of them can have one name (the + * overloads of a method), which the backends tell apart by their order. So they go in by their sort key, which the + * order of the units does not change. + */ + private void addBridgeFunctions() { + for (ImClass c : imProg.getClasses()) { + Map functions = bridgeFunctions.get(c); + if (functions != null) { + List sorted = new ArrayList<>(functions.values()); + sortList(sorted); + c.getFunctions().addAll(sorted); + } + } + } + public void removeEmptyPackageInits() { Set emptyInitFunctions = new HashSet<>(); for (ImFunction initFunc : new LinkedHashSet<>(initFuncMap.values())) { 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 501c7a288..da24b6d73 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 @@ -2,10 +2,14 @@ import com.google.common.collect.Lists; import de.peeeq.wurstscript.ast.ClassDef; +import de.peeeq.wurstscript.ast.ClassOrInterface; import de.peeeq.wurstscript.ast.FuncDef; import de.peeeq.wurstscript.ast.InterfaceDef; import de.peeeq.wurstscript.ast.TypeExpr; +import de.peeeq.wurstscript.ast.TypeParamDef; +import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.jassIm.*; +import de.peeeq.wurstscript.translation.imtojass.ImAttrType; import de.peeeq.wurstscript.types.VariableBinding; import de.peeeq.wurstscript.types.WurstTypeClass; import de.peeeq.wurstscript.types.WurstTypeClassOrInterface; @@ -103,11 +107,9 @@ private void translateInterfaceFuncDef(FuncDef f) { ImMethod m = translator.getMethodFor(subM); ImClass mClass = translator.getClassFor(subC); - if (f.attrHasEmptyBody() && !subClasses.contains(subM.attrNearestClassDef()) - && mClass.getTypeVariables().isEmpty() && m.attrClass().getTypeVariables().isEmpty()) { + if (f.attrHasEmptyBody() && !subClasses.contains(subM.attrNearestClassDef())) { FuncDef interfaceDefault = defaultOf(subCT, f); - m = methodOfItsOwn(mClass, m, interfaceDefault == null - ? m.getImplementation() : translator.getFuncFor(interfaceDefault)); + m = methodOfItsOwn(mClass, subCT, m, interfaceDefault == null ? subM : interfaceDefault); } OverrideUtils.addOverride(translator, f, mClass, m, subM, typeBinding); } @@ -122,17 +124,101 @@ private void translateInterfaceFuncDef(FuncDef f) { * own with Base's implementation, which its subclasses inherit. The overrides below C are its sub-methods, as they * are Base's ({@link ImTranslator#linkOverridesBelow}). Where another interface of C gives m a default, the * default is the implementation: a default beats an inherited method (a call through that interface runs it on - * every backend), and Lua binds one implementation for C to both. Not for generic classes: a method of a generic - * class is specialised with the functions the class owns, so those keep what they did. + * every backend), and Lua binds one implementation for C to both. + *

+ * Where neither C nor the class of the implementation is generic, the method runs that implementation's function. + * Otherwise it gets a function of C which calls it ({@link #implementationOfItsOwn}). */ - private ImMethod methodOfItsOwn(ImClass imClass, ImMethod inherited, ImFunction implementation) { - ImMethod own = JassIm.ImMethod(inherited.getTrace(), translator.selfType(imClass), inherited.getName(), - implementation, Lists.newArrayList(), new java.util.ArrayList<>(), "", false); + private ImMethod methodOfItsOwn(ImClass imClass, WurstTypeClass classType, ImMethod inherited, + FuncDef implementation) { + ImFunction function = translator.getFuncFor(implementation); + ClassOrInterface owner = implementation.attrNearestClassOrInterface(); + String name = inherited.getName(); + if (!imClass.getTypeVariables().isEmpty() + || (owner != null && !translator.getClassFor(owner).getTypeVariables().isEmpty())) { + ImFunction inheritedFunction = function; + function = translator.bridgeFunction(imClass, implementation, + () -> implementationOfItsOwn(imClass, classType, implementation, inheritedFunction, owner)); + name = function.getName(); + } + ImMethod own = JassIm.ImMethod(inherited.getTrace(), translator.selfType(imClass), name, + function, Lists.newArrayList(), new java.util.ArrayList<>(), "", false); imClass.getMethods().add(own); translator.linkOverridesBelow(own, inherited); return own; } + /** + * A function of {@code imClass} which runs the inherited implementation, for a class where one of the two is + * generic. A method is specialised with the functions of its class, but the inherited function belongs to another + * class and must be specialised for that class's type arguments as {@code imClass} sees them, which need not be + * its own ({@code C extends Base}). So the function calls the inherited one with this, as + * {@code super.m()} does, and the specialisation finds Base's type arguments from the type of this. It takes the + * inherited function's parameters with Base's type variables replaced by those arguments, and is vararg where that + * function is. The translator adds it to the class ({@link ImTranslator#bridgeFunction}). + */ + private ImFunction implementationOfItsOwn(ImClass imClass, WurstTypeClass classType, FuncDef implementation, + ImFunction inherited, @org.eclipse.jdt.annotation.Nullable ClassOrInterface owner) { + List ownerVariables = owner == null + ? Collections.emptyList() : translator.getClassFor(owner).getTypeVariables(); + List ownerArguments = owner == null + ? Collections.emptyList() : typeArgumentsAsSeenFrom(classType, owner, ownerVariables); + ImVar thisVar = JassIm.ImVar(implementation, translator.selfType(imClass), "this", false); + ImVars parameters = JassIm.ImVars(thisVar); + ImExprs arguments = JassIm.ImExprs(JassIm.ImVarAccess(thisVar)); + for (ImVar p : inherited.getParameters().subList(1, inherited.getParameters().size())) { + ImVar parameter = JassIm.ImVar(p.getTrace(), + ImAttrType.substituteType(p.getType(), ownerArguments, ownerVariables), p.getName(), false); + parameters.add(parameter); + arguments.add(JassIm.ImVarAccess(parameter)); + } + ImType returnType = ImAttrType.substituteType(inherited.getReturnType(), ownerArguments, ownerVariables); + ImExpr call = JassIm.ImFunctionCall(implementation, inherited, JassIm.ImTypeArguments(), arguments, false, + CallType.NORMAL); + ImStmts body = returnType instanceof ImVoid + ? JassIm.ImStmts(call) + : JassIm.ImStmts(JassIm.ImReturn(implementation, call)); + List flags = new java.util.ArrayList<>(); + if (inherited.hasFlag(FunctionFlagEnum.IS_VARARG)) { + flags.add(FunctionFlagEnum.IS_VARARG); + } + return JassIm.ImFunction(implementation, imClass.getName() + "_" + implementation.getName(), + JassIm.ImTypeVars(), parameters, returnType, JassIm.ImVars(), body, flags); + } + + /** + * The type arguments for {@code variables}, the type variables of {@code owner}, a class or interface above + * {@code classType}, as that class sees them. + */ + 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(); + 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())); + } + return arguments; + } + /** * The default which another interface of the class gives {@code abstractMethod}: a method of an interface which 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 261bfa17c..355a4294c 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 @@ -621,6 +621,63 @@ private static int countOccurrences(String text, String part) { return n; } + /** + * A generic class implements two interfaces, each with one of two overloads it inherits, so it gets a function of + * its own for each, both named after the method. The interfaces are in packages of their own: the Jass and the + * Lua must not depend on which of them is translated first. Base comes first either way, so its own functions are + * made in its order: a class's functions are made when they are first asked for, which an interface translated + * before the class does in the order of the units. C comes after both interfaces. + */ + @Test + public void bridgesOfAGenericClassAreTheSameInAnyUnitOrder() throws IOException { + List units = List.of( + compilationUnit("IntM.wurst", + "package IntM", + "public interface IntM", + " function m(int x) returns int"), + compilationUnit("StrM.wurst", + "package StrM", + "public interface StrM", + " function m(string s) returns int"), + compilationUnit("BaseLib.wurst", + "package BaseLib", + "public class Base", + " function m(int x) returns int", + " return x", + " function m(string s) returns int", + " return 7"), + compilationUnit("Lib.wurst", + "package Lib", + "import BaseLib", + "import IntM", + "import StrM", + "public class C extends Base implements IntM, StrM"), + compilationUnit("Main.wurst", + "package Main", + "import IntM", + "import StrM", + "import Lib", + "native testSuccess()", + "function viaInt(IntM i) returns int", + " return i.m(5)", + "function viaStr(StrM s) returns int", + " return s.m(\"a\")", + "init", + " if viaInt(new C()) == 5 and viaStr(new C()) == 7", + " testSuccess()")); + String name = "bridgesOfAGenericClass"; + 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(2), units.get(0), units.get(1), units.get(3), units.get(4)); + 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(2), units.get(1), units.get(0), units.get(3), units.get(4)); + 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/InterfaceTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterfaceTests.java index cc6064a53..97e400d30 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 @@ -728,4 +728,279 @@ public void aDefaultBeatsTheInheritedMethodWhereAnotherInterfaceHasNoBody() { " if viaDefault(new C()) == 2 and viaDefault(new D()) == 4 and viaAbstract(new C()) == 2 and viaAbstract(new D()) == 4", " testSuccess()"); } + + /** As {@link #anOverrideBelowTheClassWhichInheritsTheImplementationIsDispatched}, with C and D generic. */ + @Test + public void aGenericClassImplementingTheInterfaceWithAnInheritedMethodIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Omega", + " function m() returns int", + "class Base", + " function m() returns int", + " return 1", + "class C extends Base implements Omega", + "class D extends C", + " override function m() returns int", + " return 4", + "class E extends C", + "class X implements Omega", + " function m() returns int", + " return 3", + "function viaOmega(Omega a) returns int", + " return a.m()", + "function viaBase(Base b) returns int", + " return b.m()", + "init", + " if viaOmega(new C()) == 1 and viaOmega(new D()) == 4 and viaOmega(new E()) == 1", + " and viaOmega(new X()) == 3 and viaBase(new C()) == 1 and viaBase(new D()) == 4", + " testSuccess()"); + } + + /** A plain class overrides the method below the generic class which inherits it. */ + @Test + public void aPlainOverrideBelowAGenericClassImplementingTheInterfaceWithAnInheritedMethodIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Omega", + " function m() returns int", + "class Base", + " function m() returns int", + " return 1", + "class C extends Base implements Omega", + "class D extends C", + " override function m() returns int", + " return 4", + "class E extends D", + "function viaOmega(Omega a) returns int", + " return a.m()", + "function viaBase(Base b) returns int", + " return b.m()", + "init", + " if viaOmega(new C()) == 1 and viaOmega(new D()) == 4 and viaOmega(new E()) == 4", + " and viaBase(new C()) == 1 and viaBase(new D()) == 4", + " testSuccess()"); + } + + /** The method comes from a generic class, which a plain class implementing the interface extends. */ + @Test + public void aClassImplementingTheInterfaceWithAMethodOfAGenericSuperclassIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Omega", + " function m() returns int", + "class Base", + " function m() returns int", + " return 1", + "class C extends Base implements Omega", + "class D extends C", + " override function m() returns int", + " return 4", + "class X implements Omega", + " function m() returns int", + " return 3", + "function viaOmega(Omega a) returns int", + " return a.m()", + "init", + " if viaOmega(new C()) == 1 and viaOmega(new D()) == 4 and viaOmega(new X()) == 3 and new Base().m() == 1", + " testSuccess()"); + } + + /** + * Both classes and the interface are generic, and the inherited method reads a field of its class's type + * parameter, so the implementation C gets must be Base's specialised for C's type argument. + */ + @Test + public void aGenericInterfaceImplementedWithAMethodOfAGenericSuperclassIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Omega", + " function get() returns T", + "class Base", + " T v", + " construct(T v)", + " this.v = v", + " function get() returns T", + " return v", + "class C extends Base implements Omega", + " construct(T v)", + " super(v)", + "class X implements Omega", + " function get() returns int", + " return 3", + "function viaOmega(Omega a) returns int", + " return a.get()", + "function viaOmegaS(Omega a) returns string", + " return a.get()", + "init", + " if viaOmega(new C(5)) == 5 and viaOmega(new X()) == 3 and viaOmegaS(new C(\"s\")) == \"s\"", + " testSuccess()"); + } + + /** + * C's type parameters are not Base's: C instantiates Base with a type of its own choosing and with the second of + * its two parameters, and the method takes a parameter of Base's type parameter. C's method must run Base's for + * Base's instantiation, not C's. + */ + @Test + public void aGenericClassImplementingTheInterfaceWithAMethodOfADifferentlyInstantiatedSuperclassIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Named", + " function name(string prefix) returns string", + "interface Swap", + " function swap(T t) returns T", + "class Base", + " T v", + " construct(T v)", + " this.v = v", + " function name(string prefix) returns string", + " return prefix + \"base\"", + " function swap(T t) returns T", + " let old = v", + " v = t", + " return old", + "class Fixed extends Base implements Named", + " T other", + " construct(T other)", + " super(\"fixed\")", + " this.other = other", + "class Second extends Base implements Swap", + " construct(B b)", + " super(b)", + "class X implements Named", + " function name(string prefix) returns string", + " return prefix + \"x\"", + "function viaNamed(Named n) returns string", + " return n.name(\"a-\")", + "function viaSwap(Swap s, string t) returns string", + " return s.swap(t)", + "init", + " let s = new Second(\"one\")", + " if viaNamed(new Fixed(7)) == \"a-base\" and viaNamed(new X()) == \"a-x\"", + " and viaSwap(s, \"two\") == \"one\" and viaSwap(s, \"three\") == \"two\"", + " testSuccess()"); + } + + /** The inherited implementation takes a vararg, which the function the generic class gets must take too. */ + @Test + public void aGenericClassImplementingAVarargInterfaceMethodWithAnInheritedMethodIsDispatched() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Summer", + " function sum(vararg int xs) returns int", + "class Base", + " function sum(vararg int xs) returns int", + " var r = 0", + " for x in xs", + " r += x", + " return r", + "class C extends Base implements Summer", + "class D extends Base implements Summer", + "class X implements Summer", + " function sum(vararg int xs) returns int", + " return -1", + "function viaSummer(Summer s) returns int", + " return s.sum(1, 2, 3)", + "init", + " if viaSummer(new C()) == 6 and viaSummer(new D()) == 6 and viaSummer(new X()) == -1", + " testSuccess()"); + } + + /** + * 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). + */ + @Test + public void aStaticClassOfAGenericClassImplementingTheInterfaceWithAnInheritedMethodIsDispatched() { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface Omega", + " function m() returns int", + "class Outer", + " function make() returns Omega", + " return new C()", + " static class Base", + " function m() returns int", + " return 1", + " static class C extends Base implements Omega", + "class X implements Omega", + " function m() returns int", + " return 3", + "function viaOmega(Omega a) returns int", + " return a.m()", + "init", + " if viaOmega(new Outer().make()) == 1 and viaOmega(new X()) == 3", + " 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. + */ + @Test + public void theFunctionAGenericClassGetsForAnInheritedMethodIsInlined() throws java.io.IOException { + test().testLua(true).inline().localOptimizations().executeProg().lines( + "package test", + "native testSuccess()", + "interface Named", + " function name(string prefix) returns string", + "class Base", + " function name(string prefix) returns string", + " return prefix + \"base\"", + "class Fixed extends Base implements Named", + "class X implements Named", + " function name(string prefix) returns string", + " return prefix + \"x\"", + "function viaNamed(Named n) returns string", + " return n.name(\"a-\")", + "init", + " if viaNamed(new Fixed()) == \"a-base\" and viaNamed(new X()) == \"a-x\"", + " testSuccess()"); + String lua = com.google.common.io.Files.asCharSource(new java.io.File( + "test-output/lua/InterfaceTests_theFunctionAGenericClassGetsForAnInheritedMethodIsInlined.lua"), + java.nio.charset.StandardCharsets.UTF_8).read(); + int start = lua.indexOf("function Fixed_Fixed_name("); + org.testng.Assert.assertTrue(start >= 0, lua); + String body = lua.substring(start, lua.indexOf("\nend", start)); + org.testng.Assert.assertTrue(body.contains("\"base\""), body); + org.testng.Assert.assertFalse(body.contains("Base_"), body); + } + + /** As {@link #aDefaultBeatsTheInheritedMethodWhereAnotherInterfaceHasNoBody}, with C and D generic. */ + @Test + public void aDefaultBeatsTheInheritedMethodOfAGenericClassWhereAnotherInterfaceHasNoBody() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface Abstract", + " function m() returns int", + "interface Default", + " function m() returns int", + " return 2", + "class Base", + " function m() returns int", + " return 1", + "class C extends Base implements Abstract, Default", + "class D extends C", + " override function m() returns int", + " return 4", + "function viaDefault(Default a) returns int", + " return a.m()", + "function viaAbstract(Abstract a) returns int", + " return a.m()", + "init", + " if viaDefault(new C()) == 2 and viaDefault(new D()) == 4", + " and viaAbstract(new C()) == 2 and viaAbstract(new D()) == 4", + " testSuccess()"); + } }