diff --git a/.gitignore b/.gitignore index 7b7bf5ad3..fad3ad29c 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ de.peeeq.wurstscript/output.txt /HelperScripts/.gradle /.gradle-user-home /gradle-home-temp +/.claude/ +/LOOP.md diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 000000000..ba7ec03be --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,115 @@ +# Language feature backlog + +Working notes for ongoing work on type class bounds for `T:` generics +(shipped in #1226, #1228, #1229). Ordered: take the top unblocked item. + +Keep this file current. It is the only memory that survives between sessions. +When an item is finished, move it to Done with one line on what actually +happened. When something is learned that would have saved time, write it under +Notes rather than leaving it in a commit message. + +## Todo + +Numbering is stable: finished items leave a gap rather than shifting the ones below, +because `LOOP.md` refers to items by number. + +3. **`slotFor` is bound to `get`'s implementation** in the emitted Lua — confirmed real, and + not an artefact of item 1: it survives sanitisation unchanged. Diagnosed, not yet fixed. + The alias sets in `LuaDispatchPreparation` decide which slots a method claims, and + `sharesSemanticName` accepts a match on *either* of two names: the source name (`get`, + `slotFor`) or `semanticNameFromMethodName`, which is the substring after the last + underscore. For a specialised method that substring is a type-argument fragment — + `FastHashMap_get_specialized__integer__integer___integer` and the `slotFor` one both yield + `integer` — so two unrelated methods count as sharing a name. They also share a dispatch + signature here (`(pos) returns int` both), which is the other half of the guard, so `get` + claims `slotFor`'s slot. Fix: when both methods have a real source name, that should decide; + the substring heuristic is a fallback for when there is no trace to ask, not an alternative. + Watch the closure and bridge cases in `TypeClassTests`/`LuaBackendAuditTests` — they are what + the loose match was presumably widened for. + +4. **Finish the FastHashMap proof.** `FastHashMapTests` is the first real use of bounds. + Add `remove` with tombstones, and an assertion that the emitted code stays cheap: no + dispatch node, no instance dictionary, and no WC3 hashtable natives — array access only, + which is the whole point versus `HashMap extends Table`. + +5. **Lua dispatch inside a closure.** Works on Jass since #1229. On Lua the specialised class + is built correctly but nothing calls it, because the closure is reached through its + interface and `specializeMethod` renames the method out of its dispatch slot. + `TypeClassTests.dispatchInsideClosureIsRejectedForLua` pins the current diagnostic and + should become a success test. Related to item 1; AGENTS.md flags this machinery. + +6. **Lua dispatch inside the constructor** of a bounded generic class. Works on Jass. + +7. **Module bounds.** `module M` is rejected with a clear message today. Needs + receiver rewriting during expansion, or type parameters on `ModuleInstanciation`. + +8. **`MOD_INT`/`DIV_INT` return the left operand's type** rather than `int` + (`AttrExprType.java`, the `case MOD_INT` branch), where `caseMathOperation` returns + `WurstTypeInt.instance()` for `+`, `-`, `*`. Only observable if something is a proper + subtype of int, so it may be harmless — establish whether it is reachable, then either fix + it or leave a comment saying why the asymmetry is intended. Small. + +9. **Keep `WURST_LANGUAGE.md` and `CHANGELOG.md` current** as items land. The bounds section + says nothing about closures, which now work on Jass. Fold this into whichever item changes + the behaviour rather than doing it as a separate pass. + +10. **One `ImTypeVar` per type parameter.** Name-tolerant lookups remain in + `EliminateGenerics.indexOfTypeVar`, `inheritTypeClassBinding` and + `ProgramState.getCurrentTypeArgument`, compensating for several nodes standing for one + source parameter. Making the node canonical lets all three compare by identity and removes + a class of silent wrong dispatch. Mechanical, well covered by the suite. + +11. **Jass temp counter is not reset between compilations.** Two runs of the same commit emit + different `.j` (`temp151` vs `temp8`) because the counter is JVM-wide and depends on how + many tests ran before. Not wrong for compiling one map, but it means `.j` cannot be diffed + across runs to validate a change — only `.lua` can. Fixing it would make Jass diffable. + +12. **Standing item, never finished.** When nothing above is left, find the next thing worth + doing and add it here rather than stopping. Good sources, in order: a test that would have + caught a bug already found; a place where two mechanisms do the same job and disagree; a + comment claiming something the code no longer does; a path where a wrong result is silent + rather than loud. Add what is found as a numbered item and start on it. + +## Blocked on a decision + +- **Eliminating the remaining `castTo int`.** The motivating case is timer data attachment + (`ClosureTimers.wurst`), and the containers behind it: `Table` has 81 casts, `HashList` 13, + `HashSet` 6, `HashMap` 4. None can adopt bounds as things stand, because an instance is + declared one type at a time and these accept any type. It needs a way to give an instance + for a whole family — every class type, or every handle type — which is a language design + question: what the syntax is, where such an instance may be declared under the orphan rule, + and whether a specific instance always beats a family one. Do not start this autonomously. + +## Out of scope + +- The stdlib itself. `de.peeeq.wurstscript/temp/WurstStdlib2` is a fetched artefact for tests; + editing it changes nothing real. `FastHashMap` ships from the WurstStdlib2 repo once the + compiler-side proof is complete, and that is a separate decision. + +## Done + +- 1 + 2. Lua method names are sanitised where they are assigned, not where they are printed. + `LuaDispatchPreparation.normalizeMethodNames` is the pass that gives one name to a whole + dispatch group, so it now sanitises before uniquing — two names differing only in characters + Lua has no place for still get a slot each. `LuaTranslator` maps every slot key and every + `LuaMethod` name through the same function, so call sites and class tables agree. Lua's + identifier rule now lives in one place, `LuaIdentifiers`. `LuaAssertions.assertNamesAreValidIdentifiers` + walks the emitted Lua and fails on any name that is not an identifier; it runs for every + `testLua` compile, so the silent two-target-assignment case cannot come back. +- Substitution now carries the type class binding with the type (#1229). Also fixed the + type-variable reference on `ImTypeVarDispatch`, which a walk over types alone missed. + +## Notes + +- `%` is real modulo in Wurst; `mod` is integer modulo. `int % 8` types as `real`. +- Emitted Lua must be byte-identical for identical input (AGENTS.md §8). It is the only + emitted output that can be diffed across runs — see item 11. +- Method names are not what the frontend called them. `LuaDispatchPreparation` renames a whole + dispatch group to one name and attaches alias sets, and only then does the backend run. A + question about which Lua slot something lands in is a question about that pass, not about + `LuaTranslator`. +- Tests run five Jass configurations plus the interpreter, then the Lua target separately. + `testAssertOkLines(true, ...)` covers both the pre-transform interpreter and full + monomorphisation, so it is a stronger check than it looks. +- The stdlib copy under `de.peeeq.wurstscript/temp/WurstStdlib2` is a fetched artefact for + tests. Real stdlib changes belong in the WurstStdlib2 repo, not here. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java index 662c812b9..cbed8db1b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java @@ -11,6 +11,7 @@ import de.peeeq.wurstscript.jassIm.ImProg; import de.peeeq.wurstscript.jassIm.ImType; import de.peeeq.wurstscript.jassIm.ImVars; +import de.peeeq.wurstscript.translation.lua.translation.LuaIdentifiers; import java.util.ArrayList; import java.util.Collection; @@ -99,7 +100,9 @@ private static void normalizeMethodNames(ImProg prog, List allMethods) continue; } group.sort(Comparator.comparing(LuaDispatchPreparation::methodSortKey)); - String name = uniqueName(group.get(0).getName(), usedNames); + // The name is about to become a Lua table key. Sanitising before uniquing means two + // names that only differed in characters Lua has no place for still get one slot each. + String name = uniqueName(LuaIdentifiers.toIdentifier(group.get(0).getName()), usedNames); for (ImMethod method : group) { method.setName(name); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java index 82d003d60..4c1ed5298 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java @@ -1,9 +1,19 @@ package de.peeeq.wurstscript.translation.lua.translation; +import de.peeeq.wurstscript.luaAst.Element; +import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; +import de.peeeq.wurstscript.luaAst.LuaExprFieldAccess; +import de.peeeq.wurstscript.luaAst.LuaExprFunctionCallByName; +import de.peeeq.wurstscript.luaAst.LuaFunction; +import de.peeeq.wurstscript.luaAst.LuaMethod; +import de.peeeq.wurstscript.luaAst.LuaTableNamedField; +import de.peeeq.wurstscript.luaAst.LuaVariable; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; /** * Static assertion helpers for the Lua backend. @@ -15,6 +25,66 @@ public class LuaAssertions { private LuaAssertions() {} + /** + * Asserts that every name the backend emits is a Lua identifier. + * + *

