Skip to content

Commit 3adc5ce

Browse files
committed
isLua magic constant
1 parent 937183e commit 3adc5ce

5 files changed

Lines changed: 85 additions & 5 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstKeywords.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class WurstKeywords {
88
"tuple", "div", "mod", "let", "from", "to", "downto", "step", "endpackage", "skip", "true", "false", "var", "instanceof",
99
"super", "enum", "switch", "case", "default", "typeId", "begin", "end",
1010
// not really a keyword, but it should feel like one:
11-
"compiletime",
11+
"compiletime", "isLua",
1212
// jurst keywords, maybe split the highlighters later...:
1313
"library", "endlibrary", "scope", "endscope", "requires", "uses", "needs", "struct", "endstruct",
1414
"then", "endif", "loop", "exitwhen", "endloop", "method", "takes", "endmethod", "set", "call",

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import java.util.List;
1414
import java.util.Optional;
1515
import java.util.Set;
16-
import java.util.stream.Collectors;
1716

1817
public class GlobalsInliner implements OptimizerPass {
1918
public int optimize(ImTranslator trans) {
@@ -28,6 +27,21 @@ public int optimize(ImTranslator trans) {
2827
// so it is important, that we do not optimize away the compiletime constant
2928
continue;
3029
}
30+
if (v.getName().equals("MagicFunctions_isLua") && trans.isLuaTarget()) {
31+
// In Lua mode, isLua must evaluate to true.
32+
// Normal inlining would use the declared value (false); override it here.
33+
for (ImVarRead read : new ArrayList<>(v.attrReads())) {
34+
read.replaceBy(JassIm.ImBoolVal(true));
35+
}
36+
for (ImVarWrite write : new ArrayList<>(v.attrWrites())) {
37+
if (write.getParent() != null) {
38+
write.replaceBy(ImHelper.nullExpr());
39+
}
40+
}
41+
obsoleteVars.add(v);
42+
obsoleteCount++;
43+
continue;
44+
}
3145
if (v.getType() instanceof ImArrayType
3246
|| v.getType() instanceof ImArrayTypeMulti) {
3347
// cannot optimize arrays yet

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ private LuaNativeLowering() {}
8383
* functions, most of which are unreachable in any given program).
8484
*/
8585
public static void transform(ImProg prog) {
86+
// Replace all reads of MagicFunctions_isLua with true.
87+
// This must happen before any optimizer passes so that dead-code elimination
88+
// can remove Jass-only branches at compile time.
89+
// We use attrReads() (not a visitor) to target only rvalue uses, avoiding
90+
// ClassCastException when the same ImVarAccess appears as a write target (lvalue).
91+
for (ImVar global : prog.getGlobals()) {
92+
if ("MagicFunctions_isLua".equals(global.getName())) {
93+
for (ImVarRead read : new ArrayList<>(global.attrReads())) {
94+
read.replaceBy(JassIm.ImBoolVal(true));
95+
}
96+
break;
97+
}
98+
}
99+
86100
// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
87101
// Populated lazily during the traversal.
88102
Map<ImFunction, ImFunction> replacements = new LinkedHashMap<>();

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,20 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
116116
}
117117

118118
LuaFunction f = tr.luaFunc.getFor(e.getFunc());
119-
if ("I2S".equals(f.getName()) && isIntentionalThreadAbortCall(e)) {
119+
// Use the immutable ImFunction name rather than f.getName(), because f is a cached
120+
// LuaFunction object shared across all call sites of this native. The setName() calls
121+
// below mutate it, so f.getName() changes after the first translation and can no longer
122+
// be relied upon for sentinel checks.
123+
String imFuncName = e.getFunc().getName();
124+
if ("I2S".equals(imFuncName) && isIntentionalThreadAbortCall(e)) {
120125
return LuaAst.LuaExprFunctionCallByName("error", LuaAst.LuaExprlist(
121126
LuaAst.LuaExprStringVal(WURST_ABORT_THREAD_SENTINEL),
122127
LuaAst.LuaExprIntVal("0")
123128
));
124129
}
125-
if (f.getName().equals(ImTranslator.$DEBUG_PRINT)) {
130+
if (ImTranslator.$DEBUG_PRINT.equals(imFuncName)) {
126131
f.setName("BJDebugMsg");
127-
} else if (f.getName().equals("I2S")) {
132+
} else if ("I2S".equals(imFuncName)) {
128133
f.setName("tostring");
129134
}
130135
return LuaAst.LuaExprFunctionCall(f, tr.translateExprList(e.getArguments()));

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,31 @@ public void i2sDivisionByZeroCrashTrapUsesAbortSentinelInLua() throws IOExceptio
21242124
assertDoesNotContainRegex(compiled, "error\\s*\\(\\s*\"[^\"]*divide by zero[^\"]*\"");
21252125
}
21262126

2127+
@Test
2128+
public void i2sAbortSentinelNotBrokenByEarlierI2SCallInLua() throws IOException {
2129+
// Regression: ExprTranslation used f.setName("tostring") to rename the shared
2130+
// LuaFunction for I2S. After the first ordinary I2S(x) call was translated, f.getName()
2131+
// returned "tostring", so the sentinel check "I2S".equals(f.getName()) silently failed
2132+
// for any later I2S(1/0) abort call — leaving actual //0 division in the Lua output.
2133+
// This test forces a normal I2S call before the abort to expose the mutation bug.
2134+
test().testLua(true).withStdLib().lines(
2135+
"package Test",
2136+
"import MagicFunctions",
2137+
"import ErrorHandling",
2138+
"init",
2139+
" let x = 42",
2140+
" print(I2S(x))", // normal I2S call first — used to corrupt the cached LuaFunction
2141+
" error(\"test\")" // triggers I2S(1 div 0) inside ErrorHandling.error()
2142+
);
2143+
String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_i2sAbortSentinelNotBrokenByEarlierI2SCallInLua.lua"), Charsets.UTF_8);
2144+
// The abort must be the sentinel, not a raw //0 that Lua would crash on at runtime
2145+
assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*//\\s*0\\s*\\)");
2146+
assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*/\\s*0\\s*\\)");
2147+
assertTrue(compiled.contains("__wurst_abort_thread"));
2148+
// tostring() must still be used for the normal I2S call
2149+
assertContainsRegex(compiled, "tostring\\s*\\(");
2150+
}
2151+
21272152
@Test
21282153
public void luaErrorWrapperIgnoresAbortSentinel() throws IOException {
21292154
test().testLua(true).withStdLib().lines(
@@ -2290,4 +2315,26 @@ public void wurstInternalNativesDoNotGetNilSafetyWrapper() throws IOException {
22902315
assertDoesNotContainRegex(compiled, "__wurst_safe___wurst_");
22912316
}
22922317

2318+
@Test
2319+
public void isLuaMagicConstantIsTrueInLuaMode() throws IOException {
2320+
// isLua must be inlined to true in Lua mode by GlobalsInliner,
2321+
// so backend-specific code paths can be selected at compile time.
2322+
test().testLua(true).withStdLib().lines(
2323+
"package Test",
2324+
"import MagicFunctions",
2325+
"init",
2326+
" if isLua",
2327+
" print(\"lua-path\")",
2328+
" else",
2329+
" print(\"jass-path\")"
2330+
);
2331+
String compiled = Files.toString(
2332+
new File("test-output/lua/LuaTranslationTests_isLuaMagicConstantIsTrueInLuaMode.lua"),
2333+
Charsets.UTF_8);
2334+
// After inlining, the raw isLua variable must not appear in Lua output
2335+
assertFalse("MagicFunctions_isLua must be inlined away in Lua mode", compiled.contains("MagicFunctions_isLua"));
2336+
// The lua-path branch must be preserved
2337+
assertTrue("lua-path branch must be present", compiled.contains("lua-path"));
2338+
}
2339+
22932340
}

0 commit comments

Comments
 (0)