From 073f6ca28814331d7f67650153a88cf037d64c4f Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 18:53:11 +0200 Subject: [PATCH] Handle detached generic static initializers --- .../imtranslation/EliminateGenerics.java | 41 +++++++++----- .../lua/translation/RemoveGarbage.java | 3 + .../tests/LuaBackendAuditTests.java | 55 +++++++++++++++++++ 3 files changed, 86 insertions(+), 13 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 0b04c2f54..e32390131 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 @@ -1278,7 +1278,9 @@ private void removeNonSpecializedGlobals() { List inits = prog.getGlobalInits().remove(imVar); if (inits != null) { for (ImSet init : inits) { - init.replaceBy(ImHelper.nullExpr()); + if (init.getParent() != null) { + init.replaceBy(ImHelper.nullExpr()); + } } } } @@ -2243,18 +2245,29 @@ private void createSpecializedGlobals(ImClass originalClass, GenericTypes generi List originalInits = prog.getGlobalInits().get(originalGlobal); if (originalInits != null && !originalInits.isEmpty()) { - ImSet firstOrig = originalInits.getFirst(); - if (!(firstOrig.getParent() instanceof ImStmts parentStmts)) { - throw new CompileError(originalGlobal, - "Initializer for global " + originalGlobal.getName() + " is not inside ImStmts."); - } - // ensure all original init sets share the same parent statement list + ImStmts parentStmts = null; + boolean hasDetachedInits = false; for (ImSet s : originalInits) { - if (s.getParent() != parentStmts) { + if (s.getParent() == null) { + hasDetachedInits = true; + continue; + } + if (!(s.getParent() instanceof ImStmts)) { + throw new CompileError(originalGlobal, + "Initializer for global " + originalGlobal.getName() + " is not inside ImStmts."); + } + ImStmts currParent = (ImStmts) s.getParent(); + if (parentStmts == null) { + parentStmts = currParent; + } else if (parentStmts != currParent) { throw new CompileError(originalGlobal, "Initializer statements for global " + originalGlobal.getName() + " are not in the same ImStmts."); } } + if (hasDetachedInits && parentStmts != null) { + throw new CompileError(originalGlobal, + "Initializer statements for global " + originalGlobal.getName() + " are inconsistently attached."); + } // Helper: rebuild LHS as ImLExpr for specialized global java.util.function.Function specializeLhs = (ImLExpr lhs) -> { @@ -2292,11 +2305,13 @@ private void createSpecializedGlobals(ImClass originalClass, GenericTypes generi // Append after earlier specializations of this initializer. Each invocation of // createSpecializedGlobals has its own insertion batch; always inserting after // origSet would therefore reverse specialization discovery/initializer order. - ImStmt insertionPoint = specializedInitializerTails.getOrDefault(origSet, origSet); - IdentityHashMap> byStmt = - insertsByParent.computeIfAbsent(parentStmts, k -> new IdentityHashMap<>()); - byStmt.computeIfAbsent(insertionPoint, k -> new ArrayList<>(1)).add(specSet); - specializedInitializerTails.put(origSet, specSet); + if (parentStmts != null) { + ImStmt insertionPoint = specializedInitializerTails.getOrDefault(origSet, origSet); + IdentityHashMap> byStmt = + insertsByParent.computeIfAbsent(parentStmts, k -> new IdentityHashMap<>()); + byStmt.computeIfAbsent(insertionPoint, k -> new ArrayList<>(1)).add(specSet); + specializedInitializerTails.put(origSet, specSet); + } // keep prog.getGlobalInits consistent, but do NOT reuse the tree-attached node elsewhere specializedInitsForMap.add((ImSet) specSet.copy()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index f8b4823cb..e79acb1f1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -205,6 +205,9 @@ public static void removePhantomGenericStaticInitializers(ImProg prog, ImTransla } prog.getGlobalInits().remove(candidate.getKey()); for (ImSet initializer : candidate.getValue()) { + if (initializer.getParent() == null) { + continue; + } if (!(initializer.getParent() instanceof ImStmts statements)) { throw new IllegalStateException("Global initializer is not attached to an ImStmts node."); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index 18eedcfca..28fbe5cdb 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -6,7 +6,12 @@ import de.peeeq.wurstscript.RunArgs; import de.peeeq.wurstscript.ast.WurstModel; import de.peeeq.wurstscript.gui.WurstGuiCliImpl; +import de.peeeq.wurstscript.jassIm.ImProg; +import de.peeeq.wurstscript.jassIm.ImSet; +import de.peeeq.wurstscript.jassIm.ImVar; +import de.peeeq.wurstscript.jassIm.JassIm; import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; +import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import org.wurstscript.projectconfig.WurstProjectConfigData; import org.testng.annotations.Test; @@ -1010,6 +1015,56 @@ public void genericStaticsAreIndependentWithoutTupleInstantiation() throws IOExc 2, storages); } + @Test + public void detachedGenericStaticInitializerRemainsMetadataOnly() { + RunArgs runArgs = new RunArgs().with("-lua"); + WurstGuiCliImpl gui = new WurstGuiCliImpl(); + WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null, runArgs); + WurstModel model = parseFiles(Collections.emptyList(), Collections.singletonList(new CU( + "detachedGenericStaticInitializerRemainsMetadataOnly.wurst", String.join("\n", + "package Test", + "native testSuccess()", + "class Slot", + " static T value", + " static function set(T newValue)", + " value = newValue", + " static function get() returns T", + " return value", + "init", + " Slot.set(7)", + " Slot.set(\"ok\")", + " if Slot.get() == 7 and Slot.get() == \"ok\"", + " testSuccess()" + ))), false, compiler); + assertTrue("unexpected parse/type errors: " + gui.getErrorList(), gui.getErrorList().isEmpty()); + compiler.checkProg(model); + assertTrue("unexpected compile errors: " + gui.getErrorList(), gui.getErrorList().isEmpty()); + ImProg prog = compiler.translateProgToIm(model); + compiler.runCompiletime(WurstProjectConfigData.empty(), false, false); + + ImVar genericStatic = prog.getGlobals().stream() + .filter(global -> global.getName().contains("value")) + .findFirst() + .orElseThrow(() -> new AssertionError("expected the translated Slot.value global")); + ImSet detached = JassIm.ImSet(genericStatic.attrTrace(), JassIm.ImVarAccess(genericStatic), + ImHelper.nullExpr()); + prog.getGlobalInits().put(genericStatic, Collections.singletonList(detached)); + assertTrue("default initializer must remain detached metadata", detached.getParent() == null); + + LuaCompilationUnit luaCode = compiler.transformProgToLua(); + StringBuilder output = new StringBuilder(); + luaCode.print(output, 0); + java.util.regex.Matcher declarations = java.util.regex.Pattern + .compile("(?m)^Slot_value_\\S* = nil$") + .matcher(output); + int storages = 0; + while (declarations.find()) { + storages++; + } + assertEquals("both concrete static specializations must survive detached initialization", + 2, storages); + } + @Test public void constructedErasedInstantiationDoesNotDuplicateSpecializedStaticInitializer() { test().testLua(true).executeProg().lines(