From dd62541d1c7b45b3e32b6289df70a9f63fe5d86d Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 16 Aug 2026 13:55:36 +0200 Subject: [PATCH 1/4] Keep the fields a specialised class allocates A specialised class holds copies of the original's fields, and nothing refers to the copies: an access made before specialisation still names the original's variable. Both passes which drop unread fields therefore dropped every one of them, and an instance allocated from the specialised class came out of its create with an empty table while the emitted code went on reading those fields by name - size() returning nil, put incrementing nil. Lua resolves a field by name and both classes carry the same one, so a name read anywhere keeps the field wherever it is allocated. The cost is an unused table key in the worst case. It was reachable only where an allocation goes through the specialised class rather than the erased one, which is why nothing caught it: the existing tests allocate through the erased class. Compiling the container against the standard library is what put an allocation on the other path. --- .../translation/imoptimizer/ImOptimizer.java | 12 +++- .../lua/translation/RemoveGarbage.java | 11 +++- .../wurstscript/tests/FastHashMapTests.java | 60 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index f9c707d6e..8461d6873 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -19,6 +19,8 @@ import de.peeeq.wurstscript.utils.Pair; import de.peeeq.wurstscript.validation.TRVEHelper; +import java.util.stream.Collectors; + import java.util.*; public class ImOptimizer { @@ -124,6 +126,7 @@ public boolean removeGarbage() { ImProg prog = trans.imProg(); trans.calculateCallRelationsAndReadVariables(); final Set readVars = trans.getReadVariables(); + final Set readFieldNames = readVars.stream().map(ImVar::getName).collect(Collectors.toSet()); final Set usedFuncs = trans.getUsedFunctions(); SideEffectAnalyzer sideEffectAnalyzer = new SideEffectAnalyzer(prog); @@ -150,8 +153,15 @@ public boolean removeGarbage() { totalFunctionsRemoved += classFunctionsBefore - classFunctionsAfter; allFunctions.addAll(c.getFunctions()); + // A specialised class holds copies of the original's fields, and nothing refers to + // the copies: an access made before specialisation still names the original's + // variable, and both carry the same name. Dropping a copy leaves the allocation + // empty while the emitted code goes on reading that field, so a name which is read + // anywhere keeps the field wherever it was allocated. int classFieldsBefore = c.getFields().size(); - changes |= c.getFields().retainAll(readVars); + changes |= c.getFields().retainAll(c.getFields().stream() + .filter(field -> readVars.contains(field) || readFieldNames.contains(field.getName())) + .collect(Collectors.toSet())); int classFieldsAfter = c.getFields().size(); totalGlobalsRemoved += classFieldsBefore - classFieldsAfter; } 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 7c2fb7582..17b80113a 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 @@ -86,8 +86,17 @@ public static void removeGarbage(ImProg prog) { prog.getClasses().removeIf(c -> !used.getClasses().contains(c)); prog.getGlobals().removeIf(g -> !used.getVars().contains(g) && !TRVEHelper.protectedVariables.contains(g.getName())); prog.getFunctions().removeIf(f -> !used.getFunctions().contains(f)); + // A specialised class holds copies of the original's fields, and nothing refers to the + // copies: an access made before specialisation still names the original's variable. Lua + // resolves a field by name and both carry the same one, so a name read anywhere keeps the + // field wherever it is allocated. Dropping the copies leaves an instance of the specialised + // class with no fields at all while the emitted code goes on reading them. + Set readFieldNames = new HashSet<>(); + for (ImVar v : used.getVars()) { + readFieldNames.add(v.getName()); + } for (ImClass c : prog.getClasses()) { - c.getFields().removeIf(g -> !used.getVars().contains(g)); + c.getFields().removeIf(g -> !used.getVars().contains(g) && !readFieldNames.contains(g.getName())); c.getFunctions().removeIf(f -> !used.getFunctions().contains(f)); c.getMethods().removeIf(m -> !used.getMethods().contains(m)); for (ImMethod m : c.getMethods()) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java index 23817b5a2..e79674272 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java @@ -386,6 +386,66 @@ public void emittedCodeCostsNothingExtra() throws IOException { } } + /** + * Everything above compiles a package on its own. The container is meant to live in the standard + * library, and that is a different question: the bound has to keep dispatching with everything the + * library defines in scope, {@code int} has to keep taking the instance declared beside the map + * rather than anything the library brings, and the specialised copies have to survive a program of + * that size being optimised around them. + */ + private static String[] withStandardLibrary(String[] lines) { + return java.util.Arrays.stream(lines) + .filter(line -> !line.equals("native testSuccess()")) + .toArray(String[]::new); + } + + @Test + public void fastHashMapAgainstTheStandardLibrary() { + test().withStdLib().executeProg() + .lines(withStandardLibrary(program(fastHashMap(), INT_INSTANCE, USE_WITH_COLLISION))); + } + + /** + * Compiled rather than run, as every other test which puts the standard library on Lua is: the + * runtime shim cannot initialise the library's own packages, so no standard library program has + * ever executed on that target here. What this covers is that the container survives translation + * with the library in scope, which is where the specialised copies and the erased ones meet. + */ + @Test + public void fastHashMapAgainstTheStandardLibraryLua() throws IOException { + test().withStdLib().testLua(true) + .lines(withStandardLibrary(program(fastHashMap(), INT_INSTANCE, USE_WITH_COLLISION))); + assertSpecialisedClassesAllocateTheirFields( + Files.toString(new File("test-output/lua/FastHashMapTests_fastHashMapAgainstTheStandardLibraryLua.lua"), + Charsets.UTF_8)); + } + + /** + * A specialised class allocates the same fields as the class it was specialised from. Nothing + * refers to the copies it holds - an access made before specialisation still names the original's + * variable - so a pass which drops unread fields drops all of them, and an instance allocated + * from the specialised class comes out with no fields at all while the emitted code goes on + * reading them by name. + */ + private static void assertSpecialisedClassesAllocateTheirFields(String compiled) { + String erasedFields = allocatedFields(compiled, "FastHashMap"); + String specialisedFields = allocatedFields(compiled, "FastHashMap_specialized\\w*"); + if (!erasedFields.equals(specialisedFields)) { + throw new AssertionError("the specialised class should allocate the same fields as the erased one." + + "\n erased: " + erasedFields + + "\n specialised: " + specialisedFields); + } + } + + private static String allocatedFields(String compiled, String classPattern) { + Matcher m = Pattern.compile("function " + classPattern + ":create\\d*\\(\\)\\s*\\R" + + "\\s*local new_inst = \\(\\{([^}]*)\\}\\)").matcher(compiled); + if (!m.find()) { + throw new AssertionError("expected an allocation for " + classPattern + " in:\n" + compiled); + } + return m.group(1).trim(); + } + private String compiledJass(String testName) throws IOException { return Files.toString(new File(TEST_OUTPUT_PATH, "FastHashMapTests_" + testName + ".j"), Charsets.UTF_8); } From 892d6b8d96971ec580bfc562f510770304899deb Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 16 Aug 2026 14:32:06 +0200 Subject: [PATCH 2/4] Keep a specialised field by what it was copied from, not by its name Matching names program-wide retained every field sharing a name with any live one, so a single live field called size kept the dead size of every other class and put an unused entry in each of their instances. The copy is structural, so field i of a specialised class is field i of the original, and recording that pairing where the class is copied says exactly which fields are copies and which they came from. A copy is live when its original is, and nothing else is retained. --- .../peeeq/wurstio/WurstCompilerJassImpl.java | 2 +- .../translation/imoptimizer/ImOptimizer.java | 13 ++++++------ .../imtranslation/EliminateGenerics.java | 5 +++++ .../imtranslation/ImTranslator.java | 20 +++++++++++++++++++ .../lua/translation/RemoveGarbage.java | 19 ++++++++---------- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index f15fc9d12..843a3028a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -956,7 +956,7 @@ public LuaCompilationUnit transformProgToLua() { timeTaker.endPhase(); } beginPhase(13, "lua remove garbage"); - RemoveGarbage.removeGarbage(imProg); + RemoveGarbage.removeGarbage(imProg, imTranslator); imProg.flatten(imTranslator); timeTaker.endPhase(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index 8461d6873..04c8c2526 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -126,7 +126,6 @@ public boolean removeGarbage() { ImProg prog = trans.imProg(); trans.calculateCallRelationsAndReadVariables(); final Set readVars = trans.getReadVariables(); - final Set readFieldNames = readVars.stream().map(ImVar::getName).collect(Collectors.toSet()); final Set usedFuncs = trans.getUsedFunctions(); SideEffectAnalyzer sideEffectAnalyzer = new SideEffectAnalyzer(prog); @@ -153,14 +152,14 @@ public boolean removeGarbage() { totalFunctionsRemoved += classFunctionsBefore - classFunctionsAfter; allFunctions.addAll(c.getFunctions()); - // A specialised class holds copies of the original's fields, and nothing refers to - // the copies: an access made before specialisation still names the original's - // variable, and both carry the same name. Dropping a copy leaves the allocation - // empty while the emitted code goes on reading that field, so a name which is read - // anywhere keeps the field wherever it was allocated. + // A field of a specialised class is a copy which nothing refers to, an access made + // before specialisation still naming the original's variable. It is live exactly + // when the field it was copied from is; dropping it leaves an instance allocated + // with no fields while the emitted code goes on reading them. int classFieldsBefore = c.getFields().size(); changes |= c.getFields().retainAll(c.getFields().stream() - .filter(field -> readVars.contains(field) || readFieldNames.contains(field.getName())) + .filter(field -> readVars.contains(field) + || readVars.contains(trans.originalOfSpecializedField(field))) .collect(Collectors.toSet())); int classFieldsAfter = c.getFields().size(); totalGlobalsRemoved += classFieldsBefore - classFieldsAfter; 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 9d423291b..696f6be40 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 @@ -1427,6 +1427,11 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { } ImClass newC = c.copyWithRefs(); newC.setSuperClasses(new ArrayList<>(newC.getSuperClasses())); + // The copy is structural, so field i of the copy is field i of the original. Nothing will + // refer to the copies, so this is the only record that they are the same fields. + for (int i = 0; i < c.getFields().size() && i < newC.getFields().size(); i++) { + translator.recordSpecializedField(newC.getFields().get(i), c.getFields().get(i)); + } specializedClasses.put(c, generics, newC); prog.getClasses().add(newC); newC.getTypeVariables().removeAll(); 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 18cf87807..93f480826 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 @@ -42,6 +42,26 @@ public class ImTranslator { public static final String $DEBUG_PRINT = "$debugPrint"; + /** + * The field each field of a specialised class was copied from. + *

+ * Nothing refers to a copy: an access made before specialisation still names the original's + * variable. A pass which drops fields nothing reads would drop every copy, leaving an instance of + * the specialised class allocated with no fields while the emitted code goes on reading them. A + * copy is live exactly when the field it was made from is. + */ + private final java.util.Map specializedFieldOrigins = new java.util.IdentityHashMap<>(); + + public void recordSpecializedField(ImVar copy, ImVar original) { + specializedFieldOrigins.put(copy, original); + } + + /** The field {@code copy} was specialised from, or {@code copy} itself if it is not a copy. */ + public ImVar originalOfSpecializedField(ImVar copy) { + ImVar origin = specializedFieldOrigins.get(copy); + return origin == null ? copy : origin; + } + private static final de.peeeq.wurstscript.ast.Element emptyTrace = Ast.NoExpr(); // existing fields (keep callRelations as Guava Multimap to avoid ripple effects) 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 17b80113a..a5f01337e 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 @@ -4,6 +4,7 @@ import com.google.common.collect.Multimap; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; +import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.validation.TRVEHelper; import java.util.Collection; @@ -74,7 +75,7 @@ public void addClass(ImClass c) { } } - public static void removeGarbage(ImProg prog) { + public static void removeGarbage(ImProg prog, ImTranslator translator) { Used used = new Used(); for (ImFunction f : ImHelper.calculateFunctionsOfProg(prog)) { if (f.getName().equals("main") @@ -86,17 +87,13 @@ public static void removeGarbage(ImProg prog) { prog.getClasses().removeIf(c -> !used.getClasses().contains(c)); prog.getGlobals().removeIf(g -> !used.getVars().contains(g) && !TRVEHelper.protectedVariables.contains(g.getName())); prog.getFunctions().removeIf(f -> !used.getFunctions().contains(f)); - // A specialised class holds copies of the original's fields, and nothing refers to the - // copies: an access made before specialisation still names the original's variable. Lua - // resolves a field by name and both carry the same one, so a name read anywhere keeps the - // field wherever it is allocated. Dropping the copies leaves an instance of the specialised - // class with no fields at all while the emitted code goes on reading them. - Set readFieldNames = new HashSet<>(); - for (ImVar v : used.getVars()) { - readFieldNames.add(v.getName()); - } + // A field of a specialised class is a copy which nothing refers to, an access made before + // specialisation still naming the original's variable. It is live exactly when the field it + // was copied from is; dropping it leaves an instance of the specialised class allocated with + // no fields at all while the emitted code goes on reading them. for (ImClass c : prog.getClasses()) { - c.getFields().removeIf(g -> !used.getVars().contains(g) && !readFieldNames.contains(g.getName())); + c.getFields().removeIf(g -> !used.getVars().contains(g) + && !used.getVars().contains(translator.originalOfSpecializedField(g))); c.getFunctions().removeIf(f -> !used.getFunctions().contains(f)); c.getMethods().removeIf(m -> !used.getMethods().contains(m)); for (ImMethod m : c.getMethods()) { From 35dc16b29e1a729f4d90c05b1536b24af11cf417 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 16 Aug 2026 15:08:42 +0200 Subject: [PATCH 3/4] Give a specialised field the Lua key of the field it was copied from A field sharing its name with a method is renamed around it, both being keys of one table. That is decided per class from that class's own methods, and a specialised class need not hold the same set as the class it was copied from once unused ones are dropped, so the two could be renamed differently: a field named size came out as FastHashMap_size_field on the original and FastHashMap_size on the copy, and the allocation wrote a key nothing read. Accesses name the original's field, so the original's name is the only key either can use. Keeping the copy alive was not enough on its own; it has to be alive under the same name. --- .../lua/translation/LuaTranslator.java | 21 ++++++++++++++ .../wurstscript/tests/FastHashMapTests.java | 28 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index f00242efa..d835b5592 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -395,6 +395,27 @@ private void normalizeFieldNames() { for (ImClass c : prog.getClasses()) { normalizeFieldNames(c, processed); } + alignSpecializedFieldNames(); + } + + /** + * Gives every field of a specialised class the name of the field it was copied from. + *

+ * A field sharing its name with a method is renamed around it, because both are keys of one + * table. That is decided per class from that class's own methods, and a specialised class need + * not hold the same set as the class it was copied from once unused ones are dropped - so the + * two can be renamed differently. Accesses still name the original's field, so a copy which kept + * a name of its own would have its allocation write a key nothing reads. + */ + private void alignSpecializedFieldNames() { + for (ImClass c : prog.getClasses()) { + for (ImVar field : c.getFields()) { + ImVar origin = imTr.originalOfSpecializedField(field); + if (origin != field) { + field.setName(origin.getName()); + } + } + } } private void normalizeFieldNames(ImClass c, Set processed) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java index e79674272..f98c1a09b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java @@ -446,6 +446,34 @@ private static String allocatedFields(String compiled, String classPattern) { return m.group(1).trim(); } + /** + * A field may share its name with a method, and Lua puts both in one table, so the field is + * renamed around the method. That renaming is decided per class from that class's own methods, + * and pruning can leave a specialised class holding a different set than the class it was copied + * from - so the two can be renamed differently. The accesses still name the original's field, so + * the copy has to end up with the same key or the allocation writes one nothing reads. + */ + private static String[] fieldNamedLikeAMethod() { + String[] lines = fastHashMap(); + for (int i = 0; i < lines.length; i++) { + lines[i] = lines[i] + .replace("private int count = 0", "private int size = 0") + .replace("count++", "size++") + .replace("count--", "size--") + .replace("function size() returns int", "function size() returns int") + .replace(" return count", " return size"); + } + return lines; + } + + @Test + public void aFieldNamedLikeAMethodKeepsOneKeyAcrossSpecialisation() throws IOException { + test().testLua(true).executeProg() + .lines(program(fieldNamedLikeAMethod(), INT_INSTANCE, USE_WITH_COLLISION)); + assertSpecialisedClassesAllocateTheirFields( + compiledLua("aFieldNamedLikeAMethodKeepsOneKeyAcrossSpecialisation")); + } + private String compiledJass(String testName) throws IOException { return Files.toString(new File(TEST_OUTPUT_PATH, "FastHashMapTests_" + testName + ".j"), Charsets.UTF_8); } From 6165ebd2d72d2e2ec24dfe675525f97ab0abc93e Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 16 Aug 2026 15:37:28 +0200 Subject: [PATCH 4/4] Choose one field name against the methods of every class holding it A field of a specialised class and the field it was copied from must be one table key, because the accesses reaching either still name the original's variable. Naming each side around only its own methods leaves them different; taking the original's name afterwards puts back whatever collision the specialisation had escaped, and a field shadowing a slot is found first by a virtual call, which then tries to call a field. The name is now settled once, against the methods of the original and of every specialisation, and every copy takes it. Neither side can be renamed into the other's slot. --- .../lua/translation/LuaTranslator.java | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index d835b5592..ea86cbfc1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -391,41 +391,52 @@ private void setNameFromTrace(JassImElementWithName named) { } private void normalizeFieldNames() { + Map> namesToAvoid = collectNamesEachFieldMustAvoid(); + Map chosenNames = new IdentityHashMap<>(); Set processed = new HashSet<>(); for (ImClass c : prog.getClasses()) { - normalizeFieldNames(c, processed); + normalizeFieldNames(c, processed, namesToAvoid, chosenNames); } - alignSpecializedFieldNames(); } /** - * Gives every field of a specialised class the name of the field it was copied from. + * The method names a field has to keep clear of, gathered per original field rather than per + * class. *

- * A field sharing its name with a method is renamed around it, because both are keys of one - * table. That is decided per class from that class's own methods, and a specialised class need - * not hold the same set as the class it was copied from once unused ones are dropped - so the - * two can be renamed differently. Accesses still name the original's field, so a copy which kept - * a name of its own would have its allocation write a key nothing reads. + * A specialised class holds a copy of each field, and the accesses reaching either still name the + * original's variable — so the two must end up as one table key. They cannot be normalised + * independently: the classes need not hold the same methods once unused ones are dropped, and a + * specialisation has slots of its own that the original never had. Naming each side around only + * its own methods leaves them different; restoring the original's name afterwards puts back + * whatever collision the specialisation had escaped, and an instance field which shadows a method + * slot is found first by a virtual call, which then tries to call a field. + *

+ * One name chosen against the methods of the original and of every specialisation is safe on all + * of them. */ - private void alignSpecializedFieldNames() { + private Map> collectNamesEachFieldMustAvoid() { + Map> namesToAvoid = new IdentityHashMap<>(); for (ImClass c : prog.getClasses()) { + Set reserved = new HashSet<>(LuaReservedNames.LUA_KEYWORDS); + collectMethodNames(c, reserved, new HashSet<>()); for (ImVar field : c.getFields()) { - ImVar origin = imTr.originalOfSpecializedField(field); - if (origin != field) { - field.setName(origin.getName()); - } + namesToAvoid + .computeIfAbsent(imTr.originalOfSpecializedField(field), origin -> new HashSet<>()) + .addAll(reserved); } } + return namesToAvoid; } - private void normalizeFieldNames(ImClass c, Set processed) { + private void normalizeFieldNames(ImClass c, Set processed, + Map> namesToAvoid, Map chosenNames) { if (!processed.add(c)) { return; } // Superclasses first: all fields of a hierarchy share one instance table, // so a subclass field must be renamed around already-final ancestor names. for (ImClassType sc : c.getSuperClasses()) { - normalizeFieldNames(sc.getClassDef(), processed); + normalizeFieldNames(sc.getClassDef(), processed, namesToAvoid, chosenNames); } // Field names become raw Lua table keys / field accesses, so they must not // collide with Lua keywords, method dispatch slots, or inherited fields. @@ -433,15 +444,26 @@ private void normalizeFieldNames(ImClass c, Set processed) { collectMethodNames(c, reserved, new HashSet<>()); collectSuperFieldNames(c, reserved, new HashSet<>()); for (ImVar field : c.getFields()) { - if (reserved.contains(field.getName())) { + ImVar origin = imTr.originalOfSpecializedField(field); + String settled = chosenNames.get(origin); + if (settled != null) { + // The original and its copies are one key, decided the first time any of them is met. + field.setName(settled); + reserved.add(settled); + continue; + } + Set avoid = new HashSet<>(reserved); + avoid.addAll(namesToAvoid.getOrDefault(origin, Collections.emptySet())); + if (avoid.contains(field.getName())) { String base = field.getName() + "_field"; String candidate = base; int i = 1; - while (reserved.contains(candidate)) { + while (avoid.contains(candidate)) { candidate = base + i++; } field.setName(candidate); } + chosenNames.put(origin, field.getName()); reserved.add(field.getName()); } }