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
3 changes: 3 additions & 0 deletions de.peeeq.wurstscript/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def j25Launcher = toolchainSvc.launcherFor(java.toolchain)

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,12 +645,9 @@ private void beginPhase(int phase, String description) {

private void printDebugImProg(String debugFile) {
if (!errorHandler.isUnitTestMode() || !errorHandler.isOutputTestSource()) {
// output only in unit test mode
return;
}

try {
// TODO remove test output
File file = new File(debugFile);
file.getParentFile().mkdirs();
try (Writer w = Files.newWriter(file, Charsets.UTF_8)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public class ErrorHandler {

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

public ErrorHandler(WurstGui gui) {
this.gui = gui;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

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

@NotNull
Expand Down Expand Up @@ -74,11 +75,19 @@ public static Annotation getAnnotation(NameDef e, String annotation) {
return null;
}

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

private static String getNormalizedType(Annotation a) {
return annotationTypeCache.computeIfAbsent(a,
ann -> normalizeAnnotation(ann.getAnnotationType()));
}

/** Explicitly clear both caches. Called from GlobalCaches.clearAll() between tests. */
public static void clearCaches() {
annotationTypeCache.clear();
normalizationCache.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
* <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>
* implementation uses a stable table counter for selected opaque runtime handle
* families only. Enum-like handle families keep native semantics in Lua.</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>
* by a generated IM function that first checks each required 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.
* {@code boolexpr} and {@code code} typed params are intentionally skipped: these
* are optional/nullable in Jass (e.g. the filter arg of
* {@code TriggerRegisterPlayerUnitEvent}) and passing {@code nil} is valid.</li>
* </ol>
*
* <p>IS_NATIVE stubs added for category 1 and 2 are recognised by
Expand Down Expand Up @@ -69,6 +72,26 @@ public final class LuaNativeLowering {
"EnumDestructablesInRect", "GetEnumDestructable"
));

/** True runtime-object handles that should use Lua-side object identity for GetHandleId. */
private static final Set<String> OPAQUE_RUNTIME_HANDLE_TYPES = new HashSet<>(Arrays.asList(
"unit", "item", "destructable", "effect", "lightning", "timer", "trigger",
"triggeraction", "triggercondition", "boolexpr", "force", "group", "location",
"rect", "region", "sound", "dialog", "button", "quest", "questitem",
"leaderboard", "multiboard", "multiboarditem", "trackable", "texttag",
"image", "ubersplat", "framehandle", "fogmodifier", "hashtable"
));

/**
* When {@code true}, only opaque runtime-handle families (unit, item, timer, …)
* are shimmed via {@code __wurst_GetHandleId}; enum-like handle families
* (eventid, playerevent, …) keep native {@code GetHandleId} semantics.
*
* When {@code false} (safe default), ALL {@code GetHandleId} calls are shimmed
* unconditionally — this matches the pre-selective-shim behaviour and avoids
* any desync risk while the selective logic is being validated.
*/
public static final boolean ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING = true;

private LuaNativeLowering() {}

/**
Expand Down Expand Up @@ -100,6 +123,7 @@ public static void transform(ImProg prog) {
// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
// Populated lazily during the traversal.
Map<ImFunction, ImFunction> replacements = new LinkedHashMap<>();
Map<String, ImFunction> specialNativeStubs = 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<>();
Expand All @@ -114,7 +138,37 @@ public static void transform(ImProg prog) {
public void visit(ImFunctionCall call) {
super.visit(call);
ImFunction f = call.getFunc();
if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && isCompatGetHandleIdFunction(f)) {
if (shouldRewriteGetHandleId(call)) {
Comment thread
Frotty marked this conversation as resolved.
ImFunction replacement = specialNativeStubs.computeIfAbsent("__wurst_GetHandleId",
name -> createNativeStub(name, f));
if (!deferredAdditions.contains(replacement)) {
deferredAdditions.add(replacement);
}
call.replaceBy(JassIm.ImFunctionCall(
call.attrTrace(), replacement,
JassIm.ImTypeArguments(),
call.getArguments().copy(),
false, CallType.NORMAL));
}
return;
}
if (!f.isBj()) return;
if ("GetHandleId".equals(f.getName())) {
if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && shouldRewriteGetHandleId(call)) {
ImFunction replacement = specialNativeStubs.computeIfAbsent("__wurst_GetHandleId",
name -> createNativeStub(name, f));
if (!deferredAdditions.contains(replacement)) {
deferredAdditions.add(replacement);
}
call.replaceBy(JassIm.ImFunctionCall(
call.attrTrace(), replacement,
JassIm.ImTypeArguments(),
call.getArguments().copy(),
false, CallType.NORMAL));
}
return;
}
if (noReplacement.contains(f)) return;

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

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)) {
if (HASHTABLE_NATIVE_NAMES.contains(name)) {
return createNativeStub("__wurst_" + name, bj);
} else if (CONTEXT_CALLBACK_NATIVE_NAMES.contains(name)) {
return createNativeStub("__wurst_" + name, bj);
Expand Down Expand Up @@ -195,10 +247,12 @@ private static ImFunction createNilSafeWrapper(ImFunction bjNative) {

ImStmts body = JassIm.ImStmts();

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

/** Returns true for WC3 handle types (ImSimpleType that is not int/real/boolean/string). */
static boolean isHandleType(ImType type) {
public 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 true for handle types that are valid to pass as {@code null} in Jass without
* triggering a null-handle crash. These params are skipped in nil-safety wrappers.
*
* <p>{@code boolexpr} and {@code code} are the canonical optional types: every WC3
* API that takes them (filter, condition, action callbacks) accepts {@code null} to
* mean "no callback".
*/
static boolean isNullableHandleType(ImType type) {
if (!(type instanceof ImSimpleType)) {
return false;
}
String n = ((ImSimpleType) type).getTypename();
return n.equals("boolexpr") || n.equals("code");
}

private static boolean shouldRewriteGetHandleId(ImFunctionCall call) {
if (call.getArguments().size() != 1) {
return true;
}
return usesLuaObjectIdentityHandleId(call.getArguments().get(0).attrTyp());
}

public static boolean usesLuaObjectIdentityHandleId(ImType type) {
if (!(type instanceof ImSimpleType)) {
return false;
}
String typeName = ((ImSimpleType) type).getTypename();
return OPAQUE_RUNTIME_HANDLE_TYPES.contains(typeName);
}

private static boolean isCompatGetHandleIdFunction(ImFunction f) {
if (f.getParameters().size() != 1
|| !f.getName().endsWith("_getHandleId")
|| f.getName().endsWith("_getTCHandleId")) {
return false;
}
// Restrict to WC3 simple handle types (ImSimpleType). User-defined Wurst classes
// use ImClassType and must not have their call sites replaced.
return isHandleType(f.getParameters().get(0).getType());
}

/** Returns an IM expression representing the safe default for the given return type. */
private static ImExpr defaultValueExpr(ImType returnType) {
if (returnType instanceof ImSimpleType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ public class LuaAssertions {
private LuaAssertions() {}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public class LuaNatives {
"LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle",
"LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle"
};

static {
addNative("testSuccess", f -> {
f.getBody().add(LuaAst.LuaLiteral("print(\"testSuccess\")"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
/**
* Builds and registers the Wurst Lua infrastructure functions that are always
* emitted into the generated script, regardless of what user code looks like.
*
* These include object/string index maps, ensure-type coercers, array defaults,
* hashtable helpers, and context-callback wrappers.
*
* All methods are static and take the active {@link LuaTranslator} as the first
* argument, following the same convention as {@link ExprTranslation}.
*/
class LuaPolyfillSetup {

Expand Down Expand Up @@ -55,29 +49,22 @@ static void createStringConcatFunction(LuaTranslator tr) {
}

static void createInstanceOfFunction(LuaTranslator tr) {
String[] code = {
"return x ~= nil and x." + WURST_SUPERTYPES + "[A]"
};

tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("A", LuaAst.LuaNoExpr()));
for (String c : code) {
tr.instanceOfFunction.getBody().add(LuaAst.LuaLiteral(c));
}
tr.instanceOfFunction.getBody().add(LuaAst.LuaLiteral("return x ~= nil and x." + WURST_SUPERTYPES + "[A]"));
tr.luaModel.add(tr.instanceOfFunction);
}

static void createObjectIndexFunctions(LuaTranslator tr) {
String vName = "__wurst_objectIndexMap";
LuaVariable v = LuaAst.LuaVariable(vName, LuaAst.LuaExprNull());
tr.luaModel.add(v);
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(v), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
LuaVariable objectIndexMap = LuaAst.LuaVariable("__wurst_objectIndexMap", LuaAst.LuaExprNull());
tr.luaModel.add(objectIndexMap);
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(objectIndexMap), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0"))
))));

LuaVariable im = LuaAst.LuaVariable("__wurst_number_wrapper_map", LuaAst.LuaExprNull());
tr.luaModel.add(im);
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(im), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
LuaVariable numberWrapperMap = LuaAst.LuaVariable("__wurst_number_wrapper_map", LuaAst.LuaExprNull());
tr.luaModel.add(numberWrapperMap);
tr.deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(numberWrapperMap), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0"))
))));

Expand Down Expand Up @@ -217,5 +204,4 @@ static void createEnsureTypeFunctions(LuaTranslator tr) {
tr.ensureStrFunction.getBody().add(LuaAst.LuaLiteral("return tostring(x)"));
tr.luaModel.add(tr.ensureStrFunction);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum;
import de.peeeq.wurstscript.translation.imtranslation.GetAForB;
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering;
import de.peeeq.wurstscript.types.TypesHelper;
import de.peeeq.wurstscript.utils.Lazy;
import de.peeeq.wurstscript.utils.Utils;
Expand Down Expand Up @@ -461,6 +462,10 @@ private void translateFunc(ImFunction f) {
if (f.isNative()) {
LuaNatives.get(lf);
} else {
if (LuaNativeLowering.ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && rewriteGetHandleIdCompatFunction(f, lf)) {
luaModel.add(lf);
return;
}
if (rewriteTypeCastingCompatFunction(f, lf)) {
luaModel.add(lf);
return;
Expand Down Expand Up @@ -510,6 +515,26 @@ private void translateFunc(ImFunction f) {
}
}

private boolean rewriteGetHandleIdCompatFunction(ImFunction f, LuaFunction lf) {
if (f.getParameters().size() != 1 || !f.getName().endsWith("_getHandleId") || f.getName().endsWith("_getTCHandleId")) {
return false;
Comment thread
Frotty marked this conversation as resolved.
}
ImVar firstParam = f.getParameters().get(0);
// Restrict to WC3 simple handle types. User-defined Wurst classes use ImClassType
// and must not have their function body replaced.
if (!LuaNativeLowering.isHandleType(firstParam.getType())) {
return false;
}
LuaExpr arg = LuaAst.LuaExprVarAccess(luaVar.getFor(firstParam));
// Only called when ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING is true.
// Shim opaque runtime handles; keep native GetHandleId for enum-like handles.
String targetFunction = LuaNativeLowering.usesLuaObjectIdentityHandleId(firstParam.getType())
? "__wurst_GetHandleId" : "GetHandleId";
lf.getBody().clear();
lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCallByName(targetFunction, LuaAst.LuaExprlist(arg))));
return true;
}

private boolean rewriteTypeCastingCompatFunction(ImFunction f, LuaFunction lf) {
if (f.getParameters().isEmpty()) {
return false;
Expand Down
Loading
Loading