A name that is not one usually breaks the syntax check, but not always: a table key + * containing a comma parses as an assignment to two targets and quietly stores the value in + * the wrong place. Checking the names themselves catches that case at the point it is + * introduced, rather than as a wrong result at runtime. + */ + public static void assertNamesAreValidIdentifiers(LuaCompilationUnit luaCode) { + Set invalid = new TreeSet<>(); + luaCode.accept(new Element.DefaultVisitor() { + private void check(String kind, String name) { + // A vararg parameter is the one name that is legal without being an identifier. + if (!LuaIdentifiers.isValid(name) && !LuaIdentifiers.VARARG.equals(name)) { + invalid.add(kind + " '" + name + "'"); + } + } + + @Override + public void visit(LuaFunction f) { + super.visit(f); + check("function", f.getName()); + } + + @Override + public void visit(LuaMethod m) { + super.visit(m); + check("method", m.getName()); + } + + @Override + public void visit(LuaVariable v) { + super.visit(v); + check("variable", v.getName()); + } + + @Override + public void visit(LuaExprFieldAccess fa) { + super.visit(fa); + check("field", fa.getFieldName()); + } + + @Override + public void visit(LuaTableNamedField f) { + super.visit(f); + check("field", f.getFieldName()); + } + + @Override + public void visit(LuaExprFunctionCallByName call) { + super.visit(call); + check("call to", call.getFuncName()); + } + }); + if (!invalid.isEmpty()) { + throw new RuntimeException("Wurst Lua backend assertion failed: emitted names are not Lua identifiers: " + + String.join(", ", invalid)); + } + } + /** * Asserts that every emitted call to {@code __wurst_GetHandleId} has a helper definition. * diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaIdentifiers.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaIdentifiers.java new file mode 100644 index 000000000..b0da4381f --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaIdentifiers.java @@ -0,0 +1,67 @@ +package de.peeeq.wurstscript.translation.lua.translation; + +/** + * Lua's rule for what a generated name may look like, kept in one place. + * + *

Names from the intermediate language are not constrained to Lua's identifier syntax; + * specialised generics, for example, are named after their type arguments. Sanitising in the + * backend keeps that rule where it belongs rather than requiring every earlier pass to know + * about Lua. Any collisions the mapping introduces are resolved by the usual uniquing. + */ +public final class LuaIdentifiers { + + /** Lua's vararg parameter, which is a legal parameter name but not an identifier. */ + public static final String VARARG = "..."; + + /** + * Whether {@code name} can be used as-is as a Lua identifier or table key. + * + *

A keyword is spelled like an identifier and is not one. Wurst reserves a different set, so + * a method can be declared {@code repeat} or {@code goto} and reach the backend under that + * name; emitted as a table key it is a syntax error rather than a wrong result, but this is the + * check that is supposed to catch it first. + */ + public static boolean isValid(String name) { + if (name == null || name.isEmpty() || isDigit(name.charAt(0))) { + return false; + } + for (int i = 0; i < name.length(); i++) { + if (!isIdentifierPart(name.charAt(i))) { + return false; + } + } + return !LuaReservedNames.LUA_KEYWORDS.contains(name); + } + + /** Maps any name onto a Lua identifier, leaving names that already are one untouched. */ + public static String toIdentifier(String name) { + if (isValid(name)) { + return name; + } + StringBuilder sb = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + sb.append(isIdentifierPart(c) ? c : '_'); + } + if (sb.length() == 0 || isDigit(sb.charAt(0))) { + sb.insert(0, '_'); + } + // A trailing underscore rather than a counter, so the name a keyword maps to is the same + // wherever it is derived - call sites and class tables have to agree without consulting + // each other. + while (LuaReservedNames.LUA_KEYWORDS.contains(sb.toString())) { + sb.append('_'); + } + return sb.toString(); + } + + private static boolean isIdentifierPart(char c) { + return c == '_' || (c < 128 && Character.isLetterOrDigit(c)); + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private LuaIdentifiers() {} +} 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 4e729a863..f00242efa 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 @@ -135,7 +135,9 @@ public LuaFunction initFor(ImFunction a) { @Override public LuaMethod initFor(ImMethod a) { LuaExpr receiver = LuaAst.LuaExprVarAccess(luaClassVar.getFor(a.attrClass())); - return LuaAst.LuaMethod(receiver, a.getName(), LuaAst.LuaParams(), LuaAst.LuaStatements()); + // A method name is a table key, so it must be an identifier - but unlike a variable + // it must not be uniqued: every override has to keep landing in the same slot. + return LuaAst.LuaMethod(receiver, dispatchSlotName(a.getName()), LuaAst.LuaParams(), LuaAst.LuaStatements()); } }; @@ -196,28 +198,8 @@ public LuaTranslator(ImProg prog, ImTranslator imTr) { luaModel = LuaAst.LuaCompilationUnit(); } - /** - * Makes an intermediate-language name usable as a Lua identifier. - *

- * Names from the IM are not constrained to Lua's identifier syntax; specialised generics, for - * example, are named after their type arguments. Sanitising here keeps that rule where it - * belongs, in the backend, rather than requiring every earlier pass to know about Lua. Any - * collisions the mapping introduces are resolved by the usual uniquing. - */ - private static String toLuaIdentifier(String name) { - StringBuilder sb = new StringBuilder(name.length()); - for (int i = 0; i < name.length(); i++) { - char c = name.charAt(i); - sb.append(c == '_' || Character.isLetterOrDigit(c) && c < 128 ? c : '_'); - } - if (sb.length() == 0 || Character.isDigit(sb.charAt(0))) { - sb.insert(0, '_'); - } - return sb.toString(); - } - protected String uniqueName(String rawName) { - String name = toLuaIdentifier(rawName); + String name = LuaIdentifiers.toIdentifier(rawName); Integer nextIndex = uniqueNameCounters.get(name); if (nextIndex == null) { uniqueNameCounters.put(name, 1); @@ -462,7 +444,7 @@ private void collectMethodNames(ImClass c, Set methodNames, Set } visited.add(c); for (ImMethod method : c.getMethods()) { - methodNames.add(method.getName()); + methodNames.add(dispatchSlotName(method.getName())); } for (ImClassType sc : c.getSuperClasses()) { collectMethodNames(sc.getClassDef(), methodNames, visited); @@ -914,7 +896,7 @@ private void createMethods(ImClass c, LuaVariable classVar) { ImMethod chosen = chosenByGroup.get(groupMethods); Set memberNames = new HashSet<>(); for (ImMethod m : groupMethods) { - memberNames.add(m.getName()); + memberNames.add(dispatchSlotName(m.getName())); } Set slotNames = collectDispatchSlotNames(c, groupMethods); for (String slotName : slotNames) { @@ -934,7 +916,7 @@ private void createMethods(ImClass c, LuaVariable classVar) { ImMethod chosen = chosenByGroup.get(groupMethods); Set memberNames = new HashSet<>(); for (ImMethod m : groupMethods) { - memberNames.add(m.getName()); + memberNames.add(dispatchSlotName(m.getName())); } for (String slotName : collectDispatchSlotNames(c, groupMethods)) { if (memberNames.contains(slotName)) { @@ -973,6 +955,15 @@ && implArity(chosen) != implArity(current)) { } } + /** + * The Lua table key a dispatch slot is emitted under. Aliases and class-qualified names are + * built from IM names, which may contain characters Lua has no place for; the mapping has to + * be the same one call sites go through, so that a slot is still found under its new name. + */ + private String dispatchSlotName(String rawName) { + return LuaIdentifiers.toIdentifier(rawName); + } + private Set collectDispatchSlotNames(ImClass receiverClass, List groupMethods) { Set slotNames = new TreeSet<>(); Set semanticNames = new TreeSet<>(); @@ -982,7 +973,7 @@ private Set collectDispatchSlotNames(ImClass receiverClass, List collectDispatchSlotNames(ImClass receiverClass, List()); for (String className : classNames) { for (String semanticName : semanticNames) { - slotNames.add(className + "_" + semanticName); + slotNames.add(dispatchSlotName(className + "_" + semanticName)); } } } 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 new file mode 100644 index 000000000..50becb1f3 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FastHashMapTests.java @@ -0,0 +1,207 @@ +package tests.wurstscript.tests; + +import org.testng.annotations.Test; + +/** + * Exercises type class bounds through the container they were added for: a hash map whose key type + * is bounded by {@code Hashable}, so it can hash and compare keys without erasing them to int. + *

+ * This is deliberately a whole working container rather than a minimal repro. It combines things + * nothing else tests together: two type parameters where only the first is bounded, static arrays of + * a bounded type parameter, and a bound used from a private method reached through a public one. + */ +public class FastHashMapTests extends WurstScriptTest { + + /** + * Storage follows ArrayList: one array per specialisation, carved into a section per instance. + * Collision handling is linear probing, so a lookup calls both requirements of the bound. + */ + private static String[] fastHashMap(String... extra) { + String[] head = { + "package test", + "native testSuccess()", + "constant int CAPACITY = 8", + "interface Hashable", + " function hash(T x) returns int", + " function equals(T a, T b) returns boolean", + "class FastHashMap", + " private static K array keys", + " private static V array values", + " private static boolean array used", + " private static int nextFree = 0", + " private int base", + " private int count = 0", + " construct()", + " base = nextFree", + " nextFree += CAPACITY", + " private function slotFor(K key) returns int", + " var i = K.hash(key) mod CAPACITY", + " if i < 0", + " i += CAPACITY", + " while used[base + i] and not K.equals(keys[base + i], key)", + " i = (i + 1) mod CAPACITY", + " return base + i", + " function put(K key, V value)", + " let s = slotFor(key)", + " if not used[s]", + " used[s] = true", + " keys[s] = key", + " count++", + " values[s] = value", + " function get(K key) returns V", + " return values[slotFor(key)]", + " function has(K key) returns boolean", + " return used[slotFor(key)]", + " function size() returns int", + " return count", + }; + String[] all = new String[head.length + extra.length]; + System.arraycopy(head, 0, all, 0, head.length); + System.arraycopy(extra, 0, all, head.length, extra.length); + return all; + } + + private static final String[] INT_INSTANCE = { + "implements Hashable", + " function hash(int x) returns int", + " return x", + " function equals(int a, int b) returns boolean", + " return a == b", + }; + + private static String[] program(String[]... parts) { + int size = 0; + for (String[] part : parts) { + size += part.length; + } + String[] all = new String[size]; + int at = 0; + for (String[] part : parts) { + System.arraycopy(part, 0, all, at, part.length); + at += part.length; + } + return all; + } + + /** Keys 1 and 9 land in the same slot at capacity 8, so the probe path is taken. */ + private static final String[] USE_WITH_COLLISION = { + "init", + " let m = new FastHashMap()", + " m.put(1, 10)", + " m.put(9, 90)", + " m.put(2, 20)", + " if m.get(1) == 10 and m.get(9) == 90 and m.get(2) == 20", + " if m.size() == 3 and not m.has(3)", + " testSuccess()", + }; + + @Test + public void fastHashMapRuntime() { + testAssertOkLines(true, program(fastHashMap(), INT_INSTANCE, USE_WITH_COLLISION)); + } + + @Test + public void fastHashMapRuntimeLua() { + test().testLua(true).executeProg().lines(program(fastHashMap(), INT_INSTANCE, USE_WITH_COLLISION)); + } + + /** + * A tuple key, which is the case old generics cannot serve at all: a tuple has no int + * representation to cast to, so the only way to key a map by one is to say how it hashes. + */ + private static final String[] TUPLE_INSTANCE = { + "tuple pos(int x, int y)", + "implements Hashable", + " function hash(pos p) returns int", + " return p.x * 31 + p.y", + " function equals(pos a, pos b) returns boolean", + " return a.x == b.x and a.y == b.y", + }; + + @Test + public void tupleKey() { + testAssertOkLines(true, program(fastHashMap(), TUPLE_INSTANCE, new String[]{ + "init", + " let m = new FastHashMap()", + " m.put(pos(1, 2), 12)", + " m.put(pos(2, 1), 21)", + " if m.get(pos(1, 2)) == 12 and m.get(pos(2, 1)) == 21", + " if m.has(pos(1, 2)) and not m.has(pos(9, 9))", + " testSuccess()" + })); + } + + @Test + public void tupleKeyLua() { + test().testLua(true).executeProg().lines(program(fastHashMap(), TUPLE_INSTANCE, new String[]{ + "init", + " let m = new FastHashMap()", + " m.put(pos(1, 2), 12)", + " m.put(pos(2, 1), 21)", + " if m.get(pos(1, 2)) == 12 and m.get(pos(2, 1)) == 21", + " testSuccess()" + })); + } + + /** A key type of the user's own, with the instance declared in the same package as the type. */ + @Test + public void classKeyWithUserInstance() { + testAssertOkLines(true, program(fastHashMap(), new String[]{ + "class Item", + " int id", + " construct(int id)", + " this.id = id", + "implements Hashable", + " function hash(Item i) returns int", + " return i.id", + " function equals(Item a, Item b) returns boolean", + " return a.id == b.id", + "init", + " let m = new FastHashMap()", + " let a = new Item(1)", + " let b = new Item(2)", + " m.put(a, \"a\")", + " m.put(b, \"b\")", + " if m.get(a) == \"a\" and m.get(b) == \"b\" and m.size() == 2", + " testSuccess()" + })); + } + + /** + * Two specialisations live at once. Storage is static per specialisation, so this is what would + * break if the arrays of one instantiation were shared with another. + */ + @Test + public void twoSpecialisationsCoexist() { + testAssertOkLines(true, program(fastHashMap(), INT_INSTANCE, new String[]{ + // Every string hashes alike, so this also exercises the probe path on every lookup. + "implements Hashable", + " function hash(string s) returns int", + " return 0", + " function equals(string a, string b) returns boolean", + " return a == b", + "init", + " let ints = new FastHashMap()", + " let strs = new FastHashMap()", + " ints.put(1, 100)", + " strs.put(\"a\", 200)", + " if ints.get(1) == 100 and strs.get(\"a\") == 200", + " if ints.size() == 1 and strs.size() == 1", + " testSuccess()" + })); + } + + /** Two maps of the same instantiation must not share storage either. */ + @Test + public void twoInstancesOfOneSpecialisation() { + testAssertOkLines(true, program(fastHashMap(), INT_INSTANCE, new String[]{ + "init", + " let a = new FastHashMap()", + " let b = new FastHashMap()", + " a.put(1, 10)", + " b.put(1, 20)", + " if a.get(1) == 10 and b.get(1) == 20 and a.size() == 1 and b.size() == 1", + " testSuccess()" + })); + } +} diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java index 6e0ba186c..cd8429a4e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java @@ -121,6 +121,26 @@ public void dispatchInsideClosureIsRejectedForLua() { ); } + /** + * Wurst and Lua reserve different words, so a method can be declared {@code repeat} and reach + * the backend under that name. A closure adds the name it implements as a dispatch alias + * directly, without the uniquing that protects method names, so the alias arrives as a bare + * keyword and is emitted as a table key — {@code expected near 'repeat'} from luac. + */ + @Test + public void closureImplementingALuaKeywordName() { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface Producer", + " function repeat() returns int", + "init", + " Producer p = () -> 42", + " if p.repeat() == 42", + " testSuccess()" + ); + } + /** Each type argument picks its own instance, so one generic serves several types. */ @Test public void twoInstancesOfOneClass() { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index 3498cec3f..545f28c4d 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -25,6 +25,7 @@ import de.peeeq.wurstscript.jassprinter.JassPrinter; import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; import de.peeeq.wurstscript.luaAst.*; +import de.peeeq.wurstscript.translation.lua.translation.LuaAssertions; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.translation.imtranslation.RecycleCodeGeneratorQueue; import de.peeeq.wurstscript.utils.Utils; @@ -500,6 +501,7 @@ private void translateAndTestLua(String name, boolean executeProg, WurstGui gui, compiler.runCompiletime(WurstProjectConfigData.empty(), false, false); LuaCompilationUnit luaCode = compiler.transformProgToLua(); + LuaAssertions.assertNamesAreValidIdentifiers(luaCode); checkLuaRootPurity(luaCode); StringBuilder sb = new StringBuilder(); luaCode.print(sb, 0);