From af5f4a1cc1fb2667586f28eb2c8323f1c8569827 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 21:22:56 +0200 Subject: [PATCH] New generics: specialise a subclass's superclasses without rewriting the list in place A method of a generic class overridden again in a plain subclass and called through a non-generic base (class C extends Base, class D extends C, Base.m() called) crashed the compiler with a ConcurrentModificationException on every target. adaptSubmethods rewrote D's superclass list in place with replaceAll(specializeType). Specialising C runs the trigger registered on C, which specialises C.m and adapts its submethods, D.m among them, and so rewrites D's list again inside the outer replaceAll. The specialisation of a generic class's own superclasses had the same shape, and rewriteRuntimeTypeSuperEdges rewrites every class's list in place too. One helper now specialises a class's superclasses into a new list from a copy and replaces the old one; adaptSubmethods, specializeClass and the use queued for each non-generic class all call it. The InterfaceTranslator note which named this crash as a reason to skip generic classes is updated. --- .../imtranslation/EliminateGenerics.java | 30 ++++++++---- .../imtranslation/InterfaceTranslator.java | 3 +- .../tests/GenericsWithTypeclassesTests.java | 47 +++++++++++++++++++ 3 files changed, 68 insertions(+), 12 deletions(-) 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 04c40255e..a018d2b5f 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 @@ -1971,7 +1971,7 @@ private void adaptSubmethods(List oldSubMethods, ImMethod newM, Generi } }); }else { - subClass.getSuperClasses().replaceAll(this::specializeType); + specializeSuperClasses(subClass); ImClassType newClassTspecialized = specializeType(newClassT); if (subClass.isSubclassOf(newClassTspecialized.getClassDef())) { newM.getSubMethods().add(subMethod); @@ -1980,6 +1980,23 @@ private void adaptSubmethods(List oldSubMethods, ImMethod newM, Generi } } + /** + * Replaces the superclasses of a class with their specialisations. + *

+ * Specialising a superclass can specialise its class. That runs the triggers registered on that + * class, which specialise its methods and adapt their submethods, and so come back here, for this + * class too; and {@link #rewriteRuntimeTypeSuperEdges} rewrites the superclasses of every class. + * The list can therefore change while this runs: it is read from a copy and replaced, never + * rewritten in place. + */ + private void specializeSuperClasses(ImClass c) { + List specialized = new ArrayList<>(); + for (ImClassType superClass : new ArrayList<>(c.getSuperClasses())) { + specialized.add(specializeType(superClass)); + } + c.setSuperClasses(specialized); + } + /** * Replaces all uses of the given typeVars with the type arguments given in parameter generics. */ @@ -2238,7 +2255,7 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { : c.getName() + "⟪" + generics.makeName() + "⟫"); List typeVars = c.getTypeVariables(); rewriteGenerics(newC, generics, typeVars); - newC.getSuperClasses().replaceAll(this::specializeType); + specializeSuperClasses(newC); if (needsRuntimeTypeSpecialization(c, generics, new HashSet<>())) { rewriteRuntimeTypeSuperEdges(c, generics, newC); } @@ -2632,14 +2649,7 @@ public void visit(ImClass c) { // handle generic classes after they are specialized return; } - genericsUses.add(() -> { - List newSuperClasses = new ArrayList<>(); - for (ImClassType imClassType : c.getSuperClasses()) { - ImClassType specializeType = EliminateGenerics.this.specializeType(imClassType); - newSuperClasses.add(specializeType); - } - c.setSuperClasses(newSuperClasses); - }); + genericsUses.add(() -> specializeSuperClasses(c)); super.visit(c); } 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 bcf4567d7..501c7a288 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 @@ -123,8 +123,7 @@ private void translateInterfaceFuncDef(FuncDef f) { * 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, and a generic class with an override in a non-generic - * subclass does not compile yet (EliminateGenerics.adaptSubmethods), so those keep what they did. + * class is specialised with the functions the class owns, so those keep what they did. */ private ImMethod methodOfItsOwn(ImClass imClass, ImMethod inherited, ImFunction implementation) { ImMethod own = JassIm.ImMethod(inherited.getTrace(), translator.selfType(imClass), inherited.getName(), 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 c1b21c004..1bf2b5440 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 @@ -2460,4 +2460,51 @@ public void genericStaticInit() throws IOException { assertTrue(jass.contains("set Box_cap_Box_real__u = 16")); } + @Test + public void genericOverrideOverriddenAgainByPlainSubclass() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Base", + " function m() returns int", + " return 1", + "class C extends Base", + " override function m() returns int", + " return 2", + "class D extends C", + " override function m() returns int", + " return 4", + "function viaBase(Base b) returns int", + " return b.m()", + "init", + " if viaBase(new Base()) == 1 and viaBase(new C()) == 2 and viaBase(new D()) == 4", + " testSuccess()"); + } + + @Test + public void genericOverrideOverriddenAgainBySubclassWithInterface() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "interface I", + " function n() returns int", + "class Base", + " function m() returns int", + " return 1", + "class C extends Base", + " override function m() returns int", + " return 2", + "class D extends C implements I", + " override function m() returns int", + " return 4", + " function n() returns int", + " return 8", + "function viaBase(Base b) returns int", + " return b.m()", + "init", + " let d = new D()", + " if viaBase(new Base()) == 1 and viaBase(new C()) == 2 and viaBase(d) == 4 and d.n() == 8", + " testSuccess()"); + } + }