Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -889,6 +889,13 @@ public LuaCompilationUnit transformProgToLua() {
}
ImTranslator imTranslator2 = getImTranslator();
ImOptimizer optimizer = new ImOptimizer(timeTaker, imTranslator2);

// Lower Lua-specific native calls into IM-level wrappers before optimization,
// so the optimizer can inline and eliminate the nil-safety checks and remapped stubs.
beginPhase(4, "lua native lowering");
LuaNativeLowering.transform(imProg);
timeTaker.endPhase();

// inliner
stage = 5;
if (runArgs.isInline()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ protected File compileMap(File projectFolder, WurstGui gui, Optional<File> mapCo

String compiledMapScript = sb.toString();
LuaTranslator.assertNoLeakedHashtableNativeCalls(compiledMapScript);
LuaTranslator.assertNoLeakedGetHandleIdCalls(compiledMapScript);
File buildDir = getBuildDir();
File outFile = new File(buildDir, BUILD_COMPILED_LUA_NAME);
Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package de.peeeq.wurstscript.translation.imtranslation;

import de.peeeq.wurstscript.WurstOperator;
import de.peeeq.wurstscript.jassIm.*;

import java.util.*;

/**
* IM-level lowering pass for the Lua backend, run before optimization so the
* optimizer can inline and eliminate the generated wrappers.
*
* <p>Three classes of WC3 BJ calls are transformed:
* <ol>
* <li><b>GetHandleId</b> – replaced 1:1 by {@code __wurst_GetHandleId}, whose Lua
* implementation uses a stable table counter instead of the WC3 handle ID
* (which can desync in Lua mode).</li>
* <li><b>Hashtable natives</b> ({@code SaveInteger}, {@code LoadBoolean}, …) and
* <b>context-callback natives</b> ({@code ForForce}, {@code ForGroup}, …) –
* replaced 1:1 by their {@code __wurst_} prefixed equivalents, whose Lua
* implementations are provided by {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.</li>
* <li><b>All other BJ calls with at least one handle-typed parameter</b> – wrapped
* by a generated IM function that first checks each handle param for {@code null}
* and returns the type-appropriate default (0 / 0.0 / false / "" / nil), then
* delegates to the original BJ function. This matches Jass behavior, which
* silently returns defaults on null-handle calls instead of crashing.</li>
* </ol>
*
* <p>IS_NATIVE stubs added for category 1 and 2 are recognised by
* {@link de.peeeq.wurstscript.translation.lua.translation.LuaTranslator#translateFunc} as
* Wurst-owned natives and filled in by
* {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.
*/
public final class LuaNativeLowering {

/** Hashtable native names that need to be remapped to {@code __wurst_} equivalents. */
private static final Set<String> HASHTABLE_NATIVE_NAMES = new HashSet<>(Arrays.asList(
"InitHashtable",
"SaveInteger", "SaveBoolean", "SaveReal", "SaveStr",
"LoadInteger", "LoadBoolean", "LoadReal", "LoadStr",
"HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", "HaveSavedHandle",
"FlushChildHashtable", "FlushParentHashtable",
"RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString", "RemoveSavedHandle",
// Handle-typed save/load variants
"SavePlayerHandle", "SaveWidgetHandle", "SaveDestructableHandle", "SaveItemHandle", "SaveUnitHandle",
"SaveAbilityHandle", "SaveTimerHandle", "SaveTriggerHandle", "SaveTriggerConditionHandle",
"SaveTriggerActionHandle", "SaveTriggerEventHandle", "SaveForceHandle", "SaveGroupHandle",
"SaveLocationHandle", "SaveRectHandle", "SaveBooleanExprHandle", "SaveSoundHandle", "SaveEffectHandle",
"SaveUnitPoolHandle", "SaveItemPoolHandle", "SaveQuestHandle", "SaveQuestItemHandle",
"SaveDefeatConditionHandle", "SaveTimerDialogHandle", "SaveLeaderboardHandle", "SaveMultiboardHandle",
"SaveMultiboardItemHandle", "SaveTrackableHandle", "SaveDialogHandle", "SaveButtonHandle",
"SaveTextTagHandle", "SaveLightningHandle", "SaveImageHandle", "SaveUbersplatHandle", "SaveRegionHandle",
"SaveFogStateHandle", "SaveFogModifierHandle", "SaveAgentHandle", "SaveHashtableHandle", "SaveFrameHandle",
"LoadPlayerHandle", "LoadWidgetHandle", "LoadDestructableHandle", "LoadItemHandle", "LoadUnitHandle",
"LoadAbilityHandle", "LoadTimerHandle", "LoadTriggerHandle", "LoadTriggerConditionHandle",
"LoadTriggerActionHandle", "LoadTriggerEventHandle", "LoadForceHandle", "LoadGroupHandle",
"LoadLocationHandle", "LoadRectHandle", "LoadBooleanExprHandle", "LoadSoundHandle", "LoadEffectHandle",
"LoadUnitPoolHandle", "LoadItemPoolHandle", "LoadQuestHandle", "LoadQuestItemHandle",
"LoadDefeatConditionHandle", "LoadTimerDialogHandle", "LoadLeaderboardHandle", "LoadMultiboardHandle",
"LoadMultiboardItemHandle", "LoadTrackableHandle", "LoadDialogHandle", "LoadButtonHandle",
"LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle",
"LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle"
));

/** Context-callback natives that need to be remapped to {@code __wurst_} equivalents. */
private static final Set<String> CONTEXT_CALLBACK_NATIVE_NAMES = new HashSet<>(Arrays.asList(
"ForForce", "GetEnumPlayer",
"ForGroup", "GetEnumUnit",
"EnumItemsInRect", "GetEnumItem",
"EnumDestructablesInRect", "GetEnumDestructable"
));

private LuaNativeLowering() {}

/**
* Transforms the IM program in place.
*
* <p>Must be called <em>before</em> the optimizer so that the optimizer
* can inline and eliminate the generated wrappers.
*/
public static void transform(ImProg prog) {
// Maps original BJ function → replacement (either a IS_NATIVE stub or a nil-safety wrapper)
Map<ImFunction, ImFunction> replacements = new LinkedHashMap<>();
// Nil-safety wrappers are collected separately and added to prog AFTER the traversal,
// so the traversal does not visit their bodies and replace their internal BJ delegate calls.
List<ImFunction> deferredWrappers = new ArrayList<>();

// Snapshot to avoid ConcurrentModificationException when createNativeStub adds to prog.getFunctions()
List<ImFunction> snapshot = new ArrayList<>(prog.getFunctions());
for (ImFunction f : snapshot) {
if (!f.isBj()) {
continue;
}
String name = f.getName();

if ("GetHandleId".equals(name)) {
replacements.put(f, createNativeStub("__wurst_GetHandleId", f, prog));
} else if (HASHTABLE_NATIVE_NAMES.contains(name)) {
replacements.put(f, createNativeStub("__wurst_" + name, f, prog));
} else if (CONTEXT_CALLBACK_NATIVE_NAMES.contains(name)) {
replacements.put(f, createNativeStub("__wurst_" + name, f, prog));
} else if (hasHandleParam(f)) {
ImFunction wrapper = createNilSafeWrapper(f);
replacements.put(f, wrapper);
deferredWrappers.add(wrapper);
}
}

if (replacements.isEmpty()) {
return;
}

// Replace all call sites in the existing IM (before adding wrappers).
// Wrappers are deferred so their internal BJ delegate calls are not replaced.
prog.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImFunctionCall call) {
super.visit(call);
ImFunction replacement = replacements.get(call.getFunc());
if (replacement != null) {
call.replaceBy(JassIm.ImFunctionCall(
call.attrTrace(), replacement,
JassIm.ImTypeArguments(),
call.getArguments().copy(),
false, CallType.NORMAL));
}
}
});

// Add nil-safety wrapper functions AFTER traversal so their own bodies are not traversed.
prog.getFunctions().addAll(deferredWrappers);
}

