diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java
index faa65f1a7..2bd9aab17 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java
@@ -2,7 +2,10 @@
import de.peeeq.wurstscript.CompilerIntrinsics;
import de.peeeq.wurstscript.ast.FuncDef;
+import de.peeeq.wurstscript.jassIm.ImAnyType;
import de.peeeq.wurstscript.jassIm.ImFunction;
+import de.peeeq.wurstscript.jassIm.ImType;
+import de.peeeq.wurstscript.jassIm.ImTypeVarRef;
import de.peeeq.wurstscript.jassIm.ImVoid;
import de.peeeq.wurstscript.types.TypesHelper;
@@ -74,6 +77,19 @@ public static String nativeStubFor(ImFunction f) {
private static boolean isKeyedPair(ImFunction f) {
return f.getParameters().size() == 2
&& TypesHelper.isIntType(f.getParameters().get(0).getType())
- && TypesHelper.isIntType(f.getParameters().get(1).getType());
+ && isKeyType(f.getParameters().get(1).getType());
+ }
+
+ /**
+ * Types a key may have.
+ *
+ *
A {@code T:} type parameter is the point of this: new generics are erased on Lua rather
+ * than squeezed through {@code castTo int} the way the old {@code } containers are, so the
+ * value arriving here is the element itself and becomes the table key directly - which is what
+ * makes native Lua hashing possible at all. {@code int} stays accepted for keys that are
+ * already integers, and an erased type parameter can also present as ImAnyType by this point.
+ */
+ private static boolean isKeyType(ImType t) {
+ return TypesHelper.isIntType(t) || t instanceof ImTypeVarRef || t instanceof ImAnyType;
}
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java
index 51bfb04ef..6d278d64b 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java
@@ -532,16 +532,29 @@ private static ImFunctionCall call(ImFunction f, ImExpr... args) {
private static ImFunction createNativeStub(String name, ImFunction original) {
ImVars params = JassIm.ImVars();
for (ImVar p : original.getParameters()) {
- params.add(JassIm.ImVar(p.attrTrace(), p.getType().copy(), p.getName(), false));
+ params.add(JassIm.ImVar(p.attrTrace(), erasedForStub(p.getType()), p.getName(), false));
}
return JassIm.ImFunction(
original.attrTrace(), name,
JassIm.ImTypeVars(), params,
- original.getReturnType().copy(),
+ erasedForStub(original.getReturnType()),
JassIm.ImVars(), JassIm.ImStmts(),
Collections.singletonList(FunctionFlagEnum.IS_NATIVE));
}
+ /**
+ * A type safe to put in a stub's signature.
+ *
+ * Stubs are built with no type variables of their own, so copying an ImTypeVarRef would
+ * leave the stub referring to a variable owned by the function it replaced - a free variable,
+ * and malformed IM for every pass that walks types afterwards. A native stub is never generic:
+ * its body is hand-written Lua that does not consult the type, so erasing is the whole fix
+ * rather than rebinding a variable nothing will read.
+ */
+ private static ImType erasedForStub(ImType t) {
+ return t instanceof ImTypeVarRef ? JassIm.ImAnyType() : t.copy();
+ }
+
/**
* Creates a nil-safety wrapper for {@code bjNative}.
*
diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java
index 1c596b124..ddab3f693 100644
--- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java
+++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java
@@ -8,6 +8,10 @@
import de.peeeq.wurstscript.ast.WurstModel;
import de.peeeq.wurstscript.gui.WurstGui;
import de.peeeq.wurstscript.gui.WurstGuiCliImpl;
+import de.peeeq.wurstscript.jassIm.ImFunction;
+import de.peeeq.wurstscript.jassIm.ImType;
+import de.peeeq.wurstscript.jassIm.ImTypeVarRef;
+import de.peeeq.wurstscript.jassIm.ImVar;
import de.peeeq.wurstscript.luaAst.LuaAst;
import de.peeeq.wurstscript.luaAst.LuaCompilationUnit;
import de.peeeq.wurstscript.luaAst.LuaExpr;
@@ -1868,6 +1872,102 @@ public void genericOverrideChainBindsGlobalStateSlotToMostSpecificImplInLua() th
assertDoesNotContainRegex(compiled, "GlobalCheckState\\." + dispatchedSlot + "\\s*=\\s*NoOpState_NoOpState_update");
}
+ /**
+ * Native stubs carry no type variables of their own, so a stub signature must never refer to
+ * one. Copying a T: parameter type verbatim leaves the stub pointing at a variable owned by
+ * the function it replaced - malformed IM for every later pass that walks types.
+ *
+ *
Asserted as a rule rather than through a symptom: the free reference does not break
+ * emission today, so a behavioural test would pass with or without the fix.
+ */
+ @Test
+ public void nativeStubsCarryNoFreeTypeVariables() {
+ WurstGui gui = new WurstGuiCliImpl();
+ WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null,
+ new RunArgs().with("-lua"));
+ List inputs = new ArrayList<>();
+ inputs.add(new CU("nativeStubsCarryNoFreeTypeVariables.wurst", String.join(System.lineSeparator(),
+ "package KeyedTable",
+ "@annotation public function compilerintrinsic()",
+ "@compilerintrinsic public function keyedTableAdd(int tbl, T key)",
+ " skip",
+ "@compilerintrinsic public function keyedTableContains(int tbl, T key) returns boolean",
+ " return false",
+ "endpackage",
+ "package Test",
+ "import KeyedTable",
+ "init",
+ " keyedTableAdd(1, 7)",
+ " let hit = keyedTableContains(1, 7)",
+ "endpackage")));
+
+ WurstModel model = parseFiles(Collections.emptyList(), inputs, false, compiler);
+ assertNotNull("parse returned null model, errors = " + gui.getErrorList(), model);
+ compiler.checkProg(model);
+ assertTrue("unexpected compile errors: " + gui.getErrorList(), gui.getErrorList().isEmpty());
+ compiler.translateProgToIm(model);
+ compiler.runCompiletime(WurstProjectConfigData.empty(), false, false);
+ compiler.transformProgToLua();
+
+ for (ImFunction f : compiler.getImProg().getFunctions()) {
+ if (!f.isNative()) {
+ continue;
+ }
+ for (ImVar p : f.getParameters()) {
+ assertFalse(f.getName() + " parameter " + p.getName()
+ + " refers to a type variable the stub does not declare",
+ isFreeTypeVar(p.getType(), f));
+ }
+ assertFalse(f.getName() + " return type refers to a type variable the stub does not declare",
+ isFreeTypeVar(f.getReturnType(), f));
+ }
+ }
+
+ private static boolean isFreeTypeVar(ImType t, ImFunction owner) {
+ return t instanceof ImTypeVarRef ref
+ && !owner.getTypeVariables().contains(ref.getTypeVariable());
+ }
+
+ /**
+ * A generic key is the whole point: new generics are erased on Lua rather than cast to int
+ * like the old containers, so the element itself becomes the table key and Lua hashes it
+ * natively. Bodies are trivial because these are Lua-only primitives - callers guard on isLua.
+ */
+ @Test
+ public void keyedTableGenericKeyReachesLuaUncast() throws IOException {
+ test().testLua(true).inline().withStdLib().lines(
+ "package KeyedTable",
+ "@compilerintrinsic public function keyedTableCreate() returns int",
+ " return 0",
+ "@compilerintrinsic public function keyedTableAdd(int tbl, T key)",
+ " skip",
+ "@compilerintrinsic public function keyedTableContains(int tbl, T key) returns boolean",
+ " return false",
+ "endpackage",
+ "package Test",
+ "import KeyedTable",
+ "init",
+ " let t = keyedTableCreate()",
+ " let u = CreateUnit(Player(0), 'hfoo', 0., 0., 0.)",
+ " keyedTableAdd(t, u)",
+ " if keyedTableContains(t, u)",
+ " print(\"present\")",
+ "endpackage");
+
+ String compiled = Files.toString(
+ new File("test-output/lua/LuaTranslationTests_keyedTableGenericKeyReachesLuaUncast.lua"),
+ Charsets.UTF_8);
+
+ assertTrue("a generic key must still lower to the keyed-table stubs",
+ compiled.contains("__wurst_keyedTableAdd"));
+ assertTrue("add is a single store", getFunctionBody(compiled, "__wurst_keyedTableAdd").contains("] = true"));
+
+ // The unit must be handed over as itself. An index round-trip would show up here.
+ String init = getFunctionBody(compiled, "init_Test");
+ assertFalse("the element must not be converted to a class index: " + init,
+ init.contains("__wurst_classFromIndex"));
+ }
+
/**
* KeyedTable source shared by the tests below: the Jass path built on the library's hashtable
* wrapper, which the Lua backend replaces with a table keyed directly by the element.