Skip to content

Commit e9c344e

Browse files
committed
lua shimming
1 parent eac9e84 commit e9c344e

6 files changed

Lines changed: 338 additions & 54 deletions

File tree

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

Lines changed: 84 additions & 11 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>
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>
21+
* <li><b>Non-native blizzard.j BJ functions with at least one handle-typed parameter</b>
22+
* – wrapped by a generated IM function that first checks each 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+
* Common.j natives are intentionally excluded: they either accept {@code nil} natively
27+
* (e.g. {@code TriggerRegisterPlayerUnitEvent} with a null filter) or are already
28+
* covered by categories 1 and 2 above.</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,13 +193,11 @@ 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);
148-
} else if (hasHandleParam(bj)) {
200+
} else if (hasHandleParam(bj) && !bj.isNative()) {
149201
return createNilSafeWrapper(bj);
150202
}
151203
return null;
@@ -252,6 +304,27 @@ static boolean isHandleType(ImType type) {
252304
return !n.equals("integer") && !n.equals("real") && !n.equals("boolean") && !n.equals("string");
253305
}
254306

307+
private static boolean shouldRewriteGetHandleId(ImFunctionCall call) {
308+
if (call.getArguments().size() != 1) {
309+
return true;
310+
}
311+
return usesLuaObjectIdentityHandleId(call.getArguments().get(0).attrTyp());
312+
}
313+
314+
public static boolean usesLuaObjectIdentityHandleId(ImType type) {
315+
if (!(type instanceof ImSimpleType)) {
316+
return false;
317+
}
318+
String typeName = ((ImSimpleType) type).getTypename();
319+
return OPAQUE_RUNTIME_HANDLE_TYPES.contains(typeName);
320+
}
321+
322+
private static boolean isCompatGetHandleIdFunction(ImFunction f) {
323+
return f.getParameters().size() == 1
324+
&& f.getName().endsWith("_getHandleId")
325+
&& !f.getName().endsWith("_getTCHandleId");
326+
}
327+
255328
/** Returns an IM expression representing the safe default for the given return type. */
256329
private static ImExpr defaultValueExpr(ImType returnType) {
257330
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: 20 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,21 @@ 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+
LuaExpr arg = LuaAst.LuaExprVarAccess(luaVar.getFor(firstParam));
524+
// Only called when ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING is true.
525+
// Shim opaque runtime handles; keep native GetHandleId for enum-like handles.
526+
String targetFunction = LuaNativeLowering.usesLuaObjectIdentityHandleId(firstParam.getType())
527+
? "__wurst_GetHandleId" : "GetHandleId";
528+
lf.getBody().clear();
529+
lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCallByName(targetFunction, LuaAst.LuaExprlist(arg))));
530+
return true;
531+
}
532+
513533
private boolean rewriteTypeCastingCompatFunction(ImFunction f, LuaFunction lf) {
514534
if (f.getParameters().isEmpty()) {
515535
return false;

0 commit comments

Comments
 (0)