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 b3bece3c5..04d0e0327 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 @@ -766,6 +766,37 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { return newC; } + private ImExpr rewriteGenericGlobalsInExpr(ImExpr e, ImClass owningClass, GenericTypes generics) { + e.accept(new Element.DefaultVisitor() { + @Override public void visit(ImVarAccess va) { + super.visit(va); + ImVar v = va.getVar(); + ImClass owner = globalToClass.get(v); + if (owner == null) return; + + GenericTypes g = normalizeToClassArity(generics, owner, "init-rhs"); + if (g == null || g.containsTypeVariable()) return; + + ImVar sg = ensureSpecializedGlobal(v, owner, g); + if (sg != null) va.setVar(sg); + } + + @Override public void visit(ImVarArrayAccess aa) { + super.visit(aa); + ImVar v = aa.getVar(); + ImClass owner = globalToClass.get(v); + if (owner == null) return; + + GenericTypes g = normalizeToClassArity(generics, owner, "init-rhs"); + if (g == null || g.containsTypeVariable()) return; + + ImVar sg = ensureSpecializedGlobal(v, owner, g); + if (sg != null) aa.setVar(sg); + } + }); + return e; + } + private void createSpecializedGlobals(ImClass originalClass, GenericTypes generics, List typeVars) { String key = gKey(generics); @@ -841,6 +872,7 @@ private void createSpecializedGlobals(ImClass originalClass, GenericTypes generi // Create specialized init sets and schedule: insert each right after its corresponding original init set for (ImSet origSet : originalInits) { ImExpr rhs = origSet.getRight().copy(); + rhs = rewriteGenericGlobalsInExpr(rhs, originalClass, generics); rhs = specializeNullInitializer(rhs, specializedType); ImLExpr newLeft = specializeLhs.apply(origSet.getLeft()); @@ -913,6 +945,15 @@ private void collectGenericUsages() { collectGenericUsages(prog); } + private boolean isGlobalInitStmt(ImSet s, ImVar v) { + List inits = prog.getGlobalInits().get(v); + if (inits == null) return false; + for (ImSet x : inits) { + if (x == s) return true; // identity + } + return false; + } + private void collectGenericUsages(Element element) { element.accept(new Element.DefaultVisitor() { @Override @@ -965,19 +1006,23 @@ public void visit(ImVarArrayAccess vaa) { @Override public void visit(ImSet set) { super.visit(set); - if (set.getLeft() instanceof ImVarAccess) { - ImVarAccess va = (ImVarAccess) set.getLeft(); - if (globalToClass.containsKey(va.getVar())) { - recordGenericGlobalUse(set, va.getVar()); - genericsUses.add(new GenericGlobalAccess(va)); - } - } else if (set.getLeft() instanceof ImVarArrayAccess) { - ImVarArrayAccess vaa = (ImVarArrayAccess) set.getLeft(); - if (globalToClass.containsKey(vaa.getVar())) { - recordGenericGlobalUse(set, vaa.getVar()); - genericsUses.add(new GenericGlobalArrayAccess(vaa)); - } + + ImVar v = null; + if (set.getLeft() instanceof ImVarAccess va) v = va.getVar(); + else if (set.getLeft() instanceof ImVarArrayAccess aa) v = aa.getVar(); + else return; + + if (!globalToClass.containsKey(v)) return; + + // IMPORTANT: do not treat global-init statements as “generic global accesses” + if (isGlobalInitStmt(set, v)) { + return; } + + recordGenericGlobalUse(set, v); + genericsUses.add(set.getLeft() instanceof ImVarAccess + ? new GenericGlobalAccess((ImVarAccess) set.getLeft()) + : new GenericGlobalArrayAccess((ImVarArrayAccess) set.getLeft())); } @Override @@ -1393,12 +1438,6 @@ private GenericTypes inferGenericsFromFunction(Element element, ImClass owningCl } } - Map specs = specializedClasses.row(owningClass); - if (specs.size() == 1) { - GenericTypes only = specs.keySet().iterator().next(); - return normalizeToClassArity(only, owningClass, "singleSpecializationFallback"); - } - return null; } current = current.getParent(); 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 3603f2cf2..a6153e34b 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 @@ -2,14 +2,17 @@ import com.google.common.base.Charsets; import com.google.common.io.Files; +import de.peeeq.wurstscript.utils.Utils; import org.testng.annotations.Ignore; import org.testng.annotations.Test; import java.io.File; import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; +import static org.testng.Assert.*; import static tests.wurstscript.tests.BugTests.TEST_DIR; public class GenericsWithTypeclassesTests extends WurstScriptTest { @@ -2022,6 +2025,55 @@ public void genericClassWithStaticMemberArray() { ); } + + @Test + public void genericStaticGlobalsSpecializedInJassInit() throws IOException { + test().executeProg(false).lines( + "package test", + " public class Box", + " static int INITIAL = 16", + " static int MAX = 256", + " construct()", + " function total() returns int", + " return INITIAL + MAX", + " init", + " let a = new Box()", + " let b = new Box()", + " let c = new Box()", + " var initSum = a.total() + b.total() + c.total()", + " if initSum == 3 * (16 + 256)", + " initSum = initSum + 1", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_genericStaticGlobalsSpecializedInJassInit_no_opts.j"); + String compiled = Files.toString(output, Charsets.UTF_8); + + Set initTargets = new HashSet<>(); + for (String line : compiled.split("\\R")) { + String trimmed = line.trim(); + if (trimmed.startsWith("set Box_INITIAL")) { + String[] parts = trimmed.split("\s+"); + if (parts.length > 1) { + initTargets.add(parts[1]); + } + } + } + assertEquals(initTargets.size(), 3); + + Set maxTargets = new HashSet<>(); + for (String line : compiled.split("\\R")) { + String trimmed = line.trim(); + if (trimmed.startsWith("set Box_MAX")) { + String[] parts = trimmed.split("\s+"); + if (parts.length > 1) { + maxTargets.add(parts[1]); + } + } + } + assertEquals(maxTargets.size(), 3); + } + @Test public void fullArrayListTest() throws IOException { test().withStdLib().executeProg().executeTests().file(new File(TEST_DIR + "arrayList.wurst")); @@ -2112,4 +2164,104 @@ public void genericStaticRawAccessIsRejected() { ); } + @Test + public void staticInitAppliedToAllSpecializations_rawJassNoOpts() throws IOException { + // Compile a tiny inline program (no external files) and then inspect the *no_opts* Jass output. + testAssertOkLines( true, + "package test", + "native testSuccess()", + "", + "class Box", + " private static constant cap = 16", + " private static constant cappy = cap + 3", + " function getCap() returns int", + " return cap", + " function getCappy() returns int", + " return cappy", + "", + "tuple tt(real p, string s)", + "init", + " let bi = new Box", + " let br = new Box", + " let sr = new Box", + " let tr = new Box", + " if bi.getCap() == 16 and br.getCap() == 16 and sr.getCap() == 16 and tr.getCap() == 16 and bi.getCappy() == 19 and br.getCappy() == 19 and sr.getCappy() == 19 and tr.getCappy() == 19", + " testSuccess()", + "endpackage", + "" + ); + + // Read raw output (prefer .j, fallback .jim if that’s what the harness writes) + String out = readNoOptsOutput("GenericsWithTypeclassesTests_staticInitAppliedToAllSpecializations_rawJassNoOpts"); + + // Core assertion: both specializations must be initialized to 16 in the init function(s) + assertTrue(out.contains("set Box_cap_integer_u = 16"), + "Missing init for int specialization (expected: set Box_cap_integer_u = 16)"); + assertTrue(out.contains("set Box_cap_real_u = 16"), + "Missing init for real specialization (expected: set Box_cap_real_u = 16)"); + + // Guard against the observed collapse where everything becomes one specialization: + int intSets = countLinesContaining(out, "set Box_cap_integer_u = 16"); + int realSets = countLinesContaining(out, "set Box_cap_real_u = 16"); + assertTrue(intSets >= 1 && realSets >= 1, "Specialization init collapsed."); + } + + private static String readNoOptsOutput(String baseName) throws IOException { + // Most runs produce .j; some configs dump IM/Jass into .jim. Pick whichever exists. + File j = new File(TEST_OUTPUT_PATH + baseName + "_no_opts.j"); + if (j.exists()) return com.google.common.io.Files.toString(j, Charsets.UTF_8); + + File jim = new File(TEST_OUTPUT_PATH + baseName + "_no_opts.jim"); + if (jim.exists()) return com.google.common.io.Files.toString(jim, Charsets.UTF_8); + + throw new IOException("No no_opts output found for " + baseName + " (tried .j and .jim)"); + } + + private static int countLinesContaining(String text, String needle) { + int c = 0; + for (String line : text.split("\\R")) { + if (line.contains(needle)) c++; + } + return c; + } + + @Test + public void genericStaticInit() throws IOException { + test().executeProg(false) + .executeTests(false) + .withStdLib(false) + .withInputs(Map.of("test.wurst", Utils.string( + "package test", + "public class Box", + " static int cap = 16", + " function getCap() returns int", + " return cap" + ), + "usage.wurst", Utils.string( + "package usage", + "import test", + "native testSuccess()", + "native print(int i)", + "function useInt()", + " let b = new Box", + " print(b.getCap())", + "function useReal()", + " let b = new Box>", + " print(b.getCap())", + "init", + " useInt()", + " useReal()", + " testSuccess()" + ))) + .run(); + + String jass = com.google.common.io.Files.toString( + new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_genericStaticInit_no_opts.j"), + Charsets.UTF_8 + ); + + assertTrue(jass.contains("set Box_cap_integer_u = 16")); + assertTrue(jass.contains("set Box_cap_Box_real__u = 16")); + } + }