From a8a302d5530f71b25d23b98088bc2eb8fc3764d8 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 17:54:42 +0200 Subject: [PATCH 1/4] Old generics: bridge an interface implemented through a superclass InterfaceTranslator took the type arguments of the interface only from the class's own implements list. A subclass of a class implementing I, or a class implementing J with J extends I, got an empty binding, so its implementation was registered without the bridge which converts the argument from its index: Jass and the interpreter passed the index as the real, Lua bound the superclass's bridge. The binding is taken from the class's transitive super interfaces now. --- CHANGELOG.md | 8 +++ .../imtranslation/InterfaceTranslator.java | 8 +-- .../wurstscript/tests/GenericsTests.java | 60 +++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2892b3f05..34f83990d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## 1.9 (in progress) +- A call through an interface with an old-style type parameter whose type converts to an index (`I`) reaches + the implementation of a subclass, and of a class which implements an interface extending it, with the argument + converted back. Only a class which named the interface in its own `implements` got the bridge which converts it: + `class D extends B` with `B implements I` got the index as its real (Jass, interpreter), or B's implementation + (Lua), whether D overrides the function or not, and so did a class implementing `J` where `J extends I`. + The type arguments are taken from wherever the class implements the interface now. Castle fight and zombie defense + compile to the same scripts. + - On Lua, a compile-time expression can run the generic keyed-map intrinsics (`keyedMapPutNative`, `keyedMapGetNative`) again when nothing calls their Jass fallbacks at run time. The interpreter runs them through those fallbacks and finds them by name, but the tree shake in front of the compile-time run kept the intrinsic 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 4370c9163..67c4ab5e3 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 @@ -1,6 +1,5 @@ package de.peeeq.wurstscript.translation.imtranslation; -import com.google.common.collect.ImmutableCollection; import com.google.common.collect.Lists; import de.peeeq.wurstscript.ast.ClassDef; import de.peeeq.wurstscript.ast.FuncDef; @@ -89,18 +88,19 @@ private void translateInterfaceFuncDef(FuncDef f) { List subClasses = Lists.newArrayList(translator.getInterfaceInstances(interfaceDef)); - // TODO also add extended interfaces // set sub methods Map subClasses2 = translator.getClassesWithImplementation(subClasses, f); for (Entry subE : subClasses2.entrySet()) { ClassDef subC = subE.getKey(); WurstTypeClass subCT = subC.attrTypC(); - ImmutableCollection interfaces = subCT.implementedInterfaces(); + // The type arguments the class gives the interface, where it implements it: itself, through a superclass + // (the instances are the subclasses of an implementing class too) or through an interface which extends + // this one. The override converts its arguments from their index with them. VariableBinding typeBinding = VariableBinding.emptyMapping(); - for (WurstTypeInterface t : interfaces) { + for (WurstTypeInterface t : subCT.transitiveSuperInterfaces()) { if (t.getDef() == interfaceDef) { VariableBinding typeArgBinding = t.getTypeArgBinding(); typeBinding = typeArgBinding; diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java index 9fc21b2ef..86859d0a4 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java @@ -296,6 +296,66 @@ public void implicitConversions5() { // #490 ); } + /** {@code I} with the conversions of real, implemented by B and through B by its subclasses. */ + private static final String[] BRIDGED_INTERFACE = { + "package test", + "native testSuccess()", + "@extern native R2I(real r) returns int", + "function realToIndex(real r) returns int", + " return R2I(r * 10.)", + "function realFromIndex(int i) returns real", + " return i / 10.", + "interface I", + " function foo(T t) returns int", + "interface J extends I", + "class B implements I", + " override function foo(real t) returns int", + " return R2I(t * 100.)", + "function viaI(I i) returns int", + " return i.foo(1.5)"}; + + private static String[] bridged(String... rest) { + String[] result = java.util.Arrays.copyOf(BRIDGED_INTERFACE, BRIDGED_INTERFACE.length + rest.length); + System.arraycopy(rest, 0, result, BRIDGED_INTERFACE.length, rest.length); + return result; + } + + /** + * The override in a subclass of the class which implements the interface is called through the interface with + * the argument converted from its index, as the implementation of the superclass is. + */ + @Test + public void aSubclassOverrideIsCalledThroughTheInterfaceWithTheConvertedArgument() { + test().testLua(true).luaOnly(false).executeProg().lines(bridged( + "class D extends B", + " override function foo(real t) returns int", + " return R2I(t * 1000.)", + "init", + " if viaI(new B()) == 150 and viaI(new D()) == 1500", + " testSuccess()")); + } + + @Test + public void aSubclassWithoutAnOverrideIsCalledThroughTheInterfaceWithTheConvertedArgument() { + test().testLua(true).luaOnly(false).executeProg().lines(bridged( + "class D extends B", + "init", + " if viaI(new D()) == 150", + " testSuccess()")); + } + + /** The interface is implemented through one which extends it, with the type arguments of that one. */ + @Test + public void anImplementationOfAnExtendingInterfaceIsCalledWithTheConvertedArgument() { + test().testLua(true).luaOnly(false).executeProg().lines(bridged( + "class K implements J", + " override function foo(real t) returns int", + " return R2I(t * 1000.)", + "init", + " if viaI(new K()) == 1500", + " testSuccess()")); + } + @Test public void implicitConversionsFail() { testAssertErrorsLines(true, "Could not find function blaFromIndex", From b03ca7e33143f4efb247f3c1895ad511be13b38c Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 18:01:24 +0200 Subject: [PATCH 2/4] Drop the changelog entry --- CHANGELOG.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0226b73a..cd7781e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,5 @@ ## 1.9 (in progress) -- A call through an interface with an old-style type parameter whose type converts to an index (`I`) reaches - the implementation of a subclass, and of a class which implements an interface extending it, with the argument - converted back. Only a class which named the interface in its own `implements` got the bridge which converts it: - `class D extends B` with `B implements I` got the index as its real (Jass, interpreter), or B's implementation - (Lua), whether D overrides the function or not, and so did a class implementing `J` where `J extends I`. - The type arguments are taken from wherever the class implements the interface now. Castle fight and zombie defense - compile to the same scripts. - - The local optimisations no longer skip the condition of an if whose branches both start with a return or a loop exit. The branch merger moved the equal first statement in front of the if, so `if eff(b) > 0 ... return else return` became a plain `return` and `eff` was never called (Jass and Lua, `-localOptimizations`). A statement which From bbe4656d84125d6ae3299cfde72a5664788a415e Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 18:19:30 +0200 Subject: [PATCH 3/4] Bind an interface from its nearest declaration, the class's own first The lookup walked the superclass before the class's own interfaces, so a class which implements I itself could be bound by the I its superclass implements. It goes breadth first now, own interfaces before the superclass at each level, as the direct lookup preferred them before. --- .../imtranslation/InterfaceTranslator.java | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 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 67c4ab5e3..8e66563cc 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 @@ -8,9 +8,11 @@ import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.types.VariableBinding; import de.peeeq.wurstscript.types.WurstTypeClass; +import de.peeeq.wurstscript.types.WurstTypeClassOrInterface; import de.peeeq.wurstscript.types.WurstTypeInterface; import de.peeeq.wurstscript.types.WurstTypeNamedScope; +import java.util.ArrayDeque; import java.util.Collections; import java.util.List; import java.util.Map; @@ -95,18 +97,7 @@ private void translateInterfaceFuncDef(FuncDef f) { ClassDef subC = subE.getKey(); WurstTypeClass subCT = subC.attrTypC(); - // The type arguments the class gives the interface, where it implements it: itself, through a superclass - // (the instances are the subclasses of an implementing class too) or through an interface which extends - // this one. The override converts its arguments from their index with them. - VariableBinding typeBinding = - VariableBinding.emptyMapping(); - for (WurstTypeInterface t : subCT.transitiveSuperInterfaces()) { - if (t.getDef() == interfaceDef) { - VariableBinding typeArgBinding = t.getTypeArgBinding(); - typeBinding = typeArgBinding; - break; - } - } + VariableBinding typeBinding = typeBindingOf(subCT); FuncDef subM = subE.getValue(); ImMethod m = translator.getMethodFor(subM); @@ -117,4 +108,32 @@ private void translateInterfaceFuncDef(FuncDef f) { } + /** + * The type arguments a class gives this interface where it implements it: itself, through an interface which + * extends this one, or through a superclass (the instances are the subclasses of an implementing class too). The + * nearest declaration binds, and at the same distance the class's own interfaces before its superclass, so a + * class which implements {@code I} itself is not bound by the {@code I} of its superclass. The override + * converts its arguments from their index with them. + */ + private VariableBinding typeBindingOf(WurstTypeClass classType) { + ArrayDeque queue = new ArrayDeque<>(); + queue.add(classType); + while (!queue.isEmpty()) { + WurstTypeClassOrInterface type = queue.removeFirst(); + if (type instanceof WurstTypeInterface i && i.getDef() == interfaceDef) { + return i.getTypeArgBinding(); + } + if (type instanceof WurstTypeClass c) { + queue.addAll(c.implementedInterfaces()); + WurstTypeClass extended = c.extendedClass(); + if (extended != null) { + queue.add(extended); + } + } else { + queue.addAll(type.directSupertypes()); + } + } + return VariableBinding.emptyMapping(); + } + } From 34227a171dcb0a94f4d073cf0a2f5ba0aa45957b Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 18:24:44 +0200 Subject: [PATCH 4/4] Bridge test: check the emitted bridge and slot binding on Jass and Lua --- .../tests/wurstscript/tests/GenericsTests.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java index 86859d0a4..e142ccea1 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsTests.java @@ -325,7 +325,7 @@ private static String[] bridged(String... rest) { * the argument converted from its index, as the implementation of the superclass is. */ @Test - public void aSubclassOverrideIsCalledThroughTheInterfaceWithTheConvertedArgument() { + public void aSubclassOverrideIsCalledThroughTheInterfaceWithTheConvertedArgument() throws java.io.IOException { test().testLua(true).luaOnly(false).executeProg().lines(bridged( "class D extends B", " override function foo(real t) returns int", @@ -333,6 +333,22 @@ public void aSubclassOverrideIsCalledThroughTheInterfaceWithTheConvertedArgument "init", " if viaI(new B()) == 150 and viaI(new D()) == 1500", " testSuccess()")); + + String name = "GenericsTests_aSubclassOverrideIsCalledThroughTheInterfaceWithTheConvertedArgument"; + // Jass: D has its own bridge, which converts the index back, and the dispatch of I.foo calls it + String jass = java.nio.file.Files.readString(java.nio.file.Path.of("test-output", name + "_no_opts.j")); + org.testng.Assert.assertTrue(jass.contains("function D_foo_wrapper takes integer this, integer t returns integer"), jass); + org.testng.Assert.assertTrue(jass.contains("return D_foo(this, realFromIndex(t))"), jass); + org.testng.Assert.assertTrue(jass.contains("= D_foo_wrapper(this, t)"), "the dispatch calls D's bridge:\n" + jass); + // Lua: D binds the slot the call reads to its own bridge, not B's + String lua = java.nio.file.Files.readString(java.nio.file.Path.of("test-output", "lua", name + ".lua")); + java.util.regex.Matcher call = java.util.regex.Pattern.compile("__wurst_objectClass\\[\\w+\\]\\.(\\w+)\\(").matcher(lua); + org.testng.Assert.assertTrue(call.find(), lua); + String slot = call.group(1); + org.testng.Assert.assertTrue(lua.contains("D." + slot + " = D_foo_wrapper"), "D binds " + slot + " to its bridge:\n" + lua); + org.testng.Assert.assertTrue(lua.contains("B." + slot + " = B_foo_wrapper"), "B keeps its own:\n" + lua); + java.util.regex.Matcher bridge = java.util.regex.Pattern.compile("function D_foo_wrapper\\((\\w+), (\\w+)\\) \\s*return (\\w+)\\(\\1, realFromIndex\\(\\2\\)\\)").matcher(lua); + org.testng.Assert.assertTrue(bridge.find(), "D's bridge converts the argument back:\n" + lua); } @Test