Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
* <p>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 <T>} 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;
Comment thread
Frotty marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>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<CU> inputs = new ArrayList<>();
inputs.add(new CU("nativeStubsCarryNoFreeTypeVariables.wurst", String.join(System.lineSeparator(),
"package KeyedTable",
"@annotation public function compilerintrinsic()",
"@compilerintrinsic public function keyedTableAdd<T:>(int tbl, T key)",
" skip",
"@compilerintrinsic public function keyedTableContains<T:>(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 <T> 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<T:>(int tbl, T key)",
" skip",
"@compilerintrinsic public function keyedTableContains<T:>(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.
Expand Down
Loading