From 4fc2cf3413639854113d3d012e2cfa7fdb32aee5 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 22:19:51 +0200 Subject: [PATCH 1/2] Interface bridge: give generic classes a method of their own too A class which implements an interface method with a method it inherits from a class outside the interface (C extends Base implements Omega) gets a method of its own since #1421, because the dispatch over the interface's implementations never visits Base. Generic classes were left out, so a call through the interface reached the wrong implementation for them, on every target and already in the interpreter: C extends Base, C extends Base, C extends Base implements Omega, and the same with an override below C or a default from another interface. The bridge shared Base's function. A method is specialised with the functions of its own class, and Base's function belongs to Base and needs Base's type arguments as C sees them, which need not be C's (C extends Base). So where C or Base is generic the bridge gets a function of C which calls Base's with this, as super.m() does; the specialisation then finds Base's type arguments from the type of this. Its parameters are Base's function's with Base's type variables replaced by those arguments. Bridges between plain classes keep sharing the function. With -inline the forwarding call is inlined away (Lua output test). --- .../imtranslation/InterfaceTranslator.java | 97 +++++++- .../wurstscript/tests/InterfaceTests.java | 219 ++++++++++++++++++ 2 files changed, 307 insertions(+), 9 deletions(-) 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..2442e8f54 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,94 @@ 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())) { + function = implementationOfItsOwn(imClass, classType, implementation, function, 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. + */ + 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)); + ImFunction own = JassIm.ImFunction(implementation, imClass.getName() + "_" + implementation.getName(), + JassIm.ImTypeVars(), parameters, returnType, JassIm.ImVars(), body, Collections.emptyList()); + imClass.getFunctions().add(own); + return own; + } + + /** + * 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); + ImType type = parameter == null ? null : binding.get(parameter) + .map(bound -> bound.imTranslateType(translator)).getOrNull(); + if (type == null) { + throw new CompileError(classType.getDef(), "Could not find the type argument of " + owner.getName() + + " for " + variable.getName() + " as " + classType.getDef().getName() + " sees it."); + } + 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/InterfaceTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterfaceTests.java index cc6064a53..bb21aa00e 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,223 @@ 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 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()"); + } } From b93d74cfe9cc21e09f1b42cd050fd92c36f3b1af Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 22:46:08 +0200 Subject: [PATCH 2/2] Interface bridge: vararg, captured type parameters, unit order The function a generic class gets for an inherited implementation now: - is vararg where the inherited function is, so a call with several values reaches it whole (the interpreter rejected the arity, Lua dropped values); - maps a type parameter no supertype binds, one of an enclosing generic class which a static class inside it captures, to that parameter as the class's own functions see it, instead of failing to compile; - is made once per class and implementation, and added to its class after every unit is translated, by its sort key: two of them can share a name (the overloads of a method), and the order they were made in followed the order of the compilation units. --- .../imtranslation/ImTranslator.java | 31 ++++++++++ .../imtranslation/InterfaceTranslator.java | 25 +++++--- .../tests/DeterministicChecks.java | 57 +++++++++++++++++++ .../wurstscript/tests/InterfaceTests.java | 56 ++++++++++++++++++ 4 files changed, 160 insertions(+), 9 deletions(-) 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 2442e8f54..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 @@ -136,7 +136,9 @@ private ImMethod methodOfItsOwn(ImClass imClass, WurstTypeClass classType, ImMet String name = inherited.getName(); if (!imClass.getTypeVariables().isEmpty() || (owner != null && !translator.getClassFor(owner).getTypeVariables().isEmpty())) { - function = implementationOfItsOwn(imClass, classType, implementation, function, owner); + 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, @@ -152,7 +154,8 @@ private ImMethod methodOfItsOwn(ImClass imClass, WurstTypeClass classType, ImMet * 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. + * 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) { @@ -175,10 +178,12 @@ private ImFunction implementationOfItsOwn(ImClass imClass, WurstTypeClass classT ImStmts body = returnType instanceof ImVoid ? JassIm.ImStmts(call) : JassIm.ImStmts(JassIm.ImReturn(implementation, call)); - ImFunction own = JassIm.ImFunction(implementation, imClass.getName() + "_" + implementation.getName(), - JassIm.ImTypeVars(), parameters, returnType, JassIm.ImVars(), body, Collections.emptyList()); - imClass.getFunctions().add(own); - return own; + 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); } /** @@ -201,12 +206,14 @@ private List typeArgumentsAsSeenFrom(WurstTypeClass classType, C List arguments = new java.util.ArrayList<>(); for (ImTypeVar variable : variables) { TypeParamDef parameter = translator.getTypeParamDef(variable); - ImType type = parameter == null ? null : binding.get(parameter) - .map(bound -> bound.imTranslateType(translator)).getOrNull(); - if (type == null) { + 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; 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 bb21aa00e..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 @@ -887,6 +887,62 @@ public void aGenericClassImplementingTheInterfaceWithAMethodOfADifferentlyInstan " 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.