/**
* Creates a new IS_NATIVE (non-BJ) IM function stub with the same signature as
* {@code original}. The Lua translator will fill in the body via
* {@code LuaNatives.get()} when it encounters the stub.
*/
private static ImFunction createNativeStub(String name, ImFunction original, ImProg prog) {
ImVars params = JassIm.ImVars();
for (ImVar p : original.getParameters()) {
params.add(JassIm.ImVar(p.attrTrace(), p.getType().copy(), p.getName(), false));
}
ImFunction stub = JassIm.ImFunction(
original.attrTrace(), name,
JassIm.ImTypeVars(), params,
original.getReturnType().copy(),
JassIm.ImVars(), JassIm.ImStmts(),
Collections.singletonList(FunctionFlagEnum.IS_NATIVE));
prog.getFunctions().add(stub);
return stub;
}

/**
* Creates a nil-safety wrapper for {@code bjNative}.
*
* <p>The generated function checks each handle-typed parameter against
* {@code null} and returns the type-appropriate default value if any is
* null. Otherwise it delegates to the original BJ function.
*/
private static ImFunction createNilSafeWrapper(ImFunction bjNative) {
ImVars params = JassIm.ImVars();
List<ImVar> paramVars = new ArrayList<>();
for (ImVar p : bjNative.getParameters()) {
ImVar copy = JassIm.ImVar(p.attrTrace(), p.getType().copy(), p.getName(), false);
params.add(copy);
paramVars.add(copy);
}

ImStmts body = JassIm.ImStmts();

// Null-check each handle param: if param == null then return <default> end
ImExpr returnDefault = defaultValueExpr(bjNative.getReturnType());
for (ImVar param : paramVars) {
if (isHandleType(param.getType())) {
ImExpr condition = JassIm.ImOperatorCall(WurstOperator.EQ, JassIm.ImExprs(
JassIm.ImVarAccess(param),
JassIm.ImNull(param.getType().copy())
));
ImStmts thenBlock = JassIm.ImStmts(
JassIm.ImReturn(bjNative.attrTrace(), returnDefault.copy())
);
body.add(JassIm.ImIf(bjNative.attrTrace(), condition, thenBlock, JassIm.ImStmts()));
}
}

// Delegate to the original BJ native
ImExprs callArgs = JassIm.ImExprs();
for (ImVar pv : paramVars) {
callArgs.add(JassIm.ImVarAccess(pv));
}
ImFunctionCall delegate = JassIm.ImFunctionCall(
bjNative.attrTrace(), bjNative,
JassIm.ImTypeArguments(), callArgs, false, CallType.NORMAL);

if (bjNative.getReturnType() instanceof ImVoid) {
body.add(delegate);
} else {
body.add(JassIm.ImReturn(bjNative.attrTrace(), delegate));
}

return JassIm.ImFunction(
bjNative.attrTrace(),
"__wurst_safe_" + bjNative.getName(),
JassIm.ImTypeVars(), params,
bjNative.getReturnType().copy(),
JassIm.ImVars(), body,
Collections.emptyList());
}

private static boolean hasHandleParam(ImFunction f) {
for (ImVar p : f.getParameters()) {
if (isHandleType(p.getType())) {
return true;
}
}
return false;
}

/** Returns true for WC3 handle types (ImSimpleType that is not int/real/boolean/string). */
static boolean isHandleType(ImType type) {
if (!(type instanceof ImSimpleType)) {
return false;
}
String n = ((ImSimpleType) type).getTypename();
return !n.equals("integer") && !n.equals("real") && !n.equals("boolean") && !n.equals("string");
}

/** Returns an IM expression representing the safe default for the given return type. */
private static ImExpr defaultValueExpr(ImType returnType) {
if (returnType instanceof ImSimpleType) {
String n = ((ImSimpleType) returnType).getTypename();
switch (n) {
case "integer": return JassIm.ImIntVal(0);
case "real": return JassIm.ImRealVal("0.0");
case "boolean": return JassIm.ImBoolVal(false);
case "string": return JassIm.ImStringVal("");
}
}
// void or handle type → null
if (returnType instanceof ImVoid) {
return JassIm.ImNull(JassIm.ImVoid());
}
return JassIm.ImNull(returnType.copy());
}
}
Loading
Loading