Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ public static String stubName(String nativeName) {
return OPS.get(f.getName());
}

/**
* Whether {@code f} is the string load: the common.j native before {@link LuaNativeLowering#transform} replaces
* its calls, or the stub after. It answers the null string, "", when nothing is stored.
*/
public static boolean isStringLoad(ImTranslator translator, ImFunction f) {
Op op = f.isBj() ? (isNative(f.getName()) ? opOfName(f.getName()) : null) : op(translator, f);
return op != null && op.kind() == Kind.LOAD && op.values() == Values.STR;
}

/** Whether {@code f} is the stub of a load or a HaveSaved test, which only read the hashtable. */
public static boolean isRead(ImTranslator translator, ImFunction f) {
Op op = op(translator, f);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ private static ImExpr concatenation(ImOperatorCall call, ImTranslator translator
* Natives whose result is a string whatever the runtime does. Only {@code I2S}: the backend prints
* it as {@code tostring}, which cannot answer nil. A native which merely returns a string in the
* game, such as {@code R2S}, is not trusted to: a runtime of one's own may declare it to answer nil.
* The emulated string load is matched by identity ({@link LuaHashtable#isStringLoad}).
*/
private static final Set<String> STRING_NATIVES_NEVER_NIL = Set.of("I2S");

Expand All @@ -401,7 +402,8 @@ public static boolean neverNil(ImExpr e, ImTranslator translator) {
return f == translator.luaRawConcatFunc
|| f == translator.luaRawOrEmptyFunc
|| f == translator.luaRawToStringFunc
|| (f.isNative() && STRING_NATIVES_NEVER_NIL.contains(f.getName()));
|| (f.isNative() && STRING_NATIVES_NEVER_NIL.contains(f.getName()))
|| LuaHashtable.isStringLoad(translator, f);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ 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 -> value;
// Lua's null string is "", which is what the game's Lua natives answer where Jass answers null
case STR -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpOr(), LuaAst.LuaExprStringVal(""));
case HANDLE -> {
LuaExpr accepted = LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(tr.hashtableAccepts(op.handleType())),
LuaAst.LuaExprlist(read(copies(operands), LuaHashtable.Values.HANDLE_TYPE, tr)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,8 @@ public class LuaNatives {
// 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 loads have always answered
addHashtableLoad(withWurstPrefix("LoadStr"), "__wurst_ht_str", "nil", "x[c]");
// the null string when absent, which on Lua is "": the game's natives raise on nil
addHashtableLoad(withWurstPrefix("LoadStr"), "__wurst_ht_str", "\"\"", "x[c] or \"\"");
for (String name : HASHTABLE_HANDLE_LOAD_NAMES) {
String type = handleTypeOf(name);
StringBuilder accepted = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
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 string load is the null string, \"\", when absent, which the concatenation need not guard",
"\"x\" \\.\\. \\(\\(\\(\\w+ and \\w+\\.__wurst_ht_str\\[1\\]\\) or __wurst_htEmpty\\)\\[2\\] or \"\"\\)\\)", init);
assertMatches("a remove never writes the shared empty child",
"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"}) {
Expand Down Expand Up @@ -136,6 +136,9 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
" SaveInteger(h, 1, 1, 11)",
" check(LoadInteger(h, 1, 1) == 11 and HaveSavedInteger(h, 1, 1), \"int\")",
" check(LoadInteger(h, 1, 2) == 0 and not HaveSavedInteger(h, 1, 2), \"an absent child key reads 0\")",
// Lua's null string is "", which is what the game's Lua natives answer where Jass answers null; they raise on nil
" check(StringLength(LoadStr(h, 1, 1)) == 0 and LoadStr(h, 1, 1) == null and StringLength(LoadStr(h, 1, 2)) == 0,"
+ " \"an absent string is the null string\")",
" check(LoadReal(h, 1, 1) == 0. and not HaveSavedReal(h, 1, 1), \"each kind has its own entries\")",
" SaveBoolean(h, 1, 1, false)",
" check(HaveSavedBoolean(h, 1, 1) and not LoadBoolean(h, 1, 1), \"a stored false\")",
Expand Down Expand Up @@ -181,6 +184,7 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
" 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\")",
" check(StringLength(LoadStr(h, 1, 1)) == 0 and LoadStr(h, 1, 1) == null, \"a flushed hashtable reads the null string\")",
" FlushChildHashtable(h, 1)",
" RemoveSavedInteger(h, 1, 1)",
" FlushParentHashtable(h)",
Expand All @@ -194,6 +198,7 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
" 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(StringLength(LoadStr(none, 1, 1)) == 0 and LoadStr(none, 1, 1) == null, \"a null hashtable reads the null string\")",
" 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()"
Expand Down Expand Up @@ -225,6 +230,8 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException {
"@noinline function key(string tag, int k) returns int",
" trace += tag",
" return k",
"function loadZ(hashtable h) returns string",
" return LoadStr(h, key(\"z\", 7), 1)",
"init",
" let h = InitHashtable()",
" SaveInteger(h, key(\"p\", 1), key(\"c\", 2), key(\"v\", 3))",
Expand All @@ -248,6 +255,11 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException {
" 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 StringLength(LoadStr(h, key(\"s\", 7), 1)) != 0 or StringLength(LoadStr(none, key(\"t\", 1), 1)) != 0",
" testFail(\"an absent string through the helper\")",
// guarded before inlining, as the call of loadZ; inlined, the guard holds the stub, which answers ""
" if \"x\" + loadZ(h) != \"x\"",
" testFail(\"an absent string joined through the helper\")",
" if HaveSavedInteger(h, 1, 2) or HaveSavedInteger(h, 1, 4) or LoadInteger(h, 9, 2) != 0",
" testFail(\"removed and flushed\")",
" testSuccess()");
Expand All @@ -256,12 +268,16 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException {
LuaTranslator.assertNoLeakedHashtableNativeCalls(compiled);
String init = functionBody(compiled, "init_Test");
for (String helper : new String[]{"SaveInteger", "LoadInteger", "HaveSavedInteger", "RemoveSavedInteger",
"FlushChildHashtable"}) {
"FlushChildHashtable", "LoadStr"}) {
assertTrue("an operation with an effect in an operand calls its helper: " + helper + "\n" + init,
init.contains("__wurst_" + helper + "("));
assertTrue("a helper which is called is defined: " + helper + "\n" + compiled,
compiled.contains("function __wurst_" + helper + "("));
}
int joined = init.indexOf("\"x\" .. __wurst_LoadStr(");
assertTrue("the string load joined through the helper:\n" + init, joined >= 0);
String join = init.substring(joined, init.indexOf('\n', joined));
assertFalse("the helper answers the null string, so the join adds no guard: " + join, join.contains("or \"\""));
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ public void hashtableLoadsUseTypeSpecificBuckets() {
assertTrue(loadStr.contains("h.__wurst_ht_str"));
assertTrue(loadHandle.contains("h.__wurst_ht_handle"));
assertTrue(loadInt, loadInt.contains("return x[c] or 0"));
assertTrue("a missing string is nil: " + loadStr, loadStr.contains("return nil") && loadStr.contains("return x[c]\n"));
assertTrue("a missing string is the null string, \"\": " + loadStr,
loadStr.contains("if h == nil then return \"\" end") && loadStr.contains("return x[c] or \"\"")
&& !loadStr.contains("return nil"));
assertTrue(loadBool, loadBool.contains("return x[c] == true"));
assertFalse("no subtable is ever missing: " + loadInt, loadInt.contains("t == nil"));
}
Expand Down
1 change: 1 addition & 0 deletions docs/WC3_RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A test that passes in the first two columns says nothing about the third where t
* **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.
* **measured, 3.0.0** Null strings: Jass keeps null and `""` apart (`null == ""` is false; an unset string array element, field or global is null; `SubString` past the end, any `SubString` of `""` and `StringCase("")` answer null; `StringLength`, `S2I`, `S2R` and `StringHash` of null answer 0, as of `""`). The game's Lua natives answer `""` in each of those places, and every string native probed raises on a nil argument ("string expected, got nil": `StringLength`, `SubString`, `StringCase`, `S2I`, `S2R`, `StringHash`, `GetLocalizedString`, `BlzSetUnitName`, `SetTextTagText`, `BlzFrameSetText`, `DisplayTextToPlayer`). So on Lua the null string is `""`, as the game's natives treat it (`EliminateLocalTypes`), and nothing the compiler emits answers nil for a string: the emulated `LoadStr` answers `""` when nothing is stored. `null == ""` is therefore true on Lua and false on Jass; matching Jass would mean wrapping every native which returns a string. In Jass, `BlzSetUnitName(u, null)` crashes the client.

## The game's Lua

Expand Down
Loading