Skip to content

Commit dfbfeb2

Browse files
authored
Lua Jass shimming (#1183)
Tries to emulate Jass behavior in emitted Lua code.
1 parent eac9e84 commit dfbfeb2

12 files changed

Lines changed: 396 additions & 63 deletions

File tree

de.peeeq.wurstscript/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def j25Launcher = toolchainSvc.launcherFor(java.toolchain)
3939

4040
tasks.withType(JavaExec).configureEach {
4141
javaLauncher.set(j25Launcher)
42+
jvmArgs('-XX:+UnlockExperimentalVMOptions', '-XX:+UseCompactObjectHeaders')
4243
}
4344

4445
tasks.withType(JavaCompile).configureEach { options.release = 25 }
@@ -244,6 +245,8 @@ test {
244245
'-Xmx2g', // local: give it room to finish and dump
245246
'-XX:MaxMetaspaceSize=256m',
246247
'-XX:+HeapDumpOnOutOfMemoryError',
248+
'-XX:+UnlockExperimentalVMOptions', // needed for UseCompactObjectHeaders until it graduates
249+
'-XX:+UseCompactObjectHeaders', // Java 24+: 8-byte headers (vs 16) — big win for AST-heavy workloads
247250
)
248251
}
249252

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -645,12 +645,9 @@ private void beginPhase(int phase, String description) {
645645

646646
private void printDebugImProg(String debugFile) {
647647
if (!errorHandler.isUnitTestMode() || !errorHandler.isOutputTestSource()) {
648-
// output only in unit test mode
649648
return;
650649
}
651-
652650
try {
653-
// TODO remove test output
654651
File file = new File(debugFile);
655652
file.getParentFile().mkdirs();
656653
try (Writer w = Files.newWriter(file, Charsets.UTF_8)) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ public class ErrorHandler {
1919

2020
private final WurstGui gui;
2121
private boolean unitTestMode = false;
22-
public static boolean outputTestSource = true;
22+
/** Write intermediate IM debug files during tests. Off by default — only tests that
23+
* explicitly assert on IM output (e.g. DeterministicChecks) should set this to true. */
24+
public static boolean outputTestSource = false;
2325

2426
public ErrorHandler(WurstGui gui) {
2527
this.gui = gui;

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/HasAnnotation.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
import java.util.HashMap;
99
import java.util.Map;
10+
import java.util.WeakHashMap;
1011

1112
public class HasAnnotation {
12-
// OPTIMIZATION 1: Cache normalized annotations
13+
// OPTIMIZATION 1: Cache normalized annotations (String→String: safe as static, strings are interned/small)
1314
private static final Map<String, String> normalizationCache = new HashMap<>();
1415

1516
@NotNull
@@ -74,11 +75,19 @@ public static Annotation getAnnotation(NameDef e, String annotation) {
7475
return null;
7576
}
7677

77-
// OPTIMIZATION 8: Cache normalized annotation types per Annotation object
78-
private static final Map<Annotation, String> annotationTypeCache = new HashMap<>();
78+
// OPTIMIZATION 8: Cache normalized annotation types per Annotation object.
79+
// WeakHashMap: entries are collected automatically when the AST node (Annotation) is GC'd,
80+
// preventing this static cache from pinning entire compilation trees across tests.
81+
private static final Map<Annotation, String> annotationTypeCache = new WeakHashMap<>();
7982

8083
private static String getNormalizedType(Annotation a) {
8184
return annotationTypeCache.computeIfAbsent(a,
8285
ann -> normalizeAnnotation(ann.getAnnotationType()));
8386
}
87+
88+
/** Explicitly clear both caches. Called from GlobalCaches.clearAll() between tests. */
89+
public static void clearCaches() {
90+
annotationTypeCache.clear();
91+
normalizationCache.clear();
92+
}
8493
}

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

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@
1212
* <p>Three classes of WC3 BJ calls are transformed:
1313
* <ol>
1414
* <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>
15+
* implementation uses a stable table counter for selected opaque runtime handle
16+
* families only. Enum-like handle families keep native semantics in Lua.</li>
1717
* <li><b>Hashtable natives</b> ({@code SaveInteger}, {@code LoadBoolean}, …) and
1818
* <b>context-callback natives</b> ({@code ForForce}, {@code ForGroup}, …) –
1919
* replaced 1:1 by their {@code __wurst_} prefixed equivalents, whose Lua
2020
* implementations are provided by {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.</li>
2121
* <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>
22+
* by a generated IM function that first checks each required handle param for
23+
* {@code null} and returns the type-appropriate default (0 / 0.0 / false / "" / nil),
24+
* then delegates to the original BJ function. This matches Jass behavior, which
25+
* silently returns defaults on null-handle calls instead of crashing.
26+
* {@code boolexpr} and {@code code} typed params are intentionally skipped: these
27+
* are optional/nullable in Jass (e.g. the filter arg of
28+
* {@code TriggerRegisterPlayerUnitEvent}) and passing {@code nil} is valid.</li>
2629
* </ol>
2730
*
2831
* <p>IS_NATIVE stubs added for category 1 and 2 are recognised by
@@ -69,6 +72,26 @@ public final class LuaNativeLowering {
6972
"EnumDestructablesInRect", "GetEnumDestructable"
7073
));
7174

75+
/** True runtime-object handles that should use Lua-side object identity for GetHandleId. */
76+
private static final Set<String> OPAQUE_RUNTIME_HANDLE_TYPES = new HashSet<>(Arrays.asList(
77+
"unit", "item", "destructable", "effect", "lightning", "timer", "trigger",
78+
"triggeraction", "triggercondition", "boolexpr", "force", "group", "location",
79+
"rect", "region", "sound", "dialog", "button", "quest", "questitem",
80+
"leaderboard", "multiboard", "multiboarditem", "trackable", "texttag",
81+
"image", "ubersplat", "framehandle", "fogmodifier", "hashtable"
82+
));
83+
84+
/**
85+
* When {@code true}, only opaque runtime-handle families (unit, item, timer, …)
86+
* are shimmed via {@code __wurst_GetHandleId}; enum-like handle families
87+
* (eventid, playerevent, …) keep native {@code GetHandleId} semantics.
88+
*
89+
* When {@code false} (safe default), ALL {@code GetHandleId} calls are shimmed
90+
* unconditionally — this matches the pre-selective-shim behaviour and avoids
91+
* any desync risk while the selective logic is being validated.
92+
*/
93+
public static final boolean ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING = true;
94+
7295
private LuaNativeLowering() {}
7396

7497
/**
@@ -100,6 +123,7 @@ public static void transform(ImProg prog) {
100123
// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
101124
// Populated lazily during the traversal.
102125
Map<ImFunction, ImFunction> replacements = new LinkedHashMap<>();
126+
Map<String, ImFunction> specialNativeStubs = new LinkedHashMap<>();
103127
// BJ functions that don't need a replacement (not GetHandleId, not hashtable/callback,
104128
// no handle params). Cached to avoid rechecking the same function at every call site.
105129
Set<ImFunction> noReplacement = new HashSet<>();
@@ -114,7 +138,37 @@ public static void transform(ImProg prog) {
114138
public void visit(ImFunctionCall call) {
115139
super.visit(call);
116140
ImFunction f = call.getFunc();
141+
if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && isCompatGetHandleIdFunction(f)) {
142+
if (shouldRewriteGetHandleId(call)) {
143+
ImFunction replacement = specialNativeStubs.computeIfAbsent("__wurst_GetHandleId",
144+
name -> createNativeStub(name, f));
145+
if (!deferredAdditions.contains(replacement)) {
146+
deferredAdditions.add(replacement);
147+
}
148+
call.replaceBy(JassIm.ImFunctionCall(
149+
call.attrTrace(), replacement,
150+
JassIm.ImTypeArguments(),
151+
call.getArguments().copy(),
152+
false, CallType.NORMAL));
153+
}
154+
return;
155+
}
117156
if (!f.isBj()) return;
157+
if ("GetHandleId".equals(f.getName())) {
158+
if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && shouldRewriteGetHandleId(call)) {
159+
ImFunction replacement = specialNativeStubs.computeIfAbsent("__wurst_GetHandleId",
160+
name -> createNativeStub(name, f));
161+
if (!deferredAdditions.contains(replacement)) {
162+
deferredAdditions.add(replacement);
163+
}
164+
call.replaceBy(JassIm.ImFunctionCall(
165+
call.attrTrace(), replacement,
166+
JassIm.ImTypeArguments(),
167+
call.getArguments().copy(),
168+
false, CallType.NORMAL));
169+
}
170+
return;
171+
}
118172
if (noReplacement.contains(f)) return;
119173

120174
if (!replacements.containsKey(f)) {
@@ -139,9 +193,7 @@ public void visit(ImFunctionCall call) {
139193

140194
private ImFunction computeReplacement(ImFunction bj) {
141195
String name = bj.getName();
142-
if ("GetHandleId".equals(name)) {
143-
return createNativeStub("__wurst_GetHandleId", bj);
144-
} else if (HASHTABLE_NATIVE_NAMES.contains(name)) {
196+
if (HASHTABLE_NATIVE_NAMES.contains(name)) {
145197
return createNativeStub("__wurst_" + name, bj);
146198
} else if (CONTEXT_CALLBACK_NATIVE_NAMES.contains(name)) {
147199
return createNativeStub("__wurst_" + name, bj);
@@ -195,10 +247,12 @@ private static ImFunction createNilSafeWrapper(ImFunction bjNative) {
195247

196248
ImStmts body = JassIm.ImStmts();
197249

198-
// Null-check each handle param: if param == null then return <default> end
250+
// Null-check each required handle param: if param == null then return <default> end
251+
// boolexpr and code params are intentionally skipped — they are optional/nullable
252+
// in Jass (e.g. the filter arg of TriggerRegisterPlayerUnitEvent).
199253
ImExpr returnDefault = defaultValueExpr(bjNative.getReturnType());
200254
for (ImVar param : paramVars) {
201-
if (isHandleType(param.getType())) {
255+
if (isHandleType(param.getType()) && !isNullableHandleType(param.getType())) {
202256
ImExpr condition = JassIm.ImOperatorCall(WurstOperator.EQ, JassIm.ImExprs(
203257
JassIm.ImVarAccess(param),
204258
JassIm.ImNull(param.getType().copy())
@@ -244,14 +298,56 @@ private static boolean hasHandleParam(ImFunction f) {
244298
}
245299

246300
/** Returns true for WC3 handle types (ImSimpleType that is not int/real/boolean/string). */
247-
static boolean isHandleType(ImType type) {
301+
public static boolean isHandleType(ImType type) {
248302
if (!(type instanceof ImSimpleType)) {
249303
return false;
250304
}
251305
String n = ((ImSimpleType) type).getTypename();
252306
return !n.equals("integer") && !n.equals("real") && !n.equals("boolean") && !n.equals("string");
253307
}
254308

309+
/**
310+
* Returns true for handle types that are valid to pass as {@code null} in Jass without
311+
* triggering a null-handle crash. These params are skipped in nil-safety wrappers.
312+
*
313+
* <p>{@code boolexpr} and {@code code} are the canonical optional types: every WC3
314+
* API that takes them (filter, condition, action callbacks) accepts {@code null} to
315+
* mean "no callback".
316+
*/
317+
static boolean isNullableHandleType(ImType type) {
318+
if (!(type instanceof ImSimpleType)) {
319+
return false;
320+
}
321+
String n = ((ImSimpleType) type).getTypename();
322+
return n.equals("boolexpr") || n.equals("code");
323+
}
324+
325+
private static boolean shouldRewriteGetHandleId(ImFunctionCall call) {
326+
if (call.getArguments().size() != 1) {
327+
return true;
328+
}
329+
return usesLuaObjectIdentityHandleId(call.getArguments().get(0).attrTyp());
330+
}
331+
332+
public static boolean usesLuaObjectIdentityHandleId(ImType type) {
333+
if (!(type instanceof ImSimpleType)) {
334+
return false;
335+
}
336+
String typeName = ((ImSimpleType) type).getTypename();
337+
return OPAQUE_RUNTIME_HANDLE_TYPES.contains(typeName);
338+
}
339+
340+
private static boolean isCompatGetHandleIdFunction(ImFunction f) {
341+
if (f.getParameters().size() != 1
342+
|| !f.getName().endsWith("_getHandleId")
343+
|| f.getName().endsWith("_getTCHandleId")) {
344+
return false;
345+
}
346+
// Restrict to WC3 simple handle types (ImSimpleType). User-defined Wurst classes
347+
// use ImClassType and must not have their call sites replaced.
348+
return isHandleType(f.getParameters().get(0).getType());
349+
}
350+
255351
/** Returns an IM expression representing the safe default for the given return type. */
256352
private static ImExpr defaultValueExpr(ImType returnType) {
257353
if (returnType instanceof ImSimpleType) {

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ public class LuaAssertions {
1616
private LuaAssertions() {}
1717

1818
/**
19-
* Asserts that {@code luaCode} contains no raw call to {@code GetHandleId}.
19+
* Asserts that every emitted call to {@code __wurst_GetHandleId} has a helper definition.
2020
*
21-
* In Lua mode handle IDs can desync, so all uses of {@code GetHandleId} must
22-
* be rewritten to {@code __wurst_GetHandleId} which uses a stable table-based
23-
* counter instead.
21+
* The Lua backend now rewrites only selected opaque runtime-handle families and
22+
* intentionally leaves enum-like handle families on native {@code GetHandleId}.
2423
*/
2524
public static void assertNoLeakedGetHandleIdCalls(String luaCode) {
2625
Set<String> called = collectCalledFunctionNames(luaCode);
27-
if (called.contains("GetHandleId")) {
26+
Set<String> defined = collectDefinedFunctionNames(luaCode);
27+
if (called.contains("__wurst_GetHandleId") && !defined.contains("__wurst_GetHandleId")) {
2828
throw new RuntimeException(
29-
"Wurst Lua backend assertion failed: raw GetHandleId() call found in generated Lua. "
30-
+ "Use the __wurst_GetHandleId polyfill (table-based) instead to avoid desync.");
29+
"Wurst Lua backend assertion failed: __wurst_GetHandleId() call found in generated Lua "
30+
+ "without a matching helper definition.");
3131
}
3232
}
3333

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public class LuaNatives {
3535
"LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle",
3636
"LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle"
3737
};
38-
3938
static {
4039
addNative("testSuccess", f -> {
4140
f.getBody().add(LuaAst.LuaLiteral("print(\"testSuccess\")"));

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

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@
77
/**
88
* Builds and registers the Wurst Lua infrastructure functions that are always
99
* emitted into the generated script, regardless of what user code looks like.
10-
*
11-
* These include object/string index maps, ensure-type coercers, array defaults,
12-
* hashtable helpers, and context-callback wrappers.
13-
*
14-
* All methods are static and take the active {@link LuaTranslator} as the first
15-
* argument, following the same convention as {@link ExprTranslation}.
1610
*/
1711
class LuaPolyfillSetup {
1812

@@ -55,29 +49,22 @@ static void createStringConcatFunction(LuaTranslator tr) {
5549
}
5650

5751
static void createInstanceOfFunction(LuaTranslator tr) {
58-
String[] code = {
59-
"return x ~= nil and x." + WURST_SUPERTYPES + "[A]"
60-
};
61-
6252
tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
6353
tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("A", LuaAst.LuaNoExpr()));
64-
for (String c : code) {
65-
tr.instanceOfFunction.getBody().add(LuaAst.LuaLiteral(c));
66-
}
54+
tr.instanceOfFunction.getBody().add(LuaAst.LuaLiteral("return x ~= nil and x." + WURST_SUPERTYPES + "[A]"));
6755
tr.luaModel.add(tr.instanceOfFunction);
6856
}
6957

7058
static void createObjectIndexFunctions(LuaTranslator tr) {
71-
String vName = "__wurst_objectIndexMap";
72-
LuaVariable v = LuaAst.LuaVariable(vName, LuaAst.LuaExprNull());
73-
tr.luaModel.add(v);
74-
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(v), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
59+
LuaVariable objectIndexMap = LuaAst.LuaVariable("__wurst_objectIndexMap", LuaAst.LuaExprNull());
60+
tr.luaModel.add(objectIndexMap);
61+
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(objectIndexMap), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
7562
LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0"))
7663
))));
7764

78-
LuaVariable im = LuaAst.LuaVariable("__wurst_number_wrapper_map", LuaAst.LuaExprNull());
79-
tr.luaModel.add(im);
80-
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(im), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
65+
LuaVariable numberWrapperMap = LuaAst.LuaVariable("__wurst_number_wrapper_map", LuaAst.LuaExprNull());
66+
tr.luaModel.add(numberWrapperMap);
67+
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(numberWrapperMap), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
8168
LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0"))
8269
))));
8370

@@ -217,5 +204,4 @@ static void createEnsureTypeFunctions(LuaTranslator tr) {
217204
tr.ensureStrFunction.getBody().add(LuaAst.LuaLiteral("return tostring(x)"));
218205
tr.luaModel.add(tr.ensureStrFunction);
219206
}
220-
221207
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum;
88
import de.peeeq.wurstscript.translation.imtranslation.GetAForB;
99
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
10+
import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering;
1011
import de.peeeq.wurstscript.types.TypesHelper;
1112
import de.peeeq.wurstscript.utils.Lazy;
1213
import de.peeeq.wurstscript.utils.Utils;
@@ -461,6 +462,10 @@ private void translateFunc(ImFunction f) {
461462
if (f.isNative()) {
462463
LuaNatives.get(lf);
463464
} else {
465+
if (LuaNativeLowering.ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && rewriteGetHandleIdCompatFunction(f, lf)) {
466+
luaModel.add(lf);
467+
return;
468+
}
464469
if (rewriteTypeCastingCompatFunction(f, lf)) {
465470
luaModel.add(lf);
466471
return;
@@ -510,6 +515,26 @@ private void translateFunc(ImFunction f) {
510515
}
511516
}
512517

518+
private boolean rewriteGetHandleIdCompatFunction(ImFunction f, LuaFunction lf) {
519+
if (f.getParameters().size() != 1 || !f.getName().endsWith("_getHandleId") || f.getName().endsWith("_getTCHandleId")) {
520+
return false;
521+
}
522+
ImVar firstParam = f.getParameters().get(0);
523+
// Restrict to WC3 simple handle types. User-defined Wurst classes use ImClassType
524+
// and must not have their function body replaced.
525+
if (!LuaNativeLowering.isHandleType(firstParam.getType())) {
526+
return false;
527+
}
528+
LuaExpr arg = LuaAst.LuaExprVarAccess(luaVar.getFor(firstParam));
529+
// Only called when ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING is true.
530+
// Shim opaque runtime handles; keep native GetHandleId for enum-like handles.
531+
String targetFunction = LuaNativeLowering.usesLuaObjectIdentityHandleId(firstParam.getType())
532+
? "__wurst_GetHandleId" : "GetHandleId";
533+
lf.getBody().clear();
534+
lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCallByName(targetFunction, LuaAst.LuaExprlist(arg))));
535+
return true;
536+
}
537+
513538
private boolean rewriteTypeCastingCompatFunction(ImFunction f, LuaFunction lf) {
514539
if (f.getParameters().isEmpty()) {
515540
return false;

0 commit comments

Comments
 (0)