Skip to content

Commit 732b63c

Browse files
authored
lua cleanup (#1181)
1 parent 014f2e2 commit 732b63c

15 files changed

Lines changed: 1579 additions & 437 deletions

File tree

Lines changed: 446 additions & 0 deletions
Large diffs are not rendered by default.

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,13 @@ public LuaCompilationUnit transformProgToLua() {
889889
}
890890
ImTranslator imTranslator2 = getImTranslator();
891891
ImOptimizer optimizer = new ImOptimizer(timeTaker, imTranslator2);
892+
893+
// Lower Lua-specific native calls into IM-level wrappers before optimization,
894+
// so the optimizer can inline and eliminate the nil-safety checks and remapped stubs.
895+
beginPhase(4, "lua native lowering");
896+
LuaNativeLowering.transform(imProg);
897+
timeTaker.endPhase();
898+
892899
// inliner
893900
stage = 5;
894901
if (runArgs.isInline()) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ProjectConfigBuilder.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,14 @@ public static MapRequest.CompilationResult apply(WurstProjectConfigData projectC
8585
// Prefer the previously-injected script (with correct config() body) over the
8686
// raw map script. If it doesn't exist yet (e.g. first Lua build after a JASS-only
8787
// cache), fall through to re-inject so the config() body is never stale.
88+
// Also re-inject if war3map.j was modified after the cached script was written.
8889
File cachedInjectedScript = new File(buildDir, outputScriptName);
89-
if (cachedInjectedScript.exists()) {
90+
boolean cachedScriptStale = !cachedInjectedScript.exists()
91+
|| mapScript.lastModified() > cachedInjectedScript.lastModified();
92+
if (!cachedScriptStale) {
9093
result.script = cachedInjectedScript;
9194
} else if (StringUtils.isNotBlank(buildMapData.getName())) {
92-
WLogger.info("Cached injected script missing, re-injecting config");
95+
WLogger.info("war3map.j changed or cached script missing, re-injecting config");
9396
applyBuildMapData(projectConfig, mapScript, buildDir, w3data, w3I, result, configHash, outputScriptName);
9497
}
9598
// else result.script stays as mapScript (no wurst.build name configured)

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ protected File compileMap(File projectFolder, WurstGui gui, Optional<File> mapCo
169169

170170
String compiledMapScript = sb.toString();
171171
LuaTranslator.assertNoLeakedHashtableNativeCalls(compiledMapScript);
172+
LuaTranslator.assertNoLeakedGetHandleIdCalls(compiledMapScript);
172173
File buildDir = getBuildDir();
173174
File outFile = new File(buildDir, BUILD_COMPILED_LUA_NAME);
174175
Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile);

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
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package de.peeeq.wurstscript.translation.imtranslation;
2+
3+
import de.peeeq.wurstscript.WurstOperator;
4+
import de.peeeq.wurstscript.jassIm.*;
5+
6+
import java.util.*;
7+
8+
/**
9+
* IM-level lowering pass for the Lua backend, run before optimization so the
10+
* optimizer can inline and eliminate the generated wrappers.
11+
*
12+
* <p>Three classes of WC3 BJ calls are transformed:
13+
* <ol>
14+
* <li><b>GetHandleId</b> – replaced 1:1 by {@code __wurst_GetHandleId}, whose Lua
15+
* implementation uses a stable table counter instead of the WC3 handle ID
16+
* (which can desync in Lua mode).</li>
17+
* <li><b>Hashtable natives</b> ({@code SaveInteger}, {@code LoadBoolean}, …) and
18+
* <b>context-callback natives</b> ({@code ForForce}, {@code ForGroup}, …) –
19+
* replaced 1:1 by their {@code __wurst_} prefixed equivalents, whose Lua
20+
* implementations are provided by {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.</li>
21+
* <li><b>All other BJ calls with at least one handle-typed parameter</b> – wrapped
22+
* by a generated IM function that first checks each handle param for {@code null}
23+
* and returns the type-appropriate default (0 / 0.0 / false / "" / nil), then
24+
* delegates to the original BJ function. This matches Jass behavior, which
25+
* silently returns defaults on null-handle calls instead of crashing.</li>
26+
* </ol>
27+
*
28+
* <p>IS_NATIVE stubs added for category 1 and 2 are recognised by
29+
* {@link de.peeeq.wurstscript.translation.lua.translation.LuaTranslator#translateFunc} as
30+
* Wurst-owned natives and filled in by
31+
* {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.
32+
*/
33+
public final class LuaNativeLowering {
34+
35+
/** Hashtable native names that need to be remapped to {@code __wurst_} equivalents. */
36+
private static final Set<String> HASHTABLE_NATIVE_NAMES = new HashSet<>(Arrays.asList(
37+
"InitHashtable",
38+
"SaveInteger", "SaveBoolean", "SaveReal", "SaveStr",
39+
"LoadInteger", "LoadBoolean", "LoadReal", "LoadStr",
40+
"HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", "HaveSavedHandle",
41+
"FlushChildHashtable", "FlushParentHashtable",
42+
"RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString", "RemoveSavedHandle",
43+
// Handle-typed save/load variants
44+
"SavePlayerHandle", "SaveWidgetHandle", "SaveDestructableHandle", "SaveItemHandle", "SaveUnitHandle",
45+
"SaveAbilityHandle", "SaveTimerHandle", "SaveTriggerHandle", "SaveTriggerConditionHandle",
46+
"SaveTriggerActionHandle", "SaveTriggerEventHandle", "SaveForceHandle", "SaveGroupHandle",
47+
"SaveLocationHandle", "SaveRectHandle", "SaveBooleanExprHandle", "SaveSoundHandle", "SaveEffectHandle",
48+
"SaveUnitPoolHandle", "SaveItemPoolHandle", "SaveQuestHandle", "SaveQuestItemHandle",
49+
"SaveDefeatConditionHandle", "SaveTimerDialogHandle", "SaveLeaderboardHandle", "SaveMultiboardHandle",
50+
"SaveMultiboardItemHandle", "SaveTrackableHandle", "SaveDialogHandle", "SaveButtonHandle",
51+
"SaveTextTagHandle", "SaveLightningHandle", "SaveImageHandle", "SaveUbersplatHandle", "SaveRegionHandle",
52+
"SaveFogStateHandle", "SaveFogModifierHandle", "SaveAgentHandle", "SaveHashtableHandle", "SaveFrameHandle",
53+
"LoadPlayerHandle", "LoadWidgetHandle", "LoadDestructableHandle", "LoadItemHandle", "LoadUnitHandle",
54+
"LoadAbilityHandle", "LoadTimerHandle", "LoadTriggerHandle", "LoadTriggerConditionHandle",
55+
"LoadTriggerActionHandle", "LoadTriggerEventHandle", "LoadForceHandle", "LoadGroupHandle",
56+
"LoadLocationHandle", "LoadRectHandle", "LoadBooleanExprHandle", "LoadSoundHandle", "LoadEffectHandle",
57+
"LoadUnitPoolHandle", "LoadItemPoolHandle", "LoadQuestHandle", "LoadQuestItemHandle",
58+
"LoadDefeatConditionHandle", "LoadTimerDialogHandle", "LoadLeaderboardHandle", "LoadMultiboardHandle",
59+
"LoadMultiboardItemHandle", "LoadTrackableHandle", "LoadDialogHandle", "LoadButtonHandle",
60+
"LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle",
61+
"LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle"
62+
));
63+
64+
/** Context-callback natives that need to be remapped to {@code __wurst_} equivalents. */
65+
private static final Set<String> CONTEXT_CALLBACK_NATIVE_NAMES = new HashSet<>(Arrays.asList(
66+
"ForForce", "GetEnumPlayer",
67+
"ForGroup", "GetEnumUnit",
68+
"EnumItemsInRect", "GetEnumItem",
69+
"EnumDestructablesInRect", "GetEnumDestructable"
70+
));
71+
72+
private LuaNativeLowering() {}
73+
74+
/**
75+
* Transforms the IM program in place.
76+
*
77+
* <p>Must be called <em>before</em> the optimizer so that the optimizer
78+
* can inline and eliminate the generated wrappers.
79+
*
80+
* <p>Stubs and wrappers are created lazily (on first call-site encounter) and added
81+
* to prog only after the traversal completes. This avoids the memory cost of
82+
* creating wrappers for every BJ function in the IM (common.j declares hundreds of
83+
* functions, most of which are unreachable in any given program).
84+
*/
85+
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+
100+
// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
101+
// Populated lazily during the traversal.
102+
Map<ImFunction, ImFunction> replacements = new LinkedHashMap<>();
103+
// BJ functions that don't need a replacement (not GetHandleId, not hashtable/callback,
104+
// no handle params). Cached to avoid rechecking the same function at every call site.
105+
Set<ImFunction> noReplacement = new HashSet<>();
106+
// All generated functions (stubs and wrappers) are deferred until after the traversal:
107+
// - Stubs: deferred so ConcurrentModificationException is avoided on prog.getFunctions()
108+
// - Wrappers: deferred so the visitor doesn't see their internal BJ delegate calls and
109+
// recursively wrap them, which would cause infinite wrapping.
110+
List<ImFunction> deferredAdditions = new ArrayList<>();
111+
112+
prog.accept(new Element.DefaultVisitor() {
113+
@Override
114+
public void visit(ImFunctionCall call) {
115+
super.visit(call);
116+
ImFunction f = call.getFunc();
117+
if (!f.isBj()) return;
118+
if (noReplacement.contains(f)) return;
119+
120+
if (!replacements.containsKey(f)) {
121+
ImFunction r = computeReplacement(f);
122+
if (r != null) {
123+
replacements.put(f, r);
124+
deferredAdditions.add(r);
125+
} else {
126+
noReplacement.add(f);
127+
}
128+
}
129+
ImFunction replacement = replacements.get(f);
130+
131+
if (replacement != null) {
132+
call.replaceBy(JassIm.ImFunctionCall(
133+
call.attrTrace(), replacement,
134+
JassIm.ImTypeArguments(),
135+
call.getArguments().copy(),
136+
false, CallType.NORMAL));
137+
}
138+
}
139+
140+
private ImFunction computeReplacement(ImFunction bj) {
141+
String name = bj.getName();
142+
if ("GetHandleId".equals(name)) {
143+
return createNativeStub("__wurst_GetHandleId", bj);
144+
} else if (HASHTABLE_NATIVE_NAMES.contains(name)) {
145+
return createNativeStub("__wurst_" + name, bj);
146+
} else if (CONTEXT_CALLBACK_NATIVE_NAMES.contains(name)) {
147+
return createNativeStub("__wurst_" + name, bj);
148+
} else if (hasHandleParam(bj)) {
149+
return createNilSafeWrapper(bj);
150+
}
151+
return null;
152+
}
153+
});
154+
155+
// Add all generated functions after the traversal so their bodies are not visited
156+
// by the replacement visitor above.
157+
prog.getFunctions().addAll(deferredAdditions);
158+
}
159+
160+
/**
161+
* Creates a new IS_NATIVE (non-BJ) IM function stub with the same signature as
162+
* {@code original}. The Lua translator will fill in the body via
163+
* {@code LuaNatives.get()} when it encounters the stub.
164+
*
165+
* <p>The caller is responsible for adding the stub to prog.getFunctions().
166+
*/
167+
private static ImFunction createNativeStub(String name, ImFunction original) {
168+
ImVars params = JassIm.ImVars();
169+
for (ImVar p : original.getParameters()) {
170+
params.add(JassIm.ImVar(p.attrTrace(), p.getType().copy(), p.getName(), false));
171+
}
172+
return JassIm.ImFunction(
173+
original.attrTrace(), name,
174+
JassIm.ImTypeVars(), params,
175+
original.getReturnType().copy(),
176+
JassIm.ImVars(), JassIm.ImStmts(),
177+
Collections.singletonList(FunctionFlagEnum.IS_NATIVE));
178+
}
179+
180+
/**
181+
* Creates a nil-safety wrapper for {@code bjNative}.
182+
*
183+
* <p>The generated function checks each handle-typed parameter against
184+
* {@code null} and returns the type-appropriate default value if any is
185+
* null. Otherwise it delegates to the original BJ function.
186+
*/
187+
private static ImFunction createNilSafeWrapper(ImFunction bjNative) {
188+
ImVars params = JassIm.ImVars();
189+
List<ImVar> paramVars = new ArrayList<>();
190+
for (ImVar p : bjNative.getParameters()) {
191+
ImVar copy = JassIm.ImVar(p.attrTrace(), p.getType().copy(), p.getName(), false);
192+
params.add(copy);
193+
paramVars.add(copy);
194+
}
195+
196+
ImStmts body = JassIm.ImStmts();
197+
198+
// Null-check each handle param: if param == null then return <default> end
199+
ImExpr returnDefault = defaultValueExpr(bjNative.getReturnType());
200+
for (ImVar param : paramVars) {
201+
if (isHandleType(param.getType())) {
202+
ImExpr condition = JassIm.ImOperatorCall(WurstOperator.EQ, JassIm.ImExprs(
203+
JassIm.ImVarAccess(param),
204+
JassIm.ImNull(param.getType().copy())
205+
));
206+
ImStmts thenBlock = JassIm.ImStmts(
207+
JassIm.ImReturn(bjNative.attrTrace(), returnDefault.copy())
208+
);
209+
body.add(JassIm.ImIf(bjNative.attrTrace(), condition, thenBlock, JassIm.ImStmts()));
210+
}
211+
}
212+
213+
// Delegate to the original BJ native
214+
ImExprs callArgs = JassIm.ImExprs();
215+
for (ImVar pv : paramVars) {
216+
callArgs.add(JassIm.ImVarAccess(pv));
217+
}
218+
ImFunctionCall delegate = JassIm.ImFunctionCall(
219+
bjNative.attrTrace(), bjNative,
220+
JassIm.ImTypeArguments(), callArgs, false, CallType.NORMAL);
221+
222+
if (bjNative.getReturnType() instanceof ImVoid) {
223+
body.add(delegate);
224+
} else {
225+
body.add(JassIm.ImReturn(bjNative.attrTrace(), delegate));
226+
}
227+
228+
return JassIm.ImFunction(
229+
bjNative.attrTrace(),
230+
"__wurst_safe_" + bjNative.getName(),
231+
JassIm.ImTypeVars(), params,
232+
bjNative.getReturnType().copy(),
233+
JassIm.ImVars(), body,
234+
Collections.emptyList());
235+
}
236+
237+
private static boolean hasHandleParam(ImFunction f) {
238+
for (ImVar p : f.getParameters()) {
239+
if (isHandleType(p.getType())) {
240+
return true;
241+
}
242+
}
243+
return false;
244+
}
245+
246+
/** Returns true for WC3 handle types (ImSimpleType that is not int/real/boolean/string). */
247+
static boolean isHandleType(ImType type) {
248+
if (!(type instanceof ImSimpleType)) {
249+
return false;
250+
}
251+
String n = ((ImSimpleType) type).getTypename();
252+
return !n.equals("integer") && !n.equals("real") && !n.equals("boolean") && !n.equals("string");
253+
}
254+
255+
/** Returns an IM expression representing the safe default for the given return type. */
256+
private static ImExpr defaultValueExpr(ImType returnType) {
257+
if (returnType instanceof ImSimpleType) {
258+
String n = ((ImSimpleType) returnType).getTypename();
259+
switch (n) {
260+
case "integer": return JassIm.ImIntVal(0);
261+
case "real": return JassIm.ImRealVal("0.0");
262+
case "boolean": return JassIm.ImBoolVal(false);
263+
case "string": return JassIm.ImStringVal("");
264+
}
265+
}
266+
// void or handle type → null
267+
if (returnType instanceof ImVoid) {
268+
return JassIm.ImNull(JassIm.ImVoid());
269+
}
270+
return JassIm.ImNull(returnType.copy());
271+
}
272+
}

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()));

0 commit comments

Comments
 (0)