diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index ff03cd096..dd3ce757b 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -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 } @@ -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 ) } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index 60f0b1d34..8df2f1fbb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -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)) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java index ce3a180ef..9856b41df 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java @@ -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; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/HasAnnotation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/HasAnnotation.java index 10bfc9990..aded598d4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/HasAnnotation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/HasAnnotation.java @@ -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 normalizationCache = new HashMap<>(); @NotNull @@ -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 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 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(); + } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index 1a13c46f8..84f36e341 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -12,17 +12,20 @@ *

Three classes of WC3 BJ calls are transformed: *

    *
  1. GetHandleId – 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).
  2. + * implementation uses a stable table counter for selected opaque runtime handle + * families only. Enum-like handle families keep native semantics in Lua. *
  3. Hashtable natives ({@code SaveInteger}, {@code LoadBoolean}, …) and * context-callback natives ({@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}.
  4. *
  5. All other BJ calls with at least one handle-typed parameter – 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.
  6. + * 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. *
* *

IS_NATIVE stubs added for category 1 and 2 are recognised by @@ -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 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() {} /** @@ -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 replacements = new LinkedHashMap<>(); + Map 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 noReplacement = new HashSet<>(); @@ -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)) { + 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)) { @@ -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); @@ -195,10 +247,12 @@ private static ImFunction createNilSafeWrapper(ImFunction bjNative) { ImStmts body = JassIm.ImStmts(); - // Null-check each handle param: if param == null then return end + // Null-check each required handle param: if param == null then return 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()) @@ -244,7 +298,7 @@ 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; } @@ -252,6 +306,48 @@ static boolean isHandleType(ImType type) { 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. + * + *

{@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) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java index 13779b2a7..82d003d60 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java @@ -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 called = collectCalledFunctionNames(luaCode); - if (called.contains("GetHandleId")) { + Set 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."); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java index 5f4b16e0e..09ac3b9bb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java @@ -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\")")); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java index f007f135c..39f08eec1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java @@ -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 { @@ -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")) )))); @@ -217,5 +204,4 @@ static void createEnsureTypeFunctions(LuaTranslator tr) { tr.ensureStrFunction.getBody().add(LuaAst.LuaLiteral("return tostring(x)")); tr.luaModel.add(tr.ensureStrFunction); } - } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index b89bbc5f5..880ce0c2d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -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; @@ -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; @@ -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; + } + 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; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java index 1950cfc40..bb6cce60e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java @@ -1,6 +1,7 @@ package de.peeeq.wurstscript.validation; import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.attributes.HasAnnotation; import de.peeeq.wurstscript.intermediatelang.ILconst; import de.peeeq.wurstscript.intermediatelang.interpreter.LocalState; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; @@ -142,6 +143,7 @@ public static void clearAll() { LOCAL_STATE_CACHE.clear(); LOCAL_STATE_NOARG_CACHE.clear(); lookupCache.clear(); + HasAnnotation.clearCaches(); } public enum LookupType { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index 1a9bc0d7d..ce4974aaa 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -67,6 +67,13 @@ private void assertFunctionBodyContains(String output, String functionName, Stri assertTrue("Function " + functionName + " was not found.", found); } + /** Skip the calling test when selective GetHandleId shimming is disabled. */ + private static void requireSelectiveGetHandleIdShimming() { + if (!de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering.ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING) { + throw new org.testng.SkipException("Skipped: selective GetHandleId shimming is disabled (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING=false)"); + } + } + private String getFunctionBody(String output, String functionName) { Pattern pattern = Pattern.compile("function\\s*" + functionName + "\\s*\\(.*\\).*\\n" + "((?:\\n|.)*?)end"); Matcher matcher = pattern.matcher(output); @@ -2217,9 +2224,9 @@ public void subclassAllocationIncludesInheritedFieldsInLua() throws IOException @Test public void getHandleIdIsRemappedToWurstPolyfillInLua() throws IOException { - // User code that calls GetHandleId(unit) must be rewritten to __wurst_GetHandleId(unit) - // so that the table-based counter is used instead of the desync-prone native handle ID. - // Use withStdLib so that 'unit'/'agent' types are known to the compiler. + requireSelectiveGetHandleIdShimming(); + // Direct GetHandleId calls on opaque runtime handles should still use the Lua-side + // identity shim. test().testLua(true).withStdLib().lines( "package Test", "import MagicFunctions", @@ -2229,30 +2236,227 @@ public void getHandleIdIsRemappedToWurstPolyfillInLua() throws IOException { " print(I2S(id))" ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_getHandleIdIsRemappedToWurstPolyfillInLua.lua"), Charsets.UTF_8); - // Raw GetHandleId must not appear as a call - assertDoesNotContainRegex(compiled, "\\bGetHandleId\\("); - // __wurst_GetHandleId must be defined assertContainsRegex(compiled, "function\\s+__wurst_GetHandleId\\s*\\("); - // The polyfill must delegate to the object-index mechanism - assertContainsRegex(compiled, "__wurst_objectToIndex"); - // The Lua backend assertion should not throw + assertContainsRegex(compiled, "id\\s*=\\s*__wurst_GetHandleId\\(u\\)"); + String helperBody = getFunctionBody(compiled, "__wurst_GetHandleId"); + assertTrue(helperBody.contains("return __wurst_objectToIndex(h)")); + assertFalse(helperBody.contains("__wurst_tryRegisterKnownEventHandle")); + assertFalse(helperBody.contains("__wurst_canonicalHandleIdByHandle")); de.peeeq.wurstscript.translation.lua.translation.LuaTranslator.assertNoLeakedGetHandleIdCalls(compiled); } + @Test + public void closureEventsHandleGetHandleIdUsesNativeGetHandleIdInLua() throws IOException { + test().testLua(true).withStdLib().executeProg(false) + .file(new File("testscripts/realbugs/nullclosurebug.wurst")); + + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_closureEventsHandleGetHandleIdUsesNativeGetHandleIdInLua.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "function\\s+handle_getHandleId\\s*\\("); + String helperBody = getFunctionBody(compiled, "handle_getHandleId"); + assertTrue(helperBody.contains("return GetHandleId(")); + assertFalse(helperBody.contains("__wurst_GetHandleId(")); + assertContainsRegex(compiled, "eventid_toIntId\\(GetTriggerEventId\\(\\)\\)"); + assertContainsRegex(compiled, "function\\s+eventid_isPlayerunitEvent\\s*\\("); + assertContainsRegex(compiled, "registerPlayerUnitEvent1\\(ConvertPlayerUnitEvent\\(eventId\\)"); + assertContainsRegex(compiled, "eventId\\s*=\\s*handle_getHandleId\\(evnt\\)"); + assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(evnt\\)"); + assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(GetTriggerEventId\\(\\)\\)"); + } + + @Test + public void closureEventsEventIdCompatDoesNotUseOpaqueHandleShimInLua() throws IOException { + requireSelectiveGetHandleIdShimming(); + test().testLua(true).withStdLib().lines( + "package Test", + "import ClosureEvents", + "init", + " let u = GetTriggerUnit()", + " let unitId = GetHandleId(u)", + " let ev = EVENT_PLAYER_UNIT_SPELL_EFFECT", + " let id = ev.getHandleId()", + " if unitId + id >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_closureEventsEventIdCompatDoesNotUseOpaqueHandleShimInLua.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "function\\s+handle_getHandleId\\s*\\("); + String helperBody = getFunctionBody(compiled, "handle_getHandleId"); + assertTrue(helperBody.contains("return GetHandleId(")); + assertFalse(helperBody.contains("__wurst_GetHandleId(")); + assertDoesNotContainRegex(compiled, "function\\s+handle_getHandleId\\s*\\([^\\)]*\\)\\s*\\n\\s*return\\s+__wurst_GetHandleId\\("); + assertContainsRegex(compiled, "=\\s*__wurst_GetHandleId\\("); + } + + @Test + public void registerEventsSpellCastKeepsNativeEventHandleIdsWhileUnitHandlesStayShimmedInLua() throws IOException { + requireSelectiveGetHandleIdShimming(); + test().testLua(true).withStdLib().lines( + "package Test", + "import RegisterEvents", + "init", + " let u = GetTriggerUnit()", + " let unitId = GetHandleId(u)", + " registerPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_CAST) ->", + " skip", + " if unitId >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_registerEventsSpellCastKeepsNativeEventHandleIdsWhileUnitHandlesStayShimmedInLua.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "registerPlayerUnitEvent\\(EVENT_PLAYER_UNIT_SPELL_CAST"); + assertContainsRegex(compiled, "function\\s+registerPlayerUnitEvent\\s*\\("); + assertContainsRegex(compiled, "hid\\s*=\\s*handle_getHandleId\\("); + String helperBody = getFunctionBody(compiled, "handle_getHandleId"); + assertTrue(helperBody.contains("return GetHandleId(")); + assertFalse(helperBody.contains("__wurst_GetHandleId(")); + assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(p1\\)"); + assertContainsRegex(compiled, "=\\s*__wurst_GetHandleId\\("); + } + + @Test + public void eventGetHandleIdMethodKeepsNativeSemanticsWhileOpaqueHandleMethodUsesLuaShim() throws IOException { + requireSelectiveGetHandleIdShimming(); + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " let u = GetTriggerUnit()", + " let uid = u.getHandleId()", + " let pe = EVENT_PLAYER_LEAVE", + " let pid = pe.getHandleId()", + " let pue = EVENT_PLAYER_UNIT_SPELL_CAST", + " let pueid = pue.getHandleId()", + " if uid + pid + pueid >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_eventGetHandleIdMethodKeepsNativeSemanticsWhileOpaqueHandleMethodUsesLuaShim.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "=\\s*__wurst_GetHandleId\\("); + assertContainsRegex(compiled, "handle_getHandleId\\("); + String helperBody = getFunctionBody(compiled, "handle_getHandleId"); + assertTrue(helperBody.contains("return GetHandleId(")); + assertFalse(helperBody.contains("__wurst_GetHandleId(")); + } + + @Test + public void eventHandleIdCallKeepsNativeGetHandleIdInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import MagicFunctions", + "init", + " let eventId = GetHandleId(EVENT_PLAYER_LEAVE)", + " if eventId >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_eventHandleIdCallKeepsNativeGetHandleIdInLua.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "eventId\\s*=\\s*GetHandleId\\(EVENT_PLAYER_LEAVE\\)"); + assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(EVENT_PLAYER_LEAVE\\)"); + } + + @Test + public void eventTempHandleIdCallKeepsNativeGetHandleIdInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import MagicFunctions", + "init", + " let ev = EVENT_PLAYER_UNIT_SPELL_EFFECT", + " let id = GetHandleId(ev)", + " if id >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_eventTempHandleIdCallKeepsNativeGetHandleIdInLua.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "ev\\s*=\\s*EVENT_PLAYER_UNIT_SPELL_EFFECT"); + assertContainsRegex(compiled, "id\\s*=\\s*GetHandleId\\(ev\\)"); + assertDoesNotContainRegex(compiled, "id\\s*=\\s*__wurst_GetHandleId\\(ev\\)"); + } + + @Test + public void enumConvertCallsStayNativeInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import MagicFunctions", + "init", + " let pe = ConvertPlayerEvent(311)", + " let pc = ConvertPlayerColor(0)", + " let et = ConvertEffectType(0)", + " if GetHandleId(pe) >= 0 and GetHandleId(pc) >= 0 and GetHandleId(et) >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_enumConvertCallsStayNativeInLua.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "pe\\s*=\\s*ConvertPlayerEvent\\(311\\)"); + assertContainsRegex(compiled, "pc\\s*=\\s*ConvertPlayerColor\\(0\\)"); + assertContainsRegex(compiled, "et\\s*=\\s*ConvertEffectType\\(0\\)"); + assertContainsRegex(compiled, "GetHandleId\\(pe\\)"); + assertContainsRegex(compiled, "GetHandleId\\(pc\\)"); + assertContainsRegex(compiled, "GetHandleId\\(et\\)"); + assertDoesNotContainRegex(compiled, "function\\s+__wurst_ConvertPlayerEvent\\s*\\("); + assertDoesNotContainRegex(compiled, "function\\s+__wurst_ConvertPlayerColor\\s*\\("); + assertDoesNotContainRegex(compiled, "function\\s+__wurst_ConvertEffectType\\s*\\("); + } + + @Test + public void conservativeLuaHandleIdLoweringDoesNotEmitCanonicalEnumHelpers() throws IOException { + requireSelectiveGetHandleIdShimming(); + test().testLua(true).withStdLib().lines( + "package Test", + "import MagicFunctions", + "init", + " let u = GetTriggerUnit()", + " let uid = GetHandleId(u)", + " let eid = GetHandleId(EVENT_PLAYER_LEAVE)", + " if uid + eid >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_conservativeLuaHandleIdLoweringDoesNotEmitCanonicalEnumHelpers.lua"), Charsets.UTF_8); + + assertContainsRegex(compiled, "uid\\s*=\\s*__wurst_GetHandleId\\(u\\)"); + assertContainsRegex(compiled, "eid\\s*=\\s*GetHandleId\\(EVENT_PLAYER_LEAVE\\)"); + assertDoesNotContainRegex(compiled, "__wurst_registerCanonicalHandleId"); + assertDoesNotContainRegex(compiled, "__wurst_tryRegisterKnownEventHandle"); + assertDoesNotContainRegex(compiled, "__wurst_canonicalHandleIdByHandle"); + } + + @Test + public void getHandleIdAssertionOnlyRequiresPolyfillDefinitionWhenUsed() { + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("GetHandleId(x)"); + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("function Foo:GetHandleId(x) return 0 end"); + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("function Foo.GetHandleId(x) return 0 end"); + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("-- GetHandleId(x) is allowed for enum-like handles"); + try { + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("__wurst_GetHandleId(x)"); + fail("Expected RuntimeException for missing __wurst_GetHandleId helper definition"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains("__wurst_GetHandleId")); + } + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("function __wurst_GetHandleId(h) return 0 end\n__wurst_GetHandleId(x)"); + } + @Test public void getHandleIdAssertionDetectsLeak() { - // Verify the assertion helper throws when a raw GetHandleId call is present. + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("GetHandleId(x)"); try { de.peeeq.wurstscript.translation.lua.translation.LuaTranslator - .assertNoLeakedGetHandleIdCalls("GetHandleId(x)"); - fail("Expected RuntimeException for leaked GetHandleId call"); + .assertNoLeakedGetHandleIdCalls("__wurst_GetHandleId(x)"); + fail("Expected RuntimeException for missing __wurst_GetHandleId helper definition"); } catch (RuntimeException e) { - assertTrue(e.getMessage().contains("GetHandleId")); + assertTrue(e.getMessage().contains("__wurst_GetHandleId")); } } @Test public void getHandleIdAssertionDoesNotFalsePositiveOnDeclarations() { + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("GetHandleId(x)"); // A function declaration named GetHandleId (e.g. from a class method) must not // trigger the assertion — only actual calls should. de.peeeq.wurstscript.translation.lua.translation.LuaTranslator @@ -2260,16 +2464,18 @@ public void getHandleIdAssertionDoesNotFalsePositiveOnDeclarations() { de.peeeq.wurstscript.translation.lua.translation.LuaTranslator .assertNoLeakedGetHandleIdCalls("function Foo.GetHandleId(x) return 0 end"); de.peeeq.wurstscript.translation.lua.translation.LuaTranslator - .assertNoLeakedGetHandleIdCalls("-- GetHandleId(x) is remapped"); + .assertNoLeakedGetHandleIdCalls("-- GetHandleId(x) is allowed for enum-like handles"); de.peeeq.wurstscript.translation.lua.translation.LuaTranslator .assertNoLeakedGetHandleIdCalls("local s = \"GetHandleId(x)\""); + de.peeeq.wurstscript.translation.lua.translation.LuaTranslator + .assertNoLeakedGetHandleIdCalls("function __wurst_GetHandleId(h) return 0 end\n__wurst_GetHandleId(x)"); } // ----- Null-safe extern native wrappers ----- @Test public void externNativesWithHandleParamsGetNilSafetyWrapper() throws IOException { - // BJ natives (IS_BJ) with handle-type parameters must be wrapped at the IM level + // IS_BJ functions with handle-type parameters must be wrapped at the IM level // so that passing nil returns a safe default instead of crashing the Lua runtime. // GetUnitTypeId is a common.j native (IS_BJ) with a handle param, returns integer. // Result is used in print() so the optimizer does not dead-code-eliminate the call. diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index 911aaffff..a270f044d 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -30,6 +30,7 @@ import de.peeeq.wurstscript.utils.Utils; import de.peeeq.wurstscript.validation.GlobalCaches; import org.testng.Assert; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import java.io.*; @@ -66,6 +67,13 @@ public void _clearBefore() { GlobalCaches.clearAll(); } + @AfterMethod(alwaysRun = true) + public void _clearAfter() { + // Release all AST strong-refs held by caches so the GC can reclaim the + // stdlib copy from the previous test before the next test allocates its own. + GlobalCaches.clearAll(); + } + class TestConfig { private final String name; private boolean withStdLib;