Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
446 changes: 446 additions & 0 deletions generate-obj-mappings.ts → HelperScripts/generate-obj-mappings.ts

Large diffs are not rendered by default.

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 @@ -85,11 +85,14 @@ public static MapRequest.CompilationResult apply(WurstProjectConfigData projectC
// Prefer the previously-injected script (with correct config() body) over the
// raw map script. If it doesn't exist yet (e.g. first Lua build after a JASS-only
// cache), fall through to re-inject so the config() body is never stale.
// Also re-inject if war3map.j was modified after the cached script was written.
File cachedInjectedScript = new File(buildDir, outputScriptName);
if (cachedInjectedScript.exists()) {
boolean cachedScriptStale = !cachedInjectedScript.exists()
|| mapScript.lastModified() > cachedInjectedScript.lastModified();
if (!cachedScriptStale) {
result.script = cachedInjectedScript;
} else if (StringUtils.isNotBlank(buildMapData.getName())) {
WLogger.info("Cached injected script missing, re-injecting config");
WLogger.info("war3map.j changed or cached script missing, re-injecting config");
applyBuildMapData(projectConfig, mapScript, buildDir, w3data, w3I, result, configHash, outputScriptName);
}
// else result.script stays as mapScript (no wurst.build name configured)
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
Expand Up @@ -8,7 +8,7 @@ public class WurstKeywords {
"tuple", "div", "mod", "let", "from", "to", "downto", "step", "endpackage", "skip", "true", "false", "var", "instanceof",
"super", "enum", "switch", "case", "default", "typeId", "begin", "end",
// not really a keyword, but it should feel like one:
"compiletime",
"compiletime", "isLua",
// jurst keywords, maybe split the highlighters later...:
"library", "endlibrary", "scope", "endscope", "requires", "uses", "needs", "struct", "endstruct",
"then", "endif", "loop", "exitwhen", "endloop", "method", "takes", "endmethod", "set", "call",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class GlobalsInliner implements OptimizerPass {
public int optimize(ImTranslator trans) {
Expand All @@ -28,6 +27,21 @@ public int optimize(ImTranslator trans) {
// so it is important, that we do not optimize away the compiletime constant
continue;
}
if (v.getName().equals("MagicFunctions_isLua") && trans.isLuaTarget()) {
// In Lua mode, isLua must evaluate to true.
// Normal inlining would use the declared value (false); override it here.
for (ImVarRead read : new ArrayList<>(v.attrReads())) {
read.replaceBy(JassIm.ImBoolVal(true));
}
for (ImVarWrite write : new ArrayList<>(v.attrWrites())) {
if (write.getParent() != null) {
write.replaceBy(ImHelper.nullExpr());
}
}
obsoleteVars.add(v);
obsoleteCount++;
continue;
}
if (v.getType() instanceof ImArrayType
|| v.getType() instanceof ImArrayTypeMulti) {
// cannot optimize arrays yet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
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.
*
* <p>Stubs and wrappers are created lazily (on first call-site encounter) and added
* to prog only after the traversal completes. This avoids the memory cost of
* creating wrappers for every BJ function in the IM (common.j declares hundreds of
* functions, most of which are unreachable in any given program).
*/
public static void transform(ImProg prog) {
// Replace all reads of MagicFunctions_isLua with true.
// This must happen before any optimizer passes so that dead-code elimination
// can remove Jass-only branches at compile time.
// We use attrReads() (not a visitor) to target only rvalue uses, avoiding
// ClassCastException when the same ImVarAccess appears as a write target (lvalue).
for (ImVar global : prog.getGlobals()) {
if ("MagicFunctions_isLua".equals(global.getName())) {
for (ImVarRead read : new ArrayList<>(global.attrReads())) {
read.replaceBy(JassIm.ImBoolVal(true));
}
break;
}
}

// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
// Populated lazily during the traversal.
Map<ImFunction, ImFunction> replacements = new LinkedHashMap<>();
// BJ functions that don't need a replacement (not GetHandleId, not hashtable/callback,
// no handle params). Cached to avoid rechecking the same function at every call site.
Set<ImFunction> noReplacement = new HashSet<>();
// All generated functions (stubs and wrappers) are deferred until after the traversal:
// - Stubs: deferred so ConcurrentModificationException is avoided on prog.getFunctions()
// - Wrappers: deferred so the visitor doesn't see their internal BJ delegate calls and
// recursively wrap them, which would cause infinite wrapping.
List<ImFunction> deferredAdditions = new ArrayList<>();

prog.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImFunctionCall call) {
super.visit(call);
ImFunction f = call.getFunc();
if (!f.isBj()) return;
if (noReplacement.contains(f)) return;

if (!replacements.containsKey(f)) {
ImFunction r = computeReplacement(f);
if (r != null) {
replacements.put(f, r);
deferredAdditions.add(r);
} else {
noReplacement.add(f);
}
}
ImFunction replacement = replacements.get(f);

if (replacement != null) {
call.replaceBy(JassIm.ImFunctionCall(
call.attrTrace(), replacement,
JassIm.ImTypeArguments(),
call.getArguments().copy(),
false, CallType.NORMAL));
}
}

private ImFunction computeReplacement(ImFunction bj) {
String name = bj.getName();
if ("GetHandleId".equals(name)) {
return createNativeStub("__wurst_GetHandleId", bj);
} else if (HASHTABLE_NATIVE_NAMES.contains(name)) {
return createNativeStub("__wurst_" + name, bj);
} else if (CONTEXT_CALLBACK_NATIVE_NAMES.contains(name)) {
return createNativeStub("__wurst_" + name, bj);
} else if (hasHandleParam(bj)) {
return createNilSafeWrapper(bj);
}
return null;
}
});

// Add all generated functions after the traversal so their bodies are not visited
// by the replacement visitor above.
prog.getFunctions().addAll(deferredAdditions);
}

/**
* 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.
*
* <p>The caller is responsible for adding the stub to prog.getFunctions().
*/
private static ImFunction createNativeStub(String name, ImFunction original) {
ImVars params = JassIm.ImVars();
for (ImVar p : original.getParameters()) {
params.add(JassIm.ImVar(p.attrTrace(), p.getType().copy(), p.getName(), false));
}
return JassIm.ImFunction(
original.attrTrace(), name,
JassIm.ImTypeVars(), params,
original.getReturnType().copy(),
JassIm.ImVars(), JassIm.ImStmts(),
Collections.singletonList(FunctionFlagEnum.IS_NATIVE));
}

/**
* 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,20 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
}

LuaFunction f = tr.luaFunc.getFor(e.getFunc());
if ("I2S".equals(f.getName()) && isIntentionalThreadAbortCall(e)) {
// Use the immutable ImFunction name rather than f.getName(), because f is a cached
// LuaFunction object shared across all call sites of this native. The setName() calls
// below mutate it, so f.getName() changes after the first translation and can no longer
// be relied upon for sentinel checks.
String imFuncName = e.getFunc().getName();
if ("I2S".equals(imFuncName) && isIntentionalThreadAbortCall(e)) {
return LuaAst.LuaExprFunctionCallByName("error", LuaAst.LuaExprlist(
LuaAst.LuaExprStringVal(WURST_ABORT_THREAD_SENTINEL),
LuaAst.LuaExprIntVal("0")
));
}
if (f.getName().equals(ImTranslator.$DEBUG_PRINT)) {
if (ImTranslator.$DEBUG_PRINT.equals(imFuncName)) {
f.setName("BJDebugMsg");
} else if (f.getName().equals("I2S")) {
} else if ("I2S".equals(imFuncName)) {
f.setName("tostring");
}
return LuaAst.LuaExprFunctionCall(f, tr.translateExprList(e.getArguments()));
Expand Down
Loading
Loading