From b2d78b543c28b1b76eaeb4d3f0ce9b17f92ffa6a Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 11 Oct 2026 13:19:02 +0200 Subject: [PATCH] Lua hashtables and the interpreter answer as Jass does Measured on the 3.0.0 client: a handle load is typed, saving a null handle stores nothing, FlushParentHashtable destroys the hashtable, and a null hashtable answers defaults and ignores writes. The Lua emulation tags each saved handle with the type of its native and checks it on load, skips null handles, points a flushed hashtable at a table which keeps nothing, and tests for a nil hashtable; the interpreter does the same, told which native was called. --- .../wurstio/CompiletimeFunctionRunner.java | 5 + .../jassinterpreter/NativeJassFunction.java | 29 ++- .../ReflectionNativeProvider.java | 15 +- .../providers/HashtableProvider.java | 170 ++++++++++++------ .../imtranslation/LuaHashtable.java | 63 ++++++- .../translation/LuaHashtableTranslation.java | 119 +++++++++--- .../lua/translation/LuaNatives.java | 115 ++++++++---- .../lua/translation/LuaTranslator.java | 12 ++ .../tests/CompiletimeHashtableTests.java | 8 +- .../wurstscript/tests/LuaHashtableTests.java | 90 +++++++--- .../wurstscript/tests/LuaNativesTests.java | 6 +- .../tests/LuaTranslationTests.java | 6 +- docs/WC3_RUNTIME.md | 2 + 13 files changed, 482 insertions(+), 158 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 73b3e9798..6be94c336 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -1138,6 +1138,11 @@ private ImExpr constantToExprHashtable(Element trace, ImVar htVar, IlConstHandle throw new CompileError(errorPos, "Unsupported value stored in HashMap: " + v + " // " + v.getClass().getSimpleName()); } } + if (map.isDestroyed()) { + // FlushParentHashtable destroyed it at compile time, so it is destroyed when the map starts too + batch.add(JassIm.ImFunctionCall(trace, findNative("FlushParentHashtable", errorPos), JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(htVar)), false, CallType.NORMAL)); + } if (!batch.isEmpty()) { getCompiletimeStateInitFunction().getBody().addAll(batch); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/NativeJassFunction.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/NativeJassFunction.java index fec9a7d38..77353c8fc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/NativeJassFunction.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/NativeJassFunction.java @@ -8,24 +8,47 @@ public class NativeJassFunction implements ExecutableJassFunction { + private final String name; private final Method method; private final Object provider; - public NativeJassFunction(Object provider, Method method) { + public NativeJassFunction(String name, Object provider, Method method) { + this.name = name; this.provider = provider; this.method = method; } + /** + * 1 when the method's first parameter is a String: it implements several natives, and is passed the name of the + * one called before the native's own arguments. + */ + int nameParameters() { + Class[] declared = method.getParameterTypes(); + return declared.length > 0 && declared[0] == String.class ? 1 : 0; + } + + /** The arguments the method is invoked with: the native's, after the native's name when the method takes it. */ + Object[] callArguments(ILconst[] arguments) { + if (nameParameters() == 0) { + return arguments; + } + Object[] result = new Object[arguments.length + 1]; + result[0] = name; + System.arraycopy(arguments, 0, result, 1, arguments.length); + return result; + } + @Override public ILconst execute(JassInterpreter jassInterpreter, ILconst[] arguments) { try { + int offset = nameParameters(); for (int i = 0; i < arguments.length; i++) { if (arguments[i] instanceof ILconstNull - && !method.getParameterTypes()[i].isAssignableFrom(arguments[i].getClass())) { + && !method.getParameterTypes()[i + offset].isAssignableFrom(arguments[i].getClass())) { arguments[i] = null; } } - Object result = method.invoke(provider, (Object[]) arguments); + Object result = method.invoke(provider, callArguments(arguments)); return (ILconst) result; } catch (IllegalArgumentException | IllegalAccessException e) { throw new Error(e); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java index c194e2859..defd2bc78 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java @@ -66,10 +66,10 @@ private void addProvider(Provider provider) { if (methodMap.containsKey(funcName)) { throw new Error("Trying to add multiple implementations of <" + funcName + ">"); } - methodMap.put(funcName, new NativeJassFunction(provider, method)); + methodMap.put(funcName, new NativeJassFunction(funcName, provider, method)); } } else { - methodMap.put(method.getName(), new NativeJassFunction(provider, method)); + methodMap.put(method.getName(), new NativeJassFunction(method.getName(), provider, method)); } } } @@ -81,23 +81,26 @@ public ILconst invoke(String funcname, ILconst[] args) throws NoSuchNativeExcept throw new NoSuchNativeException(""); } - if (candidate.getMethod().getParameterCount() == args.length) { + // A method whose first parameter is a String implements several natives and is told which one was called. + Class[] declared = candidate.getMethod().getParameterTypes(); + int offset = candidate.nameParameters(); + if (declared.length == args.length + offset) { String[] parameterTypes = new String[args.length]; for (int i = 0; i < args.length; i++) { parameterTypes[i] = "" + args[i]; - if (!candidate.getMethod().getParameterTypes()[i].isAssignableFrom(args[i].getClass())) { + if (!declared[i + offset].isAssignableFrom(args[i].getClass())) { if (args[i] instanceof ILconstNull) { // handle null as a special case and pass it to the native as a Java null args[i] = null; continue; } throw new Error("The native <" + funcname + "> expects different parameter " + i + "!" + - "\n\tExpected: " + candidate.getMethod().getParameterTypes()[i].getSimpleName() + " Actual: " + parameterTypes[i]); + "\n\tExpected: " + declared[i + offset].getSimpleName() + " Actual: " + parameterTypes[i]); } } } try { - return (ILconst) candidate.getMethod().invoke(candidate.getProvider(), (Object[]) args); + return (ILconst) candidate.getMethod().invoke(candidate.getProvider(), candidate.callArguments(args)); } catch (IllegalAccessException | InvocationTargetException e) { if (e.getCause() instanceof Error) { throw (Error) e.getCause(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/HashtableProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/HashtableProvider.java index bc7a99a70..d3a48bb93 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/HashtableProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/HashtableProvider.java @@ -6,9 +6,15 @@ import de.peeeq.wurstio.jassinterpreter.mocks.UnitMock; import de.peeeq.wurstscript.intermediatelang.*; import de.peeeq.wurstscript.intermediatelang.interpreter.AbstractInterpreter; +import de.peeeq.wurstscript.translation.imtranslation.LuaHashtable; import java.util.*; +/** + * The hashtable natives, as measured on the 3.0.0 client (the Lua backend's emulation, {@link LuaHashtable}, answers + * the same): handle loads are typed, saving a null handle stores nothing, FlushParentHashtable destroys the + * hashtable, and a null hashtable answers every load with its default and ignores every write. + */ public class HashtableProvider extends Provider { public HashtableProvider(AbstractInterpreter interpreter) { super(interpreter); @@ -16,8 +22,15 @@ public HashtableProvider(AbstractInterpreter interpreter) { public static class WurstHashtable { private final Map, Object>>> data = new LinkedHashMap<>(); + /** The type of the native each handle was saved with, under the same keys. */ + private final Map> handleTypes = new LinkedHashMap<>(); + /** Set by FlushParentHashtable, which destroys the hashtable: nothing saved afterwards stays. */ + private boolean destroyed; public void save(int parentKey, int childKey, Object value) { + if (destroyed) { + return; + } data.computeIfAbsent(parentKey, k -> new LinkedHashMap<>(8)) .computeIfAbsent(childKey, k -> new LinkedHashMap<>(2)) .put(value.getClass(), value); @@ -39,7 +52,24 @@ public boolean has(int parentKey, int childKey, Class clazz) { return child != null && child.containsKey(clazz); } + public void saveHandleType(int parentKey, int childKey, String type) { + if (!destroyed) { + handleTypes.computeIfAbsent(parentKey, k -> new LinkedHashMap<>(8)).put(childKey, type); + } + } + + public @org.eclipse.jdt.annotation.Nullable String handleType(int parentKey, int childKey) { + Map parent = handleTypes.get(parentKey); + return parent == null ? null : parent.get(childKey); + } + public void remove(int parentKey, int childKey, Class clazz) { + if (clazz == IlConstHandle.class) { + Map types = handleTypes.get(parentKey); + if (types != null) { + types.remove(childKey); + } + } Map, Object>> parent = data.get(parentKey); if (parent != null) { Map, Object> child = parent.get(childKey); @@ -57,10 +87,18 @@ public void remove(int parentKey, int childKey, Class clazz) { public void flushParent() { data.clear(); + handleTypes.clear(); + destroyed = true; + } + + /** Whether FlushParentHashtable destroyed it. */ + public boolean isDestroyed() { + return destroyed; } public void flushChild(int parentKey) { data.remove(parentKey); + handleTypes.remove(parentKey); } public static class Entry { @@ -96,33 +134,51 @@ public IlConstHandle InitHashtable() { "SaveQuestItemHandle", "SaveDefeatConditionHandle", "SaveTimerDialogHandle", "SaveLeaderboardHandle", "SaveMultiboardHandle", "SaveMultiboardItemHandle", "SaveTrackableHandle", "SaveDialogHandle", "SaveButtonHandle", "SaveTextTagHandle", "SaveLightningHandle", "SaveImageHandle", "SaveUbersplatHandle", "SaveRegionHandle", "SaveFogStateHandle", "SaveFogModifierHandle", "SaveAgentHandle", - "SaveHashtableHandle", + "SaveHashtableHandle", "SaveFrameHandle", }) - public void Save(IlConstHandle ht, ILconstInt key1, ILconstInt key2, ILconst value) { - if (value instanceof ILconstNull) { + public void Save(String nativeName, IlConstHandle ht, ILconstInt key1, ILconstInt key2, ILconst value) { + if (ht == null || value instanceof ILconstNull) { // Measured on the 3.0.0 client: saving a null handle stores nothing, and a handle already // saved under the key stays. return; } WurstHashtable table = (WurstHashtable) ht.getObj(); table.save(key1.getVal(), key2.getVal(), value); + if (value instanceof IlConstHandle) { + table.saveHandleType(key1.getVal(), key2.getVal(), handleTypeOf(nativeName)); + } + } + + private static String handleTypeOf(String nativeName) { + LuaHashtable.Op op = LuaHashtable.opOfName(nativeName); + if (op == null || op.handleType() == null) { + throw new IllegalStateException("not a handle save or load: " + nativeName); + } + return op.handleType(); + } + + private static @org.eclipse.jdt.annotation.Nullable WurstHashtable tableOf(IlConstHandle ht) { + return ht == null ? null : (WurstHashtable) ht.getObj(); } public ILconstInt LoadInteger(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - ILconstInt res = table.get(key1.getVal(), key2.getVal(), ILconstInt.class); + WurstHashtable table = tableOf(ht); + ILconstInt res = table == null ? null : table.get(key1.getVal(), key2.getVal(), ILconstInt.class); return res != null ? res : ILconstInt.create(0); } public ILconstReal LoadReal(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - ILconstReal res = table.get(key1.getVal(), key2.getVal(), ILconstReal.class); + WurstHashtable table = tableOf(ht); + ILconstReal res = table == null ? null : table.get(key1.getVal(), key2.getVal(), ILconstReal.class); return res != null ? res : new ILconstReal(0); } /** Measured on the 3.0.0 client: saving a null string does store it, replacing what was there, * and it loads as null. It is kept as {@link ILconstNull} in the string slot. */ public void SaveStr(IlConstHandle ht, ILconstInt key1, ILconstInt key2, ILconst value) { + if (ht == null) { + return; + } RemoveSavedString(ht, key1, key2); WurstHashtable table = (WurstHashtable) ht.getObj(); table.save(key1.getVal(), key2.getVal(), value); @@ -130,7 +186,10 @@ public void SaveStr(IlConstHandle ht, ILconstInt key1, ILconstInt key2, ILconst /** Measured on the 3.0.0 client: a missing string loads as null, not as "". */ public ILconst LoadStr(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - WurstHashtable table = (WurstHashtable) ht.getObj(); + WurstHashtable table = tableOf(ht); + if (table == null) { + return ILconstNull.instance(); + } ILconst res = table.get(key1.getVal(), key2.getVal(), ILconstString.class); if (res == null) { res = table.get(key1.getVal(), key2.getVal(), ILconstNull.class); @@ -139,64 +198,63 @@ public ILconst LoadStr(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { } public ILconstBool LoadBoolean(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - ILconstBool res = table.get(key1.getVal(), key2.getVal(), ILconstBool.class); + WurstHashtable table = tableOf(ht); + ILconstBool res = table == null ? null : table.get(key1.getVal(), key2.getVal(), ILconstBool.class); return res != null ? res : ILconstBool.FALSE; } - @Implements(funcNames = {"LoadPlayerHandle", "LoadAbilityHandle", + @Implements(funcNames = {"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"}) - public ILconst LoadHandle(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - IlConstHandle handle = table.get(key1.getVal(), key2.getVal(), IlConstHandle.class); - return handle == null ? ILconstNull.instance() : handle; - } - - // Measured on the 3.0.0 client: handle loads are typed. A saved unit loads as null through - // LoadItemHandle and as itself through LoadWidgetHandle. - - public ILconst LoadUnitHandle(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - return loadHandleOf(ht, key1, key2, UnitMock.class); - } - - public ILconst LoadItemHandle(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - return loadHandleOf(ht, key1, key2, ItemMock.class); - } - - public ILconst LoadDestructableHandle(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - return loadHandleOf(ht, key1, key2, DestructableMock.class); - } - - public ILconst LoadWidgetHandle(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { - return loadHandleOf(ht, key1, key2, UnitMock.class, ItemMock.class, DestructableMock.class); - } - - private ILconst loadHandleOf(IlConstHandle ht, ILconstInt key1, ILconstInt key2, Class... types) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - IlConstHandle handle = table.get(key1.getVal(), key2.getVal(), IlConstHandle.class); - if (handle != null) { - for (Class type : types) { - if (type.isInstance(handle.getObj())) { - return handle; - } - } + "LoadHashtableHandle", "LoadFrameHandle"}) + public ILconst LoadHandle(String nativeName, IlConstHandle ht, ILconstInt key1, ILconstInt key2) { + WurstHashtable table = tableOf(ht); + IlConstHandle handle = table == null ? null : table.get(key1.getVal(), key2.getVal(), IlConstHandle.class); + if (handle == null) { + return ILconstNull.instance(); + } + // Measured on the 3.0.0 client: handle loads are typed. A saved unit loads as itself through LoadUnitHandle + // and LoadWidgetHandle and as null through LoadItemHandle or LoadTimerHandle. + String type = handleTypeOf(nativeName); + String saved = table.handleType(key1.getVal(), key2.getVal()); + if (saved == null || !LuaHashtable.acceptedTags(type).contains(saved)) { + return ILconstNull.instance(); } - return ILconstNull.instance(); + if (saved.equals(type) || (type.equals("Widget") && LuaHashtable.WIDGET_TYPES.contains(saved))) { + return handle; + } + // Saved through a supertype (a widget, an agent): what the handle is decides, where the mock tells. + Class required = switch (type) { + case "Unit" -> UnitMock.class; + case "Item" -> ItemMock.class; + case "Destructable" -> DestructableMock.class; + default -> null; + }; + Object obj = handle.getObj(); + if (type.equals("Widget")) { + return obj instanceof UnitMock || obj instanceof ItemMock || obj instanceof DestructableMock + || saved.equals("Widget") ? handle : ILconstNull.instance(); + } + return required == null || required.isInstance(obj) ? handle : ILconstNull.instance(); } + /** Measured on the 3.0.0 client: the hashtable is destroyed, nothing saved to it afterwards stays. */ public void FlushParentHashtable(IlConstHandle ht) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - table.flushParent(); + WurstHashtable table = tableOf(ht); + if (table != null) { + table.flushParent(); + } } public void FlushChildHashtable(IlConstHandle ht, ILconstInt parentKey) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - table.flushChild(parentKey.getVal()); + WurstHashtable table = tableOf(ht); + if (table != null) { + table.flushChild(parentKey.getVal()); + } } public void RemoveSavedInteger(IlConstHandle ht, ILconstInt key1, ILconstInt key2) { @@ -242,12 +300,14 @@ public ILconstBool HaveSavedHandle(IlConstHandle ht, ILconstInt key1, ILconstInt } private void removeSaved(IlConstHandle ht, ILconstInt key1, ILconstInt key2, Class clazz) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - table.remove(key1.getVal(), key2.getVal(), clazz); + WurstHashtable table = tableOf(ht); + if (table != null) { + table.remove(key1.getVal(), key2.getVal(), clazz); + } } private boolean haveSaved(IlConstHandle ht, ILconstInt key1, ILconstInt key2, Class clazz) { - WurstHashtable table = (WurstHashtable) ht.getObj(); - return table.has(key1.getVal(), key2.getVal(), clazz); + WurstHashtable table = tableOf(ht); + return table != null && table.has(key1.getVal(), key2.getVal(), clazz); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java index fc22f423d..7333fa66d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java @@ -11,8 +11,19 @@ * The Jass hashtable natives on Lua. A hashtable is a table with one subtable per kind of value, as Jass keeps the * kinds apart under the same keys: {@code { __wurst_ht_int = {}, __wurst_ht_bool = {}, ... }}. A subtable maps a * parent key to a child table, which maps a child key to the value. Only InitHashtable creates one (a compile-time - * hashtable is emitted as a call of it and its saves) and FlushParentHashtable gives it five new subtables, so every - * subtable is always there. + * hashtable is emitted as a call of it and its saves), so every subtable is always there. + * + *

Each operation answers what Jass answers, measured on the 3.0.0 client: + *

    + *
  • A handle load is typed: a saved unit loads as itself through LoadUnitHandle and LoadWidgetHandle and as + * null through LoadItemHandle or LoadTimerHandle. Lua cannot tell a handle's type, so a save also writes the + * type of its native as a tag into {@link Values#HANDLE_TYPE}, and a load answers the handle only for a tag it + * accepts ({@link #acceptedTags}).
  • + *
  • Saving a null handle stores nothing, and a handle saved there before stays.
  • + *
  • FlushParentHashtable destroys the hashtable: later saves do not stick and every load answers its default. + * It points every subtable at one shared table which ignores writes.
  • + *
  • A null hashtable answers every load with its default and ignores every write.
  • + *
* *

{@link LuaNativeLowering} points every call of one of these natives at a stub {@code __wurst_}, made once * per native and kept in {@link ImTranslator#luaHashtableStubs}. The optimizer and the backend recognise the stub by @@ -34,7 +45,9 @@ public enum Kind { /** The kinds of value, each kept in its own subtable. */ public enum Values { INT("__wurst_ht_int"), BOOL("__wurst_ht_bool"), REAL("__wurst_ht_real"), STR("__wurst_ht_str"), - HANDLE("__wurst_ht_handle"); + HANDLE("__wurst_ht_handle"), + /** The tag of each saved handle, the type of the native which saved it, under the same keys. */ + HANDLE_TYPE("__wurst_ht_htype"); /** The field of the hashtable which holds the subtable. */ public final String field; @@ -44,8 +57,11 @@ public enum Values { } } - /** An operation on the values of one kind, or on all of them ({@code values} is null). */ - public record Op(Kind kind, @Nullable Values values) { + /** + * An operation on the values of one kind, or on all of them ({@code values} is null). {@code handleType} is the + * type of a handle save or load, the tag a save writes and the type a load asks for. + */ + public record Op(Kind kind, @Nullable Values values, @Nullable String handleType) { } /** The handle types with a Save and a Load native of their own; agent has only a Save. */ @@ -56,6 +72,12 @@ public record Op(Kind kind, @Nullable Values values) { "MultiboardItem", "Trackable", "Dialog", "Button", "TextTag", "Lightning", "Image", "Ubersplat", "Region", "FogState", "FogModifier", "Hashtable", "Frame"); + /** The tag of a handle saved as an agent, whose own type is not known. */ + public static final String AGENT = "Agent"; + + /** The handle types a widget can be; a handle saved as one of them is a widget. */ + public static final List WIDGET_TYPES = List.of("Unit", "Item", "Destructable"); + /** The operations by stub name. Looked up, never iterated. */ private static final Map OPS = new HashMap<>(); @@ -69,11 +91,28 @@ public record Op(Kind kind, @Nullable Values values) { family(Values.STR, "SaveStr", "LoadStr", "HaveSavedString", "RemoveSavedString"); put("HaveSavedHandle", Kind.HAVE, Values.HANDLE); put("RemoveSavedHandle", Kind.REMOVE, Values.HANDLE); - put("SaveAgentHandle", Kind.SAVE, Values.HANDLE); + OPS.put(stubName("SaveAgentHandle"), new Op(Kind.SAVE, Values.HANDLE, AGENT)); for (String type : HANDLE_TYPES) { - put("Save" + type + "Handle", Kind.SAVE, Values.HANDLE); - put("Load" + type + "Handle", Kind.LOAD, Values.HANDLE); + OPS.put(stubName("Save" + type + "Handle"), new Op(Kind.SAVE, Values.HANDLE, type)); + OPS.put(stubName("Load" + type + "Handle"), new Op(Kind.LOAD, Values.HANDLE, type)); + } + } + + /** + * The tags a load of {@code type} answers the handle for: its own type and, for a widget, the widget types. A + * handle saved through a supertype (a widget, an agent) may be of the type loaded, which Lua cannot tell, so + * those tags are accepted too. + */ + public static List acceptedTags(String type) { + List tags = new java.util.ArrayList<>(); + tags.add(type); + if (type.equals("Widget")) { + tags.addAll(WIDGET_TYPES); + } else if (WIDGET_TYPES.contains(type)) { + tags.add("Widget"); } + tags.add(AGENT); + return tags; } private static void family(Values values, String save, String load, String have, String remove) { @@ -84,7 +123,13 @@ private static void family(Values values, String save, String load, String have, } private static void put(String nativeName, Kind kind, @Nullable Values values) { - OPS.put(stubName(nativeName), new Op(kind, values)); + OPS.put(stubName(nativeName), new Op(kind, values, null)); + } + + /** The operation of the hashtable native or stub {@code name}, or null. */ + public static @Nullable Op opOfName(String name) { + Op op = OPS.get(name); + return op != null ? op : OPS.get(stubName(name)); } private LuaHashtable() { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java index b9ec02d1d..aa5207ecb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java @@ -17,15 +17,21 @@ * the subtable of the value kind: *

    *
  • InitHashtable is the table constructor;
  • - *
  • a load is {@code (h.T[p] or __wurst_htEmpty)[c]}, then the default the helper answers: {@code or 0}, - * {@code or 0.0}, {@code == true}, and nothing for a string or a handle, which are nil when absent;
  • + *
  • a load is {@code (h and h.T[p] or __wurst_htEmpty)[c]}, then the default the helper answers: + * {@code or 0}, {@code or 0.0}, {@code == true}, and nothing for a string; a handle load answers the value only + * when the set of tags it accepts holds the tag saved with it, {@code (__wurst_htAccepts_X[tag] and value or nil)} + * with tag the same read of {@code __wurst_ht_htype};
  • *
  • a HaveSaved test is the same read {@code ~= nil};
  • - *
  • a save is {@code (h.T[p] or __wurst_htNewChild(h.T, p))[c] = v};
  • - *
  • a remove is {@code if h.T[p] then h.T[p][c] = nil end}. Not through the empty child: in Lua 5.3 - * {@code t[k] = nil} under an absent key inserts a dead key, and nothing may write the shared table;
  • - *
  • FlushChildHashtable is {@code h.T[p] = nil} and FlushParentHashtable {@code h.T = {}}, for each T.
  • + *
  • a save is {@code if h then (h.T[p] or __wurst_htNewChild(h.T, p))[c] = v end}; a handle save also writes + * its tag, and does nothing for a null handle ({@code if h and v ~= nil then});
  • + *
  • a remove is {@code if h and h.T[p] then h.T[p][c] = nil end}, for a handle in the tag subtable too. Not + * through the empty child: in Lua 5.3 {@code t[k] = nil} under an absent key inserts a dead key, and nothing may + * write the shared table;
  • + *
  • FlushChildHashtable is {@code h.T[p] = nil} and FlushParentHashtable {@code h.T = __wurst_htFlushed}, for + * each T, under {@code if h then}. The flushed table ignores writes, so a flushed hashtable keeps nothing.
  • *
- * A missing key, a nil hashtable and a nil key on a store raise or read nil exactly as in the helpers. + * Each answers what Jass answers ({@link LuaHashtable}), as the helpers do; a nil hashtable reads defaults and + * ignores writes. * *

A call evaluates each argument once and in order, before the body. Printed in place an operand is read * once more on some paths (a save which creates the child, a remove) or not at all (the child key of a remove @@ -39,6 +45,8 @@ final class LuaHashtableTranslation { /** The main-chunk locals the in-place operations use, each declared when first used. */ static final String EMPTY = "__wurst_htEmpty"; static final String NEW_CHILD = "__wurst_htNewChild"; + static final String FLUSHED = "__wurst_htFlushed"; + static final String ACCEPTS = "__wurst_htAccepts_"; private LuaHashtableTranslation() { } @@ -54,9 +62,7 @@ static LuaExpr expression(ImFunctionCall call, LuaHashtable.Op op, LuaTranslator return LuaAst.LuaExprFunctionCall(tr.hashtableHelper(call.getFunc()), tr.translateExprList(args)); } List operands = translate(args, tr); - LuaExpr child = LuaAst.LuaExprBinary(entry(operands.get(0), operands.get(1), op.values()), LuaAst.LuaOpOr(), - LuaAst.LuaExprVarAccess(tr.hashtableEmpty())); - LuaExpr value = LuaAst.LuaExprArrayAccess(child, LuaAst.LuaExprlist(operands.get(2))); + LuaExpr value = read(operands, op.values(), tr); if (op.kind() == LuaHashtable.Kind.HAVE) { return LuaAst.LuaExprBinary(value, LuaAst.LuaOpUnequals(), LuaAst.LuaExprNull()); } @@ -64,10 +70,33 @@ static LuaExpr expression(ImFunctionCall call, LuaHashtable.Op op, LuaTranslator case INT -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpOr(), LuaAst.LuaExprIntVal("0")); case REAL -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpOr(), LuaAst.LuaExprRealVal("0.0")); case BOOL -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpEquals(), LuaAst.LuaExprBoolVal(true)); - case STR, HANDLE -> value; + case STR -> value; + case HANDLE -> { + LuaExpr accepted = LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(tr.hashtableAccepts(op.handleType())), + LuaAst.LuaExprlist(read(copies(operands), LuaHashtable.Values.HANDLE_TYPE, tr))); + yield LuaAst.LuaExprBinary(LuaAst.LuaExprBinary(accepted, LuaAst.LuaOpAnd(), value), LuaAst.LuaOpOr(), + LuaAst.LuaExprNull()); + } + case HANDLE_TYPE -> throw new IllegalStateException("no operation reads the tags: " + op); }; } + /** {@code (h and h.T[p] or __wurst_htEmpty)[c]}: the value under the keys, nil when there is none. */ + private static LuaExpr read(List operands, LuaHashtable.Values values, LuaTranslator tr) { + LuaExpr child = LuaAst.LuaExprBinary( + LuaAst.LuaExprBinary(operands.get(0).copy(), LuaAst.LuaOpAnd(), entry(operands.get(0).copy(), operands.get(1).copy(), values)), + LuaAst.LuaOpOr(), LuaAst.LuaExprVarAccess(tr.hashtableEmpty())); + return LuaAst.LuaExprArrayAccess(child, LuaAst.LuaExprlist(operands.get(2).copy())); + } + + private static List copies(List operands) { + List result = new ArrayList<>(); + for (LuaExpr operand : operands) { + result.add(operand.copy()); + } + return result; + } + /** * A store, a remove or a flush in place as statements, when no operand has an effect. Returns false for * anything else, which is then translated as an expression. @@ -83,36 +112,60 @@ static boolean statement(ImFunctionCall call, LuaHashtable.Op op, List operands = translate(args, tr); LuaExpr h = operands.get(0); + LuaStatements body = LuaAst.LuaStatements(); + LuaExpr guard = h.copy(); switch (kind) { case SAVE -> { - LuaExpr p = operands.get(1); - LuaExpr created = LuaAst.LuaExprFunctionCall(tr.hashtableNewChild(), - LuaAst.LuaExprlist(subtable(h.copy(), op.values()), p.copy())); - LuaExpr child = LuaAst.LuaExprBinary(entry(h, p, op.values()), LuaAst.LuaOpOr(), created); - res.add(LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess(child, LuaAst.LuaExprlist(operands.get(2))), - operands.get(3))); + LuaExpr value = operands.get(3); + if (op.values() == LuaHashtable.Values.HANDLE) { + if (value instanceof LuaExprNull) { + // saving a null handle stores nothing + return true; + } + guard = LuaAst.LuaExprBinary(guard, LuaAst.LuaOpAnd(), + LuaAst.LuaExprBinary(value.copy(), LuaAst.LuaOpUnequals(), LuaAst.LuaExprNull())); + body.add(store(operands, LuaHashtable.Values.HANDLE_TYPE, LuaAst.LuaExprStringVal(op.handleType()), tr)); + } + body.add(0, store(operands, op.values(), value, tr)); } case REMOVE -> { LuaExpr p = operands.get(1); - LuaAssignment remove = LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess( - entry(h.copy(), p.copy(), op.values()), LuaAst.LuaExprlist(operands.get(2))), LuaAst.LuaExprNull()); - res.add(LuaAst.LuaIf(entry(h, p, op.values()), LuaAst.LuaStatements(remove), LuaAst.LuaStatements())); + guard = LuaAst.LuaExprBinary(guard, LuaAst.LuaOpAnd(), entry(h.copy(), p.copy(), op.values())); + body.add(LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess(entry(h.copy(), p.copy(), op.values()), + LuaAst.LuaExprlist(operands.get(2).copy())), LuaAst.LuaExprNull())); + if (op.values() == LuaHashtable.Values.HANDLE) { + // every handle has a tag under the same keys, so the tag subtable has the child too + body.add(LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess( + entry(h.copy(), p.copy(), LuaHashtable.Values.HANDLE_TYPE), LuaAst.LuaExprlist(operands.get(2).copy())), + LuaAst.LuaExprNull())); + } } case FLUSH_CHILD -> { for (LuaHashtable.Values values : LuaHashtable.Values.values()) { - res.add(LuaAst.LuaAssignment(entry(h.copy(), operands.get(1).copy(), values), LuaAst.LuaExprNull())); + body.add(LuaAst.LuaAssignment(entry(h.copy(), operands.get(1).copy(), values), LuaAst.LuaExprNull())); } } case FLUSH_PARENT -> { for (LuaHashtable.Values values : LuaHashtable.Values.values()) { - res.add(LuaAst.LuaAssignment(subtable(h.copy(), values), emptyTable())); + body.add(LuaAst.LuaAssignment(subtable(h.copy(), values), LuaAst.LuaExprVarAccess(tr.hashtableFlushed()))); } } default -> throw new IllegalStateException("not a statement: " + op); } + res.add(LuaAst.LuaIf(guard, body, LuaAst.LuaStatements())); return true; } + /** {@code (h.T[p] or __wurst_htNewChild(h.T, p))[c] = v}. */ + private static LuaStatement store(List operands, LuaHashtable.Values values, LuaExpr value, LuaTranslator tr) { + LuaExpr h = operands.get(0); + LuaExpr p = operands.get(1); + LuaExpr created = LuaAst.LuaExprFunctionCall(tr.hashtableNewChild(), + LuaAst.LuaExprlist(subtable(h.copy(), values), p.copy())); + LuaExpr child = LuaAst.LuaExprBinary(entry(h.copy(), p.copy(), values), LuaAst.LuaOpOr(), created); + return LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess(child, LuaAst.LuaExprlist(operands.get(2).copy())), value.copy()); + } + /** {@code { __wurst_ht_int = {}, ... }}: the representation InitHashtable creates. */ private static LuaExpr newHashtable() { LuaTableFields fields = LuaAst.LuaTableFields(); @@ -129,6 +182,28 @@ static LuaVariable createEmpty(LuaTranslator tr) { return empty; } + /** + * The table every subtable of a flushed hashtable is: Jass destroys a hashtable on FlushParentHashtable, so + * nothing written to it stays. Its children are never stored, and every read under it answers nil. + */ + static LuaVariable createFlushed(LuaTranslator tr) { + LuaVariable flushed = LuaAst.LuaVariable(FLUSHED, LuaAst.LuaLiteral( + "setmetatable({}, { __newindex = function() end })")); + tr.declareChunkLocal(flushed); + return flushed; + } + + /** {@code { Unit = true, Widget = true, Agent = true }}: the tags a load of {@code type} answers the handle for. */ + static LuaVariable createAccepts(String type, LuaTranslator tr) { + LuaTableFields fields = LuaAst.LuaTableFields(); + for (String tag : LuaHashtable.acceptedTags(type)) { + fields.add(LuaAst.LuaTableNamedField(tag, LuaAst.LuaExprBoolVal(true))); + } + LuaVariable accepts = LuaAst.LuaVariable(ACCEPTS + type, LuaAst.LuaTableConstructor(fields)); + tr.declareChunkLocal(accepts); + return accepts; + } + /** {@code (t, p)}: stores a new child under the parent key and answers it. */ static LuaFunction createNewChild(LuaTranslator tr) { LuaVariable table = LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()); 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 7a793649e..fc00947b0 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 @@ -3,6 +3,8 @@ import de.peeeq.wurstscript.luaAst.LuaAst; import de.peeeq.wurstscript.luaAst.LuaFunction; import de.peeeq.wurstscript.luaAst.LuaVariable; +import de.peeeq.wurstscript.translation.imtranslation.LuaHashtable; +import org.eclipse.jdt.annotation.Nullable; import java.util.Arrays; import java.util.Collections; @@ -363,55 +365,71 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("for k in pairs(t) do t[k] = nil end")); }); - // The hashtable natives; LuaHashtable has the representation. The backend prints a call of a __wurst_ stub - // where it is (LuaHashtableTranslation), so these bodies run only where it cannot, and for a hashtable native - // declared without the BJ flag. InitHashtable creates every subtable and nothing removes one. + // The hashtable natives; LuaHashtable has the representation and what each answers. The backend prints a call + // of a __wurst_ stub where it is (LuaHashtableTranslation), so these bodies run only where it cannot, and for + // a hashtable native declared without the BJ flag. A nil hashtable reads defaults and ignores writes. addNative(Arrays.asList("InitHashtable", "__wurst_InitHashtable"), f -> - f.getBody().add(LuaAst.LuaLiteral("return { __wurst_ht_int = {}, __wurst_ht_bool = {}, __wurst_ht_real = {}, __wurst_ht_str = {}, __wurst_ht_handle = {} }"))); - - addHashtableSave(withWurstPrefix("SaveInteger"), "__wurst_ht_int"); - addHashtableSave(withWurstPrefix("SaveBoolean"), "__wurst_ht_bool"); - addHashtableSave(withWurstPrefix("SaveReal"), "__wurst_ht_real"); - addHashtableSave(withWurstPrefix("SaveStr"), "__wurst_ht_str"); - addHashtableSave(withWurstPrefix(HASHTABLE_HANDLE_SAVE_NAMES), "__wurst_ht_handle"); + f.getBody().add(LuaAst.LuaLiteral("return { __wurst_ht_int = {}, __wurst_ht_bool = {}, __wurst_ht_real = {}, " + + "__wurst_ht_str = {}, __wurst_ht_handle = {}, __wurst_ht_htype = {} }"))); + + addHashtableSave(withWurstPrefix("SaveInteger"), "__wurst_ht_int", null); + addHashtableSave(withWurstPrefix("SaveBoolean"), "__wurst_ht_bool", null); + addHashtableSave(withWurstPrefix("SaveReal"), "__wurst_ht_real", null); + addHashtableSave(withWurstPrefix("SaveStr"), "__wurst_ht_str", null); + for (String name : HASHTABLE_HANDLE_SAVE_NAMES) { + addHashtableSave(withWurstPrefix(name), "__wurst_ht_handle", handleTypeOf(name)); + } addHashtableLoad(withWurstPrefix("LoadInteger"), "__wurst_ht_int", "0", "x[c] or 0"); // true only for a stored true, as LoadBoolean answers false when nothing is stored addHashtableLoad(withWurstPrefix("LoadBoolean"), "__wurst_ht_bool", "false", "x[c] == true"); addHashtableLoad(withWurstPrefix("LoadReal"), "__wurst_ht_real", "0.0", "x[c] or 0.0"); - // nil when absent, as the string and handle loads have always answered + // nil when absent, as the string loads have always answered addHashtableLoad(withWurstPrefix("LoadStr"), "__wurst_ht_str", "nil", "x[c]"); - addHashtableLoad(withWurstPrefix(HASHTABLE_HANDLE_LOAD_NAMES), "__wurst_ht_handle", "nil", "x[c]"); + for (String name : HASHTABLE_HANDLE_LOAD_NAMES) { + String type = handleTypeOf(name); + StringBuilder accepted = new StringBuilder(); + for (String tag : LuaHashtable.acceptedTags(type)) { + accepted.append(accepted.length() == 0 ? "" : " or ").append("tag == \"").append(tag).append("\""); + } + // typed: the handle only for a tag this load accepts (LuaHashtable.acceptedTags) + addHashtableEntryOperation(withWurstPrefix(name), "__wurst_ht_handle", "nil", + "if x == nil then return nil end", + "local tag = h.__wurst_ht_htype[p]", + "tag = tag and tag[c]", + "if " + accepted + " then return x[c] end", + "return nil"); + } - addHashtableEntryOperation(withWurstPrefix("HaveSavedInteger"), "__wurst_ht_int", "return x ~= nil and x[c] ~= nil"); - addHashtableEntryOperation(withWurstPrefix("HaveSavedBoolean"), "__wurst_ht_bool", "return x ~= nil and x[c] ~= nil"); - addHashtableEntryOperation(withWurstPrefix("HaveSavedReal"), "__wurst_ht_real", "return x ~= nil and x[c] ~= nil"); - addHashtableEntryOperation(withWurstPrefix("HaveSavedString"), "__wurst_ht_str", "return x ~= nil and x[c] ~= nil"); - addHashtableEntryOperation(withWurstPrefix("HaveSavedHandle"), "__wurst_ht_handle", "return x ~= nil and x[c] ~= nil"); + for (String[] have : new String[][]{{"HaveSavedInteger", "__wurst_ht_int"}, {"HaveSavedBoolean", "__wurst_ht_bool"}, + {"HaveSavedReal", "__wurst_ht_real"}, {"HaveSavedString", "__wurst_ht_str"}, {"HaveSavedHandle", "__wurst_ht_handle"}}) { + addHashtableEntryOperation(withWurstPrefix(have[0]), have[1], "false", "return x ~= nil and x[c] ~= nil"); + } - addHashtableEntryOperation(withWurstPrefix("RemoveSavedInteger"), "__wurst_ht_int", "if x then x[c] = nil end"); - addHashtableEntryOperation(withWurstPrefix("RemoveSavedBoolean"), "__wurst_ht_bool", "if x then x[c] = nil end"); - addHashtableEntryOperation(withWurstPrefix("RemoveSavedReal"), "__wurst_ht_real", "if x then x[c] = nil end"); - addHashtableEntryOperation(withWurstPrefix("RemoveSavedString"), "__wurst_ht_str", "if x then x[c] = nil end"); - addHashtableEntryOperation(withWurstPrefix("RemoveSavedHandle"), "__wurst_ht_handle", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedInteger"), "__wurst_ht_int", "", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedBoolean"), "__wurst_ht_bool", "", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedReal"), "__wurst_ht_real", "", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedString"), "__wurst_ht_str", "", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedHandle"), "__wurst_ht_handle", "", + "if x then x[c] = nil end", "x = h.__wurst_ht_htype[p]", "if x then x[c] = nil end"); addNative(withWurstPrefix("FlushChildHashtable"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_int[p] = nil")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_bool[p] = nil")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_real[p] = nil")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_str[p] = nil")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_handle[p] = nil")); + f.getBody().add(LuaAst.LuaLiteral("if h == nil then return end")); + for (LuaHashtable.Values values : LuaHashtable.Values.values()) { + f.getBody().add(LuaAst.LuaLiteral("h." + values.field + "[p] = nil")); + } }); + // Jass destroys a flushed hashtable: every subtable becomes a table which keeps nothing written to it addNative(withWurstPrefix("FlushParentHashtable"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_int = {}")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_bool = {}")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_real = {}")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_str = {}")); - f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_handle = {}")); + f.getBody().add(LuaAst.LuaLiteral("if h == nil then return end")); + f.getBody().add(LuaAst.LuaLiteral("local flushed = setmetatable({}, { __newindex = function() end })")); + for (LuaHashtable.Values values : LuaHashtable.Values.values()) { + f.getBody().add(LuaAst.LuaLiteral("h." + values.field + " = flushed")); + } }); addNative("__wurst_GetHandleId", f -> { @@ -454,25 +472,41 @@ private static void addNative(Iterable names, Consumer g) { } /** Stores {@code i} under the child key, creating the child table under the parent key first if needed. */ - private static void addHashtableSave(Iterable names, String subtable) { + /** + * {@code handleType} is the tag a handle save writes with the value, null for any other save; a null handle + * stores nothing. + */ + private static void addHashtableSave(Iterable names, String subtable, @Nullable String handleType) { addNative(names, f -> { addParams(f, "h", "p", "c", "i"); + f.getBody().add(LuaAst.LuaLiteral(handleType == null ? "if h == nil then return end" + : "if h == nil or i == nil then return end")); f.getBody().add(LuaAst.LuaLiteral("local t = h." + subtable)); f.getBody().add(LuaAst.LuaLiteral("local x = t[p]")); f.getBody().add(LuaAst.LuaLiteral("if x == nil then x = {} t[p] = x end")); f.getBody().add(LuaAst.LuaLiteral("x[c] = i")); + if (handleType != null) { + f.getBody().add(LuaAst.LuaLiteral("t = h.__wurst_ht_htype")); + f.getBody().add(LuaAst.LuaLiteral("x = t[p]")); + f.getBody().add(LuaAst.LuaLiteral("if x == nil then x = {} t[p] = x end")); + f.getBody().add(LuaAst.LuaLiteral("x[c] = \"" + handleType + "\"")); + } }); } /** {@code absent} is the answer when there is no child table, {@code value} the answer read from it. */ private static void addHashtableLoad(Iterable names, String subtable, String absent, String value) { - addHashtableEntryOperation(names, subtable, "if x == nil then return " + absent + " end", "return " + value); + addHashtableEntryOperation(names, subtable, absent, "if x == nil then return " + absent + " end", "return " + value); } - /** Reads the child table {@code x} under the parent key, then runs {@code body} on it. */ - private static void addHashtableEntryOperation(Iterable names, String subtable, String... body) { + /** + * Reads the child table {@code x} under the parent key, then runs {@code body} on it; a nil hashtable answers + * {@code nilAnswer} (nothing for an operation which answers nothing). + */ + private static void addHashtableEntryOperation(Iterable names, String subtable, String nilAnswer, String... body) { addNative(names, f -> { addParams(f, "h", "p", "c"); + f.getBody().add(LuaAst.LuaLiteral("if h == nil then return " + nilAnswer + " end")); f.getBody().add(LuaAst.LuaLiteral("local x = h." + subtable + "[p]")); for (String line : body) { f.getBody().add(LuaAst.LuaLiteral(line)); @@ -480,6 +514,15 @@ private static void addHashtableEntryOperation(Iterable names, String su }); } + /** The handle type of a handle save or load native ("SaveUnitHandle" is "Unit"), as LuaHashtable tags it. */ + private static String handleTypeOf(String nativeName) { + LuaHashtable.Op op = LuaHashtable.opOfName(nativeName); + if (op == null || op.handleType() == null) { + throw new IllegalStateException("not a handle save or load: " + nativeName); + } + return op.handleType(); + } + private static void addParams(LuaFunction f, String... names) { for (String name : names) { f.getParams().add(LuaAst.LuaVariable(name, LuaAst.LuaNoExpr())); 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 e74a7208a..67d1d1e8b 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 @@ -351,6 +351,9 @@ LuaPolyfillSetup.OldGenericsHelpers oldGenericsHelpers() { private final Set definedHashtableHelpers = Collections.newSetFromMap(new IdentityHashMap<>()); private final Lazy hashtableEmpty = Lazy.create(() -> LuaHashtableTranslation.createEmpty(this)); private final Lazy hashtableNewChild = Lazy.create(() -> LuaHashtableTranslation.createNewChild(this)); + private final Lazy hashtableFlushed = Lazy.create(() -> LuaHashtableTranslation.createFlushed(this)); + /** Looked up by type, never iterated: the declaration order is the order of first use. */ + private final Map hashtableAccepts = new HashMap<>(); /** * The helper a call of a hashtable stub calls where the operation cannot be printed in place @@ -375,6 +378,15 @@ LuaFunction hashtableNewChild() { return hashtableNewChild.get(); } + LuaVariable hashtableFlushed() { + return hashtableFlushed.get(); + } + + /** The set of tags a handle load of {@code type} accepts, one main-chunk local per type, declared on first use. */ + LuaVariable hashtableAccepts(String type) { + return hashtableAccepts.computeIfAbsent(type, t -> LuaHashtableTranslation.createAccepts(t, this)); + } + LuaFunction fromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction stringToIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_stringToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction stringFromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_stringFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java index 8a36cd65c..744088c4c 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeHashtableTests.java @@ -71,6 +71,10 @@ public void flushedAndRemovedEntriesAreNotReplayed() { " testSuccess()")); } + /** + * Measured on the 3.0.0 client, FlushParentHashtable destroys the hashtable: what was saved is gone and nothing + * saved afterwards stays. A table destroyed at compile time is destroyed when the map starts, too. + */ @Test public void flushingTheWholeTableLeavesNothingToReplay() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true).lines(program( @@ -81,7 +85,9 @@ public void flushingTheWholeTableLeavesNothingToReplay() { " FlushParentHashtable(h)", " SaveInteger(h, 5, 5, 50)", "init", - " if LoadInteger(h, 1, 1) == 0 and LoadInteger(h, 2, 2) == 0 and LoadInteger(h, 5, 5) == 50", + " SaveInteger(h, 6, 6, 60)", + " if LoadInteger(h, 1, 1) == 0 and LoadInteger(h, 2, 2) == 0 and LoadInteger(h, 5, 5) == 0", + " and LoadInteger(h, 6, 6) == 0 and not HaveSavedInteger(h, 6, 6)", " testSuccess()")); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java index 0a688063c..24a134900 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java @@ -49,9 +49,12 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException "package Test", "init", " let h = InitHashtable()", + " let t = CreateTimer()", " SaveInteger(h, 1, 2, 3)", " SaveTimerHandle(h, 1, 2, null)", + " SaveTimerHandle(h, 1, 3, t)", " if LoadInteger(h, 1, 2) == 3 and HaveSavedInteger(h, 1, 2) and LoadTimerHandle(h, 1, 2) == null" + + " and LoadTimerHandle(h, 1, 3) == t and LoadUnitHandle(h, 1, 3) == null" + " and \"x\" + LoadStr(h, 1, 2) == \"x\"", " RemoveSavedInteger(h, 1, 2)", " FlushChildHashtable(h, 1)", @@ -64,6 +67,12 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException compiled.contains("local __wurst_htEmpty = ({})\n")); assertTrue("the child constructor is a main-chunk local:\n" + compiled, compiled.contains("local function __wurst_htNewChild(t, p) \n")); + assertTrue("the table of a flushed hashtable keeps nothing written to it:\n" + compiled, + compiled.contains("local __wurst_htFlushed = setmetatable({}, { __newindex = function() end })\n")); + assertTrue("a timer load accepts a timer, and a handle saved as an agent:\n" + compiled, + compiled.contains("local __wurst_htAccepts_Timer = ({Timer=true, Agent=true, })\n")); + assertTrue("a unit load accepts a unit, and a handle saved as a widget or an agent:\n" + compiled, + compiled.contains("local __wurst_htAccepts_Unit = ({Unit=true, Widget=true, Agent=true, })\n")); String init = functionBody(compiled, "init_Test"); assertFalse("no hashtable helper is called:\n" + init, @@ -76,28 +85,32 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException } assertMatches("InitHashtable is the table constructor", "= \\(\\{__wurst_ht_int=\\(\\{\\}\\), __wurst_ht_bool=\\(\\{\\}\\), __wurst_ht_real=\\(\\{\\}\\), " - + "__wurst_ht_str=\\(\\{\\}\\), __wurst_ht_handle=\\(\\{\\}\\), \\}\\)", init); - assertMatches("a save creates the child only when there is none", - "\n\\s*;\\((\\w+)\\.__wurst_ht_int\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_int, 1\\)\\)\\[2\\] = 3\n", + + "__wurst_ht_str=\\(\\{\\}\\), __wurst_ht_handle=\\(\\{\\}\\), __wurst_ht_htype=\\(\\{\\}\\), \\}\\)", init); + assertMatches("a save of a hashtable creates the child only when there is none", + "if (\\w+) then\\s+;\\(\\1\\.__wurst_ht_int\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_int, 1\\)\\)\\[2\\] = 3\\s+end", init); - assertMatches("a handle save goes to the handle subtable", - ";\\((\\w+)\\.__wurst_ht_handle\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_handle, 1\\)\\)\\[2\\] = nil", + assertFalse("saving a null handle stores nothing:\n" + init, init.contains("__wurst_ht_handle, 1))[2] = nil")); + assertMatches("a handle save writes the handle and the type of its native", + "\\(\\w+\\.__wurst_ht_handle\\[1\\] or __wurst_htNewChild\\(\\w+\\.__wurst_ht_handle, 1\\)\\)\\[3\\] = \\w+\\s+" + + ";\\(\\w+\\.__wurst_ht_htype\\[1\\] or __wurst_htNewChild\\(\\w+\\.__wurst_ht_htype, 1\\)\\)\\[3\\] = \"Timer\"", init); - assertMatches("an int load reads through the empty child and answers 0", - "\\(\\(\\w+\\.__wurst_ht_int\\[1\\] or __wurst_htEmpty\\)\\[2\\] or 0\\)", init); + assertMatches("only for a handle which is not nil", "if \\(\\w+ and \\(\\w+ ~= nil\\)\\) then", init); + assertMatches("an int load reads through the empty child, also of a nil hashtable, and answers 0", + "\\(\\(\\(\\w+ and \\w+\\.__wurst_ht_int\\[1\\]\\) or __wurst_htEmpty\\)\\[2\\] or 0\\)", init); assertMatches("a test is the same read", - "\\(\\(\\w+\\.__wurst_ht_int\\[1\\] or __wurst_htEmpty\\)\\[2\\] ~= nil\\)", init); - assertMatches("a handle load answers nil when absent, as the helper did", - "\\(\\(\\w+\\.__wurst_ht_handle\\[1\\] or __wurst_htEmpty\\)\\[2\\] == nil\\)", init); - assertMatches("so does a string load, which the concatenation guards", - "\\(\\w+\\.__wurst_ht_str\\[1\\] or __wurst_htEmpty\\)\\[2\\] or \"\"\\)", init); + "\\(\\(\\(\\w+ and \\w+\\.__wurst_ht_int\\[1\\]\\) or __wurst_htEmpty\\)\\[2\\] ~= nil\\)", init); + assertMatches("a handle load answers the handle only for a tag it accepts", + "\\(\\(__wurst_htAccepts_Timer\\[\\(\\(\\w+ and \\w+\\.__wurst_ht_htype\\[1\\]\\) or __wurst_htEmpty\\)\\[3\\]\\] and " + + "\\(\\(\\w+ and \\w+\\.__wurst_ht_handle\\[1\\]\\) or __wurst_htEmpty\\)\\[3\\]\\) or nil\\)", init); + assertMatches("a string load is nil when absent, which the concatenation guards", + "\\(\\(\\(\\w+ and \\w+\\.__wurst_ht_str\\[1\\]\\) or __wurst_htEmpty\\)\\[2\\] or \"\"\\)", init); assertMatches("a remove never writes the shared empty child", - "if (\\w+)\\.__wurst_ht_int\\[1\\] then\\s+\\1\\.__wurst_ht_int\\[1\\]\\[2\\] = nil\\s+end", init); - for (String subtable : new String[]{"int", "bool", "real", "str", "handle"}) { + "if \\((\\w+) and \\1\\.__wurst_ht_int\\[1\\]\\) then\\s+\\1\\.__wurst_ht_int\\[1\\]\\[2\\] = nil\\s+end", init); + for (String subtable : new String[]{"int", "bool", "real", "str", "handle", "htype"}) { assertMatches("FlushChildHashtable drops the child of every kind", "\\w+\\.__wurst_ht_" + subtable + "\\[1\\] = nil", init); - assertMatches("FlushParentHashtable replaces every subtable", - "\\w+\\.__wurst_ht_" + subtable + " = \\(\\{\\}\\)", init); + assertMatches("FlushParentHashtable destroys the hashtable, as Jass does", + "\\w+\\.__wurst_ht_" + subtable + " = __wurst_htFlushed", init); } assertFalse("a flush tests no subtable, every one is always there:\n" + init, Pattern.compile("if \\w+\\.__wurst_ht_\\w+ then").matcher(init).find()); @@ -135,6 +148,14 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException " let t = CreateTimer()", " SaveTimerHandle(h, 1, 1, t)", " check(LoadTimerHandle(h, 1, 1) == t and HaveSavedHandle(h, 1, 1), \"handle\")", + // measured on the 3.0.0 client: a handle load is typed, and saving a null handle stores nothing + " check(LoadUnitHandle(h, 1, 1) == null and LoadGroupHandle(h, 1, 1) == null and LoadWidgetHandle(h, 1, 1) == null," + + " \"a timer loads as null through another type\")", + " timer noTimer = null", + " SaveTimerHandle(h, 1, 1, noTimer)", + " check(LoadTimerHandle(h, 1, 1) == t and HaveSavedHandle(h, 1, 1), \"a null handle replaces nothing\")", + " SaveTimerHandle(h, 3, 3, noTimer)", + " check(not HaveSavedHandle(h, 3, 3), \"a null handle is not saved\")", " RemoveSavedInteger(h, 1, 1)", " check(not HaveSavedInteger(h, 1, 1) and LoadInteger(h, 1, 1) == 0 and LoadReal(h, 1, 1) == 2.5," + " \"a remove takes only its kind\")", @@ -155,8 +176,24 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException " check(LoadInteger(h, 2, 1) == 22, \"a flushed child takes a save\")", " FlushParentHashtable(h)", " check(not HaveSavedBoolean(h, 1, 2) and not HaveSavedInteger(h, 2, 1), \"a flushed hashtable is empty\")", + // measured on the 3.0.0 client: FlushParentHashtable destroys the hashtable " SaveInteger(h, 1, 1, 31)", - " check(LoadInteger(h, 1, 1) == 31, \"a flushed hashtable takes a save\")", + " SaveTimerHandle(h, 1, 1, t)", + " check(LoadInteger(h, 1, 1) == 0 and not HaveSavedInteger(h, 1, 1) and LoadTimerHandle(h, 1, 1) == null," + + " \"a flushed hashtable keeps nothing\")", + " FlushChildHashtable(h, 1)", + " RemoveSavedInteger(h, 1, 1)", + " FlushParentHashtable(h)", + // measured on the 3.0.0 client: a null hashtable answers defaults and ignores writes + " hashtable none = null", + " SaveInteger(none, 1, 1, 5)", + " SaveTimerHandle(none, 1, 1, t)", + " RemoveSavedInteger(none, 1, 1)", + " FlushChildHashtable(none, 1)", + " FlushParentHashtable(none)", + " check(LoadInteger(none, 1, 1) == 0 and LoadReal(none, 1, 1) == 0. and not LoadBoolean(none, 1, 1)" + + " and \"x\" + LoadStr(none, 1, 1) == \"x\" and LoadTimerHandle(none, 1, 1) == null" + + " and not HaveSavedInteger(none, 1, 1), \"a null hashtable reads defaults\")", " check(LoadInteger(ct, 1, 2) == 12 and LoadStr(ct, 1, 3) == \"ct\" and LoadBoolean(ct, 4, 5)" + " and LoadReal(ct, 6, 7) == 1.5 and not HaveSavedInteger(ct, 1, 3), \"a compile-time hashtable\")", " testSuccess()" @@ -200,6 +237,17 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException { " FlushChildHashtable(h, key(\"f\", 1))", " if trace != \"pcvwPCHrsRSf\"", " testFail(\"operands evaluated as \" + trace)", + // the helpers answer as the operations printed in place + " let tm = CreateTimer()", + " timer noTimer = null", + " SaveTimerHandle(h, key(\"T\", 3), 1, tm)", + " SaveTimerHandle(h, key(\"N\", 3), 1, noTimer)", + " if LoadTimerHandle(h, key(\"L\", 3), 1) != tm or LoadUnitHandle(h, key(\"U\", 3), 1) != null", + " testFail(\"a typed load through the helper\")", + " hashtable none = null", + " SaveInteger(none, key(\"n\", 1), 1, 1)", + " if LoadInteger(none, key(\"l\", 1), 1) != 0 or HaveSavedInteger(none, key(\"h\", 1), 1)", + " testFail(\"a null hashtable through the helper\")", " if HaveSavedInteger(h, 1, 2) or HaveSavedInteger(h, 1, 4) or LoadInteger(h, 9, 2) != 0", " testFail(\"removed and flushed\")", " testSuccess()"); @@ -217,7 +265,7 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException { assertMatches("a save with plain operands is printed in place", ";\\((\\w+)\\.__wurst_ht_int\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_int, 1\\)\\)\\[6\\] = 7", init); assertMatches("a load with plain operands is printed in place", - "\\(\\w+\\.__wurst_ht_int\\[9\\] or __wurst_htEmpty\\)\\[2\\] or 0\\)", init); + "\\(\\(\\w+ and \\w+\\.__wurst_ht_int\\[9\\]\\) or __wurst_htEmpty\\)\\[2\\] or 0\\)", init); assertFalse("a helper nothing calls is not defined:\n" + compiled, compiled.contains("function __wurst_LoadReal(") || compiled.contains("function __wurst_InitHashtable(")); assertFalse("the helper no longer tests for a missing subtable:\n" + compiled, @@ -249,7 +297,7 @@ public void aTableOperationIsPrintedInPlace() throws IOException { ";\\(\\w+\\.__wurst_ht_int\\[[^\\]]+\\] or __wurst_htNewChild\\(\\w+\\.__wurst_ht_int, [^\\n]+\\)\\)\\[5\\] = 7", init); assertMatches("the load is printed in place under the instance id", - "\\(Table_ht\\.__wurst_ht_int\\[\\(\\w+ or 0\\)\\] or __wurst_htEmpty\\)\\[5\\] or 0\\)", init); + "\\(\\(Table_ht and Table_ht\\.__wurst_ht_int\\[\\(\\w+ or 0\\)\\]\\) or __wurst_htEmpty\\)\\[5\\] or 0\\)", init); } /** @@ -293,8 +341,8 @@ public void aStoreStartingWithAParenthesisIsSeparatedFromTheStatementBefore() th String init = functionBody(compiled("aStoreStartingWithAParenthesisIsSeparatedFromTheStatementBefore"), "init_Test"); - assertMatches("the save follows a statement ending in a parenthesis, after a ';'", - "= \\(\\w+ \\+ 1\\)\n\\s*;\\(", init); + assertMatches("the save, under the test of the hashtable, starts with a ';'", + "= \\(\\w+ \\+ 1\\)\n\\s*if \\w+ then\n\\s*;\\(", init); } @Test diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java index 3e99750cc..236f0839e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java @@ -96,7 +96,9 @@ public void hashtableLoadsUseTypeSpecificBuckets() { @Test public void hashtableFlushesTestNoSubtable() { String flushChild = renderNative("__wurst_FlushChildHashtable"); - assertTrue(flushChild, flushChild.contains("h.__wurst_ht_handle[p] = nil")); - assertFalse(flushChild, flushChild.contains("if ")); + assertTrue(flushChild, flushChild.contains("h.__wurst_ht_handle[p] = nil") + && flushChild.contains("h.__wurst_ht_htype[p] = nil")); + assertTrue("a nil hashtable is ignored, as Jass does: " + flushChild, flushChild.contains("if h == nil then return end")); + assertFalse(flushChild, flushChild.contains("if h.")); } } 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 f53a8641c..ae60b5805 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 @@ -500,7 +500,7 @@ public void testStdLib() throws IOException { assertTrue(compiled.contains("MagicFunctions_compiletime")); // The library's hashtables are created and filled where it uses them (LuaHashtableTranslation). assertTrue(compiled.contains(" = ({__wurst_ht_int=({}), __wurst_ht_bool=({}), __wurst_ht_real=({}), " - + "__wurst_ht_str=({}), __wurst_ht_handle=({}), })")); + + "__wurst_ht_str=({}), __wurst_ht_handle=({}), __wurst_ht_htype=({}), })")); assertTrue(compiled.contains("local function __wurst_htNewChild(")); assertFalse(compiled.contains("function __wurst_InitHashtable(")); LuaAssertions.assertNoLeakedHashtableNativeCalls(compiled); @@ -3661,7 +3661,7 @@ public void hashtableHandleExtensionsUseWurstLuaHelpers() throws IOException { String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleExtensionsUseWurstLuaHelpers.lua"), Charsets.UTF_8); assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\("); // Printed as the read it stands for, so no helper is called. - assertContainsRegex(compiled, "\\(\\w+\\.__wurst_ht_handle\\[\\w+\\] or __wurst_htEmpty\\)\\[\\w+\\] ~= nil\\)"); + assertContainsRegex(compiled, "\\(\\(\\w+ and \\w+\\.__wurst_ht_handle\\[\\w+\\]\\) or __wurst_htEmpty\\)\\[\\w+\\] ~= nil\\)"); assertDoesNotContainRegex(compiled, "\\b__wurst_HaveSavedHandle\\("); } @@ -3702,7 +3702,7 @@ public void hashtableHandleLoadSaveUseWurstLuaHelpers() throws IOException { assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\("); // Printed in place on the handle subtable, so no helper is called. assertContainsRegex(compiled, "__wurst_htNewChild\\(\\w+\\.__wurst_ht_handle, \\w+\\)"); - assertContainsRegex(compiled, "\\(\\w+\\.__wurst_ht_handle\\[\\w+\\] or __wurst_htEmpty\\)\\[\\w+\\]"); + assertContainsRegex(compiled, "\\(\\(\\w+ and \\w+\\.__wurst_ht_handle\\[\\w+\\]\\) or __wurst_htEmpty\\)\\[\\w+\\]"); assertDoesNotContainRegex(compiled, "\\b__wurst_(Save|Load)AbilityHandle\\("); } diff --git a/docs/WC3_RUNTIME.md b/docs/WC3_RUNTIME.md index 2f52569ab..2a2409c73 100644 --- a/docs/WC3_RUNTIME.md +++ b/docs/WC3_RUNTIME.md @@ -24,6 +24,8 @@ A test that passes in the first two columns says nothing about the third where t * **code** Both targets reuse the most recently freed id first (a stack, despite the name `RecycleCodeGeneratorQueue`), so `destroy` followed by `new` from the same allocator returns the same id and a stale reference then reads the new object. On Jass each group of classes joined by inheritance (a class with its `superClasses`, `ImTranslator.buildClassPartitions`) has one allocator: a subclass, its siblings and the base class share ids and the cap, so a stale id may come back as an object of a related class. The group runs out at `JASS_MAX_ARRAY_SIZE` live instances (default `Constants.MAX_ARRAY_SIZE`, 32768) with `Out of memory: Could not create X.`. On Lua there is one id space shared by every class and no cap, so a stale id may come back as an object of any class. * **code** Lua keeps one static table per field, indexed by id, and a class descriptor per live id. `destroy` removes the descriptor and recycles the id; field storage is not cleared, as Jass arrays are not. Until the id is reused a stale reference fails virtual dispatch and `instanceof` is false. Lua's collector therefore frees no Wurst object, and a table stored in a field stays reachable after `destroy`. +* **measured, 3.0.0** Hashtables: a handle load is typed (a saved unit loads as itself through `LoadUnitHandle` and `LoadWidgetHandle`, as null through `LoadItemHandle` or `LoadTimerHandle`); saving a null handle stores nothing and keeps what was there; `FlushParentHashtable` destroys the hashtable, so nothing saved to it afterwards stays and every load answers its default; a null hashtable answers every load with its default and ignores every write. The Lua emulation (`LuaHashtable`) and the interpreter (`HashtableProvider`) answer the same. A handle saved through a supertype (`SaveAgentHandle`, `SaveWidgetHandle`) and loaded as a subtype is answered on Lua, which cannot tell its type. + ## The game's Lua * **reported** It is Lua 5.3, with `debug` disabled ([Hive guide](https://www.hiveworkshop.com/threads/a-comprehensive-guide-to-mapping-in-lua.341880/)) and `collectgarbage` inaccessible since a patch, "it could cause desync" ([forum post](https://www.hiveworkshop.com/threads/wc3-reforged-lua.326354/)). The repository already treats `debug.traceback` as unavailable (`LuaTranslationTests.luaErrorWrappersAvoidUnavailableNativeTraceback`). Nothing in emitted code needs `io`, `os`, `package` or `require`; they are not known to exist there. `LuaTranslationTests.emittedLuaUsesOnlyLibrariesTheGameProvides` fails if a program's emitted helpers reach for any of these. `noLuaBackendLiteralNamesALibraryTheGameWithholds` scans the string literals under `translation/` (bar two files that only list names), because natives and fallback helpers are emitted only on demand. A name counts anywhere in the text, inside a Lua string or comment too, since a string can be run with `load`. Only the test-only `testSuccess` is exempt: it ends a test run with `os.exit`.