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 b60747f0e..f53945c81 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
@@ -401,7 +401,10 @@ 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()))
+ // a string load of the hashtable emulation answers "" when nothing is stored
+ || (LuaHashtable.op(translator, f) instanceof LuaHashtable.Op op
+ && op.kind() == LuaHashtable.Kind.LOAD && op.values() == LuaHashtable.Values.STR);
}
return false;
}
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..c2c2db304 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
@@ -18,7 +18,8 @@
*
* - 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;
+ * {@code or 0.0}, {@code == true}, {@code or ""} (a null string is {@code ""} on Lua), and nothing for a
+ * handle, which is nil when absent;
* - 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
@@ -64,7 +65,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, HANDLE -> value;
+ case STR -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpOr(), LuaAst.LuaExprStringVal(""));
+ case HANDLE -> value;
};
}
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..e4b2ea511 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
@@ -379,8 +379,9 @@ 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 and handle loads have always answered
- addHashtableLoad(withWurstPrefix("LoadStr"), "__wurst_ht_str", "nil", "x[c]");
+ // "" when absent: a null string is "" on Lua (EliminateLocalTypes), so a missing string compares equal to null
+ addHashtableLoad(withWurstPrefix("LoadStr"), "__wurst_ht_str", "\"\"", "x[c] or \"\"");
+ // nil when absent, which is the null handle
addHashtableLoad(withWurstPrefix(HASHTABLE_HANDLE_LOAD_NAMES), "__wurst_ht_handle", "nil", "x[c]");
addHashtableEntryOperation(withWurstPrefix("HaveSavedInteger"), "__wurst_ht_int", "return x ~= nil and x[c] ~= nil");
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..b66d8a3cd 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
@@ -89,8 +89,9 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
"\\(\\(\\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",
+ assertMatches("a string load answers \"\", the null string on Lua, when absent",
"\\(\\w+\\.__wurst_ht_str\\[1\\] or __wurst_htEmpty\\)\\[2\\] or \"\"\\)", init);
+ assertFalse("so the concatenation adds no guard of its own:\n" + init, init.contains("or \"\") or \"\")"));
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"}) {
@@ -117,7 +118,8 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
"init",
" let h = InitHashtable()",
" check(LoadInteger(h, 1, 1) == 0 and LoadReal(h, 1, 1) == 0. and not LoadBoolean(h, 1, 1)"
- + " and \"x\" + LoadStr(h, 1, 1) == \"x\" and LoadTimerHandle(h, 1, 1) == null, \"an absent parent key reads defaults\")",
+ + " and \"x\" + LoadStr(h, 1, 1) == \"x\" and LoadStr(h, 1, 1) == null and LoadTimerHandle(h, 1, 1) == null,"
+ + " \"an absent parent key reads defaults\")",
" check(not HaveSavedInteger(h, 1, 1) and not HaveSavedReal(h, 1, 1) and not HaveSavedBoolean(h, 1, 1)"
+ " and not HaveSavedString(h, 1, 1) and not HaveSavedHandle(h, 1, 1), \"an absent parent key holds nothing\")",
" SaveInteger(h, 1, 1, 11)",
@@ -132,6 +134,7 @@ public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException
" check(LoadReal(h, 1, 1) == 2.5 and HaveSavedReal(h, 1, 1), \"real\")",
" SaveStr(h, 1, 1, \"s\")",
" check(LoadStr(h, 1, 1) == \"s\" and HaveSavedString(h, 1, 1), \"string\")",
+ " check(LoadStr(h, 1, 2) == null and not HaveSavedString(h, 1, 2), \"an absent child key reads the null string\")",
" let t = CreateTimer()",
" SaveTimerHandle(h, 1, 1, t)",
" check(LoadTimerHandle(h, 1, 1) == t and HaveSavedHandle(h, 1, 1), \"handle\")",
@@ -195,10 +198,12 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException {
" SaveInteger(h, 1, 6, 7)",
" if LoadInteger(h, key(\"P\", 1), key(\"C\", 2)) != 3 or not HaveSavedInteger(h, 1, key(\"H\", 4))",
" testFail(\"stored through the helper\")",
+ " if LoadStr(h, key(\"L\", 1), 9) != null",
+ " testFail(\"an absent string through the helper is the null string\")",
" RemoveSavedInteger(h, key(\"r\", 9), key(\"s\", 2))",
" RemoveSavedInteger(h, key(\"R\", 1), key(\"S\", 2))",
" FlushChildHashtable(h, key(\"f\", 1))",
- " if trace != \"pcvwPCHrsRSf\"",
+ " if trace != \"pcvwPCHLrsRSf\"",
" testFail(\"operands evaluated as \" + trace)",
" if HaveSavedInteger(h, 1, 2) or HaveSavedInteger(h, 1, 4) or LoadInteger(h, 9, 2) != 0",
" testFail(\"removed and flushed\")",
@@ -207,8 +212,8 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException {
String compiled = compiled("anOperandWithAnEffectKeepsTheCallOfTheHelper");
LuaTranslator.assertNoLeakedHashtableNativeCalls(compiled);
String init = functionBody(compiled, "init_Test");
- for (String helper : new String[]{"SaveInteger", "LoadInteger", "HaveSavedInteger", "RemoveSavedInteger",
- "FlushChildHashtable"}) {
+ for (String helper : new String[]{"SaveInteger", "LoadInteger", "HaveSavedInteger", "LoadStr",
+ "RemoveSavedInteger", "FlushChildHashtable"}) {
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,
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..d9722695e 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
@@ -88,7 +88,10 @@ 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 on Lua: " + loadStr,
+ loadStr.contains("return \"\"") && loadStr.contains("return x[c] or \"\"\n"));
+ assertTrue("a missing handle is nil: " + loadHandle,
+ loadHandle.contains("return nil") && loadHandle.contains("return x[c]\n"));
assertTrue(loadBool, loadBool.contains("return x[c] == true"));
assertFalse("no subtable is ever missing: " + loadInt, loadInt.contains("t == nil"));
}