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 e2b7e9243..6ef67034b 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 @@ -421,6 +421,25 @@ private ImExpr constantToExpr(Element trace, ILconst value) { return constantToExpr(trace, value, null); } + /** + * The text for a string a compiletime expression produced, which becomes a literal in the + * generated script. + *
+ * A string held by the interpreter is a sequence of bytes and may hold half of a character -
+ * slicing one in half is a thing the standard library does deliberately. A literal cannot: the
+ * script is written as UTF-8 and neither Jass nor the escaping here can write a byte down
+ * numerically, so half a character would go in as the replacement character and come back out
+ * three bytes long. Refused rather than carried across at a different length.
+ */
+ private String literalText(ILconstString value, Element trace) {
+ if (!value.isText()) {
+ throw new CompileError(trace, "A compiletime expression returned a string holding part of a"
+ + " multibyte character, which cannot be written into the generated script. Slice it"
+ + " where the program runs rather than at compiletime, or keep whole characters.");
+ }
+ return value.text();
+ }
+
private ImExpr constantToExpr(Element trace, ILconst value, @Nullable ImType expectedType) {
if (value instanceof ILconstBool) {
return JassIm.ImBoolVal(((ILconstBool) value).getVal());
@@ -429,7 +448,7 @@ private ImExpr constantToExpr(Element trace, ILconst value, @Nullable ImType exp
} else if (value instanceof ILconstReal) {
return JassIm.ImRealVal("" + ((ILconstReal) value).getVal());
} else if (value instanceof ILconstString) {
- return JassIm.ImStringVal(((ILconstString) value).getVal());
+ return JassIm.ImStringVal(literalText((ILconstString) value, trace));
} else if (value instanceof ILconstNull) {
return expectedType == null ? ImHelper.nullExpr() : JassIm.ImNull(expectedType.copy());
} else if (value instanceof ILconstTuple) {
@@ -1043,7 +1062,7 @@ private ImExpr constantToExprHashtable(Element trace, ImVar htVar, IlConstHandle
JassIm.ImVarAccess(htVar),
JassIm.ImIntVal(key.getParentkey()),
JassIm.ImIntVal(key.getChildkey()),
- JassIm.ImStringVal(iv.getVal())
+ JassIm.ImStringVal(literalText(iv, trace))
), false, CallType.NORMAL));
} else if (v instanceof ILconstBool) {
ILconstBool iv = (ILconstBool) v;
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java
index 479a8228e..bae31c95c 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java
@@ -47,7 +47,7 @@ public CompiletimeNatives(ProgramStateIO globalState, WurstProjectConfigData pro
private ILconstTuple makeKey(String key) {
- return new ILconstTuple(new ILconstString(key));
+ return new ILconstTuple(ILconstString.fromText(key));
}
public ILconstTuple createObjectDefinition(ILconstString fileType, ILconstInt newUnitId, ILconstInt deriveFrom) {
@@ -93,7 +93,7 @@ public void ObjectDefinition_setInt(ILconstTuple unitType, ILconstString modific
public void ObjectDefinition_setString(ILconstTuple unitType, ILconstString modification, ILconstString value) {
ObjMod.Obj od = globalState.getObjectDefinition(getKey(unitType));
- modifyObject(od, modification, ObjMod.ValType.STRING, War3String.valueOf(value.getVal()));
+ modifyObject(od, modification, ObjMod.ValType.STRING, War3String.valueOf(value.text()));
}
public void ObjectDefinition_setReal(ILconstTuple unitType, ILconstString modification, ILconstReal value) {
@@ -114,7 +114,7 @@ public void ObjectDefinition_setLvlInt(ILconstTuple unitType, ILconstString modi
public void ObjectDefinition_setLvlString(ILconstTuple unitType, ILconstString modification, ILconstInt level, ILconstString value) {
ObjMod.Obj od = globalState.getObjectDefinition(getKey(unitType));
- modifyObject(od, modification, ObjMod.ValType.STRING, level.getVal(), War3String.valueOf(value.getVal()));
+ modifyObject(od, modification, ObjMod.ValType.STRING, level.getVal(), War3String.valueOf(value.text()));
}
public void ObjectDefinition_setLvlReal(ILconstTuple unitType, ILconstString modification, ILconstInt level, ILconstReal value) {
@@ -135,7 +135,7 @@ public void ObjectDefinition_setLvlDataInt(ILconstTuple unitType, ILconstString
public void ObjectDefinition_setLvlDataString(ILconstTuple unitType, ILconstString modification, ILconstInt level, ILconstInt dataPointer, ILconstString value) {
ObjMod.Obj od = globalState.getObjectDefinition(getKey(unitType));
- modifyObject(od, modification, ObjMod.ValType.STRING, level.getVal(), dataPointer.getVal(), War3String.valueOf(value.getVal()));
+ modifyObject(od, modification, ObjMod.ValType.STRING, level.getVal(), dataPointer.getVal(), War3String.valueOf(value.text()));
}
public void ObjectDefinition_setLvlDataReal(ILconstTuple unitType, ILconstString modification, ILconstInt level, ILconstInt dataPointer, ILconstReal value) {
@@ -185,15 +185,15 @@ private String getKey(ILconstTuple unitType) {
}
public void compileError(ILconstString msg) {
- throw new InterpreterException(msg.getVal());
+ throw new InterpreterException(msg.text());
}
public ILconstString getMapName() {
- return new ILconstString(projectConfigData.buildMapData().name());
+ return ILconstString.fromText(projectConfigData.buildMapData().name());
}
public ILconstString getBuildDate() {
- return new ILconstString(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toString());
+ return ILconstString.fromText(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toString());
}
public ILconstBool isProductionBuild() {
@@ -224,7 +224,7 @@ private PreparedStatement sqliteStatement(int handle) {
}
public ILconstInt sqlite_open(ILconstString path) {
- String dbPath = path.getVal();
+ String dbPath = path.text();
// SQLite "file:" URI paths can carry query parameters such as
// "?enable_load_extension=true" that would turn on extension loading and let
// load_extension() dlopen arbitrary native code on the build machine at compiletime.
@@ -250,7 +250,7 @@ public ILconstInt sqlite_open(ILconstString path) {
public ILconstInt sqlite_prepare(ILconstInt connection, ILconstString query) {
Connection conn = sqliteConnection(connection.getVal());
try {
- PreparedStatement stmt = conn.prepareStatement(query.getVal());
+ PreparedStatement stmt = conn.prepareStatement(query.text());
int handle = ++sqliteHandleCounter;
sqliteStatements.put(handle, stmt);
sqliteStatementConnections.put(handle, connection.getVal());
@@ -300,7 +300,7 @@ public void sqlite_bind_real(ILconstInt statement, ILconstInt index, ILconstReal
public void sqlite_bind_string(ILconstInt statement, ILconstInt index, ILconstString value) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
- stmt.setString(index.getVal(), value.getVal());
+ stmt.setString(index.getVal(), value.text());
markStatementForReexecution(statement.getVal());
} catch (SQLException e) {
throw new InterpreterException("Failed to bind string: " + e.getMessage());
@@ -379,7 +379,7 @@ public ILconstString sqlite_column_string(ILconstInt statement, ILconstInt index
// A SQL NULL maps to "" here; use sqlite_column_is_null to distinguish NULL
// from an empty string / zero value.
String val = rs.getString(index.getVal() + 1);
- return new ILconstString(val == null ? "" : val);
+ return ILconstString.fromText(val == null ? "" : val);
} catch (SQLException e) {
throw new InterpreterException("Failed to get column string: " + e.getMessage());
}
@@ -476,7 +476,7 @@ public void sqlite_exec(ILconstInt connection, ILconstString query) {
// and a hand-rolled splitter cannot correctly handle trigger BEGIN...END
// bodies, CASE...END, or every identifier-quoting form ([id], `id`, "id").
SQLiteConnection sqliteConn = conn.unwrap(SQLiteConnection.class);
- sqliteConn.getDatabase()._exec(query.getVal());
+ sqliteConn.getDatabase()._exec(query.text());
} catch (SQLException e) {
throw new InterpreterException("Failed to exec SQLite query: " + e.getMessage());
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java
index 6e3c574a0..f61a000f4 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java
@@ -386,7 +386,7 @@ public ILconst case_JassOpEquals(JassOpEquals jassOpEquals) {
@Override
public ILconst case_JassExprStringVal(JassExprStringVal e) {
- return new ILconstString(e.getValS());
+ return ILconstString.fromText(e.getValS());
}
@Override
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java
index d74c00313..9d82492c2 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java
@@ -10,10 +10,10 @@ public AbilityProvider(AbstractInterpreter interpreter) {
}
public ILconstString BlzGetAbilityIcon(ILconstInt abilCode) {
- return new ILconstString("");
+ return ILconstString.fromText("");
}
public ILconstString BlzGetAbilityExtendedTooltip(ILconstInt abilCode, ILconstInt level) {
- return new ILconstString("");
+ return ILconstString.fromText("");
}
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GamecacheProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GamecacheProvider.java
index d7bd46ce7..6801dd33d 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GamecacheProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GamecacheProvider.java
@@ -63,7 +63,7 @@ public ILconstReal GetStoredReal(IlConstHandle ht, ILconstString key1, ILconstSt
}
public ILconstString GetStoredString(IlConstHandle ht, ILconstString key1, ILconstString key2) {
- return haveSaved(ht, key1, key2, ILconstString.class) ? load(ht, key1, key2, ILconstString.class) : new ILconstString("");
+ return haveSaved(ht, key1, key2, ILconstString.class) ? load(ht, key1, key2, ILconstString.class) : ILconstString.fromText("");
}
public ILconstBool GetStoredBoolean(IlConstHandle ht, ILconstString key1, ILconstString key2) {
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 1a901e1a8..e4ef842d9 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
@@ -74,7 +74,7 @@ public ILconstReal LoadReal(IlConstHandle ht, ILconstInt key1, ILconstInt key2)
}
public ILconstString LoadStr(IlConstHandle ht, ILconstInt key1, ILconstInt key2) {
- return haveSaved(ht, key1, key2, ILconstString.class) ? load(ht, key1, key2, ILconstString.class) : new ILconstString("");
+ return haveSaved(ht, key1, key2, ILconstString.class) ? load(ht, key1, key2, ILconstString.class) : ILconstString.fromText("");
}
public ILconstBool LoadBoolean(IlConstHandle ht, ILconstInt key1, ILconstInt key2) {
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java
index c4bab136b..c42e27f37 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java
@@ -43,7 +43,7 @@ public ILconstString __wurst_rawToString(ILconstString x) {
}
public ILconstString __wurst_rawConcat(ILconstString x, ILconstString y) {
- return new ILconstString(x.getVal() + y.getVal());
+ return ILconstString.ofBytes(x.getVal() + y.getVal());
}
public ILconstInt __wurst_rawFloorDivInt(ILconstInt a, ILconstInt b) {
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java
index 083953019..c9cae968c 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java
@@ -23,29 +23,29 @@ public void setOutStream(PrintStream outStream) {
}
public void DisplayTextToForce(IlConstHandle force, ILconstString msg) {
- outStream.println(msg.getVal());
+ outStream.println(msg.text());
}
public void DisplayTimedTextToForce(IlConstHandle force, ILconstReal duration, ILconstString msg) {
- outStream.println(msg.getVal());
+ outStream.println(msg.text());
}
public void DisplayTextToPlayer(IlConstHandle player, ILconstReal x, ILconstReal y, ILconstString msg) {
- outStream.println(msg.getVal());
+ outStream.println(msg.text());
}
public void DisplayTimedTextToPlayer(IlConstHandle player, ILconstReal x, ILconstReal y, ILconstReal duration, ILconstString msg) {
- outStream.println(msg.getVal());
+ outStream.println(msg.text());
}
@Implements(funcNames = {"BJDebugMsg", "println"})
public void println(ILconstString msg) {
- outStream.println(msg.getVal());
+ outStream.println(msg.text());
}
public void $debugPrint(ILconstString msg) {
- outStream.println(msg.getVal());
- throw new DebugPrintError(msg.getVal());
+ outStream.println(msg.text());
+ throw new DebugPrintError(msg.text());
}
public void testSuccess() {
@@ -53,6 +53,6 @@ public void testSuccess() {
}
public void testFail(ILconstString msg) {
- throw new TestFailException(msg.getVal());
+ throw new TestFailException(msg.text());
}
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java
index 73e10def9..a8cea2ce7 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/StringProvider.java
@@ -1,16 +1,14 @@
package de.peeeq.wurstio.jassinterpreter.providers;
import de.peeeq.wurstio.jassinterpreter.InterpreterException;
-import de.peeeq.wurstscript.WLogger;
import de.peeeq.wurstscript.intermediatelang.ILconstBool;
import de.peeeq.wurstscript.intermediatelang.ILconstInt;
import de.peeeq.wurstscript.intermediatelang.ILconstReal;
import de.peeeq.wurstscript.intermediatelang.ILconstString;
+import de.peeeq.wurstscript.intermediatelang.Wc3StringHash;
import de.peeeq.wurstscript.intermediatelang.interpreter.AbstractInterpreter;
-import net.moonlightflower.wc3libs.misc.StringHash;
import org.apache.commons.lang.StringUtils;
-import java.io.UnsupportedEncodingException;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;
@@ -24,7 +22,7 @@ public StringProvider(AbstractInterpreter interpreter) {
}
public ILconstString I2S(ILconstInt i) {
- return new ILconstString("" + i.getVal());
+ return ILconstString.fromText("" + i.getVal());
}
private static final Pattern s2ipattern = Pattern.compile("([+\\-]?[0-9]+).*");
@@ -53,7 +51,7 @@ public ILconstReal S2R(ILconstString s) {
}
public ILconstString R2S(ILconstReal r) {
- return new ILconstString("" + r.getVal());
+ return ILconstString.fromText("" + r.getVal());
}
public ILconstString R2SW(ILconstReal r, ILconstInt width, ILconstInt precision) {
@@ -65,7 +63,7 @@ public ILconstString R2SW(ILconstReal r, ILconstInt width, ILconstInt precision)
String s = formatter.format(r.getVal());
// pad to desired width
s = StringUtils.rightPad(s, width.getVal());
- return new ILconstString(s);
+ return ILconstString.fromText(s);
}
public ILconstInt R2I(ILconstReal i) {
@@ -81,12 +79,7 @@ public ILconstInt StringHash(ILconstString s) {
if (s == null) {
return new ILconstInt(0);
}
- try {
- return new ILconstInt(StringHash.hash(s.getVal()));
- } catch (UnsupportedEncodingException e) {
- WLogger.severe(e);
- }
- return new ILconstInt(0);
+ return new ILconstInt(Wc3StringHash.hash(s.getVal()));
}
public ILconstInt StringLength(ILconstString string) {
@@ -110,13 +103,26 @@ public ILconstString SubString(ILconstString istr, ILconstInt start, ILconstInt
// since this is most likely a bug in your code, the interpreter will throw an exception instead:
throw new InterpreterException("SubString called with start index " + start + " greater than string length " + str.length());
}
- return new ILconstString(str.substring(s, e));
+ return ILconstString.ofBytes(str.substring(s, e));
}
+ /**
+ * Only ascii letters change case. A string is a sequence of bytes, and the bytes of a multibyte
+ * character are not letters to case at all - folding them the way a latin-1 char would fold
+ * rewrites the character into a different one.
+ */
public ILconstString StringCase(ILconstString string, ILconstBool upperCase) {
- return new ILconstString(
- upperCase.getVal() ?
- string.getVal().toUpperCase()
- : string.getVal().toLowerCase());
+ String bytes = string.getVal();
+ StringBuilder result = new StringBuilder(bytes.length());
+ for (int i = 0; i < bytes.length(); i++) {
+ char c = bytes.charAt(i);
+ if (upperCase.getVal() && c >= 'a' && c <= 'z') {
+ c -= 32;
+ } else if (!upperCase.getVal() && c >= 'A' && c <= 'Z') {
+ c += 32;
+ }
+ result.append(c);
+ }
+ return ILconstString.ofBytes(result.toString());
}
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java
index 28eca76c7..a4a72a9f7 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java
@@ -66,10 +66,10 @@ public ILconstReal GetUnitFacing(IlConstHandle unit) {
public ILconstString GetUnitName(IlConstHandle unit) {
if (unit == null) {
- return new ILconstString("");
+ return ILconstString.fromText("");
}
UnitMock unitMock = (UnitMock) unit.getObj();
- return new ILconstString(ObjectHelper.objectIdIntToString(unitMock.unitid.getVal()));
+ return ILconstString.fromText(ObjectHelper.objectIdIntToString(unitMock.unitid.getVal()));
}
public ILconstInt GetUnitGoldCost(ILconstInt unitid) {
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/WurstflectionProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/WurstflectionProvider.java
index 51830b7d9..4f86d0cd0 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/WurstflectionProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/WurstflectionProvider.java
@@ -25,7 +25,7 @@ public ILconstString typeIdToTypeName(ILconstInt typeId) {
int typeIdInt = typeId.getVal();
for (Map.Entry
+ * The value is held one char per byte, so every character in it is below 256 and Java's own length
+ * and substring already give the game's answers. Text arriving from source or from the host has to be
+ * encoded on the way in ({@link #fromText}), and anything leaving the interpreter for a file or a
+ * screen decoded on the way out ({@link #text()}). A half character has no text to decode to, which is
+ * the point: it keeps its byte until the other half is added back.
+ */
public class ILconstString extends ILconstAbstract implements ILconstAddable {
- private final String val; // including the quotes
+ /** One char per byte, so all chars are < 256. */
+ private final String val;
- public ILconstString(String strVal) {
- this.val = strVal;
+ private ILconstString(String byteView) {
+ this.val = byteView;
}
+ /** Encodes text, which Wurst source and the host both give as UTF-8. */
+ public static ILconstString fromText(String text) {
+ return new ILconstString(encode(text));
+ }
+
+ /** Wraps bytes which are already one per char, as produced by slicing or joining. */
+ public static ILconstString ofBytes(String byteView) {
+ return new ILconstString(byteView);
+ }
+
+ static String encode(String text) {
+ return new String(text.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
+ }
+
+ static String decode(String byteView) {
+ return new String(byteView.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
+ }
+
+ /** The bytes, one per char. This is what the string natives count and index. */
public String getVal() {
return val;
}
+ /** The text those bytes spell, for anything which leaves the interpreter. */
+ public String text() {
+ return decode(val);
+ }
+
+ /**
+ * Whether the bytes spell text at all.
+ *
+ * Half of a character does not, and it cannot leave the interpreter: it has to become a literal
+ * in the generated script, which is written as UTF-8 and escapes nothing numerically, so the byte
+ * has no way to be written down. Decoding it anyway turns it into a replacement character, which
+ * is three bytes where the interpreter counted one.
+ */
+ public boolean isText() {
+ return encode(decode(val)).equals(val);
+ }
+
@Override
public String print() {
- return "\"" + val + "\"";
+ return "\"" + text() + "\"";
}
public WurstType getType() {
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/Wc3StringHash.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/Wc3StringHash.java
new file mode 100644
index 000000000..c5630e991
--- /dev/null
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/Wc3StringHash.java
@@ -0,0 +1,96 @@
+package de.peeeq.wurstscript.intermediatelang;
+
+/**
+ * {@code StringHash} over bytes.
+ *
+ * The library's version takes text and encodes it as UTF-8 itself, which cannot express a string
+ * holding half of a character - and half characters are exactly what the standard library hashes to
+ * find out how the engine represents them. Decoding first would collapse every partial slice onto one
+ * value, so the continuation bytes the standard library tells apart by hash would stop being
+ * distinguishable.
+ *
+ * The function is Bob Jenkins' lookup2, the same one the library implements; only the input differs.
+ * {@code Wc3StringHashTest} checks the two agree on text, where both are defined.
+ */
+public final class Wc3StringHash {
+
+ private Wc3StringHash() {
+ }
+
+ /**
+ * @param bytes one byte per char, all below 256
+ */
+ public static int hash(String bytes) {
+ if (bytes.isEmpty()) {
+ return 0;
+ }
+ byte[] normalized = new byte[bytes.length()];
+ for (int i = 0; i < bytes.length(); i++) {
+ byte b = (byte) bytes.charAt(i);
+ // Case insensitive, and either slash names the same file. Both comparisons are on signed
+ // bytes, so anything above 127 - every byte of a multibyte character - is left alone.
+ if (b >= 'a' && b <= 'z') {
+ b -= 32;
+ } else if (b == '/') {
+ b = '\\';
+ }
+ normalized[i] = b;
+ }
+ return lookup2(normalized);
+ }
+
+ private static int lookup2(byte[] k) {
+ int a = 0x9e3779b9;
+ int b = 0x9e3779b9;
+ int c = 0;
+ int len = k.length;
+ int i = 0;
+ while (len >= 12) {
+ a += u(k[i]) + (u(k[i + 1]) << 8) + (u(k[i + 2]) << 16) + (u(k[i + 3]) << 24);
+ b += u(k[i + 4]) + (u(k[i + 5]) << 8) + (u(k[i + 6]) << 16) + (u(k[i + 7]) << 24);
+ c += u(k[i + 8]) + (u(k[i + 9]) << 8) + (u(k[i + 10]) << 16) + (u(k[i + 11]) << 24);
+ // mix
+ a -= b; a -= c; a ^= (c >>> 13);
+ b -= c; b -= a; b ^= (a << 8);
+ c -= a; c -= b; c ^= (b >>> 13);
+ a -= b; a -= c; a ^= (c >>> 12);
+ b -= c; b -= a; b ^= (a << 16);
+ c -= a; c -= b; c ^= (b >>> 5);
+ a -= b; a -= c; a ^= (c >>> 3);
+ b -= c; b -= a; b ^= (a << 10);
+ c -= a; c -= b; c ^= (b >>> 15);
+ i += 12;
+ len -= 12;
+ }
+ c += k.length;
+ // The low byte of c holds the length, so the tail starts at the second.
+ switch (len) {
+ case 11: c += u(k[i + 10]) << 24;
+ case 10: c += u(k[i + 9]) << 16;
+ case 9: c += u(k[i + 8]) << 8;
+ case 8: b += u(k[i + 7]) << 24;
+ case 7: b += u(k[i + 6]) << 16;
+ case 6: b += u(k[i + 5]) << 8;
+ case 5: b += u(k[i + 4]);
+ case 4: a += u(k[i + 3]) << 24;
+ case 3: a += u(k[i + 2]) << 16;
+ case 2: a += u(k[i + 1]) << 8;
+ case 1: a += u(k[i]);
+ default:
+ }
+ a -= b; a -= c; a ^= (c >>> 13);
+ b -= c; b -= a; b ^= (a << 8);
+ c -= a; c -= b; c ^= (b >>> 13);
+ a -= b; a -= c; a ^= (c >>> 12);
+ b -= c; b -= a; b ^= (a << 16);
+ c -= a; c -= b; c ^= (b >>> 5);
+ a -= b; a -= c; a ^= (c >>> 3);
+ b -= c; b -= a; b ^= (a << 10);
+ c -= a; c -= b; c ^= (b >>> 15);
+ return c;
+ }
+
+ private static int u(byte b) {
+ return b & 0xFF;
+ }
+}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java
index df15e6d64..25a35c851 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java
@@ -142,7 +142,7 @@ public static ILaddress evaluateLvalue(ImStatementExpr e, ProgramState globalSta
}
public static ILconst eval(ImStringVal e, ProgramState globalState, LocalState localState) {
- return new ILconstString(e.getValS());
+ return ILconstString.fromText(e.getValS());
}
public static ILconst eval(ImTupleExpr e, ProgramState globalState, LocalState localState) {
@@ -334,7 +334,7 @@ public static ILconst eval(ImGetStackTrace e, ProgramState globalState,
LocalState localState) {
StringBuilder sb = new StringBuilder();
globalState.getStackFrames().appendTo(sb);
- return new ILconstString(sb.toString());
+ return ILconstString.fromText(sb.toString());
}
public static ILconst eval(ImCompiletimeExpr expr, ProgramState globalState, LocalState localState) {
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java
index 467af1c5c..56d0b5f06 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java
@@ -623,7 +623,7 @@ public void runVoidFunc(ImFunction f, @Nullable Element trace) {
ILconst[] args = {};
if (!f.getParameters().isEmpty()) {
// this should only happen because of added stacktrace parameter
- args = new ILconstString[]{new ILconstString("initial call")};
+ args = new ILconstString[]{ILconstString.fromText("initial call")};
}
runFunc(globalState, f, trace, args);
}
diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java
index 9f984c0bf..c19764dc7 100644
--- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java
+++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java
@@ -51,8 +51,8 @@ public void modifyObjectKeepsDifferentDataPointers() throws Exception {
modifyObject.setAccessible(true);
MetaFieldId unrealId = MetaFieldId.valueOf("unat");
- modifyObject.invoke(natives, obj, new ILconstString(unrealId.getVal()), ObjMod.ValType.UNREAL, 1, 0, War3Real.valueOf(1.0));
- modifyObject.invoke(natives, obj, new ILconstString(unrealId.getVal()), ObjMod.ValType.UNREAL, 1, 1, War3Real.valueOf(2.0));
+ modifyObject.invoke(natives, obj, ILconstString.fromText(unrealId.getVal()), ObjMod.ValType.UNREAL, 1, 0, War3Real.valueOf(1.0));
+ modifyObject.invoke(natives, obj, ILconstString.fromText(unrealId.getVal()), ObjMod.ValType.UNREAL, 1, 1, War3Real.valueOf(2.0));
List
+ * The standard library depends on the difference rather than avoiding it: {@code String.wurst} cuts a
+ * character in half on purpose to find out how the engine represents a partial slice, and slices a
+ * 64 character literal byte by byte to enumerate every continuation byte. Under UTF-16 that detection
+ * quietly concludes the engine has no multibyte characters, so anything computed at compiletime -
+ * object editor text, chunked data - is built from lengths the game will not agree with.
+ */
+public class StringByteSemanticsTests extends WurstScriptTest {
+
+ private static String[] program(String... body) {
+ String[] head = {
+ "package test",
+ "native testSuccess()",
+ "@extern native StringLength(string s) returns int",
+ "@extern native SubString(string s, int start, int stop) returns string",
+ "@extern native StringHash(string s) returns int",
+ };
+ String[] all = new String[head.length + body.length];
+ System.arraycopy(head, 0, all, 0, head.length);
+ System.arraycopy(body, 0, all, head.length, body.length);
+ return all;
+ }
+
+ /**
+ * Written as an escape rather than as itself, so what reaches the compiler does not depend on
+ * the encoding javac happens to read this file with.
+ */
+ private static final String A_UMLAUT = "ä";
+
+ /** Two bytes in UTF-8, and the game counts bytes. */
+ private static final String[] LENGTH_OF_A_TWO_BYTE_CHARACTER = program(
+ "init",
+ " if StringLength(\"" + A_UMLAUT + "\") == 2",
+ " testSuccess()"
+ );
+
+ @Test
+ public void lengthCountsBytes() {
+ testAssertOkLines(true, LENGTH_OF_A_TWO_BYTE_CHARACTER);
+ }
+
+ @Test
+ public void lengthCountsBytesLua() {
+ test().testLua(true).executeProg().lines(LENGTH_OF_A_TWO_BYTE_CHARACTER);
+ }
+
+ /** A slice may stop between the bytes of one character, which is how the stdlib probes. */
+ private static final String[] SLICING_A_CHARACTER_IN_HALF = program(
+ "init",
+ " let half = SubString(\"ä\", 0, 1)",
+ " if StringLength(half) == 1 and half != \"ä\"",
+ " testSuccess()"
+ );
+
+ @Test
+ public void aSliceCanCutACharacterInHalf() {
+ testAssertOkLines(true, SLICING_A_CHARACTER_IN_HALF);
+ }
+
+ @Test
+ public void aSliceCanCutACharacterInHalfLua() {
+ test().testLua(true).executeProg().lines(SLICING_A_CHARACTER_IN_HALF);
+ }
+
+ /** The halves are the bytes of the whole, so putting them back gives it back. */
+ private static final String[] HALVES_REJOIN = program(
+ "init",
+ " let s = \"äö\"",
+ " if SubString(s, 0, 2) + SubString(s, 2, 4) == s and StringLength(s) == 4",
+ " testSuccess()"
+ );
+
+ @Test
+ public void slicesRejoinIntoTheOriginal() {
+ testAssertOkLines(true, HALVES_REJOIN);
+ }
+
+ @Test
+ public void slicesRejoinIntoTheOriginalLua() {
+ test().testLua(true).executeProg().lines(HALVES_REJOIN);
+ }
+
+ /**
+ * Whole characters cross from compiletime into the generated script and keep their length, so the
+ * value the interpreter computed is the value the program runs with.
+ */
+ @Test
+ public void aCompiletimeStringKeepsItsLengthAfterTransforms() {
+ test().executeProg().runCompiletimeFunctions(true).lines(program(
+ "function compiletime(string s) returns string",
+ " return s",
+ "constant string GREETING = compiletime(\"h" + A_UMLAUT + "llo\")",
+ "init",
+ " if StringLength(GREETING) == 6 and GREETING == \"h" + A_UMLAUT + "llo\"",
+ " testSuccess()"
+ ));
+ }
+
+ /**
+ * Half a character cannot. It has to become a literal in a script written as UTF-8, and neither
+ * Jass nor the escaping has a way to write a byte down numerically, so it would go in as the
+ * replacement character and come back out three bytes long rather than one. Refused instead, and
+ * this pins that it is refused rather than silently carried across at a different length.
+ */
+ @Test
+ public void aCompiletimeStringHoldingHalfACharacterIsRefused() {
+ test().executeProg().runCompiletimeFunctions(true)
+ .expectError("part of a multibyte character")
+ .lines(program(
+ "function compiletime(string s) returns string",
+ " return s",
+ "constant string HALF = compiletime(SubString(\"" + A_UMLAUT + "\", 0, 1))",
+ "init",
+ " if StringLength(HALF) == 1",
+ " testSuccess()"
+ ));
+ }
+}
diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/Wc3StringHashTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/Wc3StringHashTest.java
new file mode 100644
index 000000000..a466a8cf5
--- /dev/null
+++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/Wc3StringHashTest.java
@@ -0,0 +1,90 @@
+package tests.wurstscript.tests;
+
+import de.peeeq.wurstscript.intermediatelang.ILconstString;
+import de.peeeq.wurstscript.intermediatelang.Wc3StringHash;
+import org.testng.annotations.Test;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Random;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+
+/**
+ * Checks the byte hash against the library's, which is the same function reached through text. They
+ * are only comparable where the library is defined - a string of whole characters - so that is what
+ * is compared, and it is enough: the arithmetic is the same for every input, only the bytes differ.
+ */
+public class Wc3StringHashTest {
+
+ private static void agrees(String text) throws UnsupportedEncodingException {
+ assertEquals(Wc3StringHash.hash(ILconstString.fromText(text).getVal()),
+ net.moonlightflower.wc3libs.misc.StringHash.hash(text),
+ "hash of " + text);
+ }
+
+ @Test
+ public void agreesOnShortStrings() throws UnsupportedEncodingException {
+ for (String s : new String[]{"", "a", "ab", "abc", "abcd", "abcde", "abcdef", "abcdefg",
+ "abcdefgh", "abcdefghi", "abcdefghij", "abcdefghijk", "abcdefghijkl", "abcdefghijklm"}) {
+ agrees(s);
+ }
+ }
+
+ /** Case folding and the slash rule are part of the hash, not of the caller. */
+ @Test
+ public void agreesOnStringsNeedingNormalisation() throws UnsupportedEncodingException {
+ for (String s : new String[]{"ABC", "AbC", "path/to/file", "path\\to\\file",
+ "Units\\Human\\Footman.mdx", "MIXED/Case\\Path"}) {
+ agrees(s);
+ }
+ }
+
+ /** Multibyte text still agrees, because whole characters decode back to themselves. */
+ @Test
+ public void agreesOnMultibyteText() throws UnsupportedEncodingException {
+ for (String s : new String[]{"ä", "äöü", "ЀЁЂЃ", "日本語", "😀", "aäböcü1234567890"}) {
+ agrees(s);
+ }
+ }
+
+ @Test
+ public void agreesOnRandomAsciiOfEveryLength() throws UnsupportedEncodingException {
+ Random random = new Random(20260816);
+ for (int length = 0; length < 40; length++) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ sb.append((char) (32 + random.nextInt(95)));
+ }
+ agrees(sb.toString());
+ }
+ }
+
+ /**
+ * The reason for hashing bytes rather than text. Each half of a two byte character has to keep a
+ * hash of its own; decoding first would turn both into the same replacement character.
+ */
+ @Test
+ public void halvesOfACharacterHashApart() {
+ String bytes = ILconstString.fromText("ä").getVal();
+ String lead = bytes.substring(0, 1);
+ String continuation = bytes.substring(1, 2);
+ assertNotEquals(Wc3StringHash.hash(lead), Wc3StringHash.hash(continuation),
+ "the two bytes of a character must not hash alike");
+ assertNotEquals(Wc3StringHash.hash(lead), Wc3StringHash.hash(bytes),
+ "half a character must not hash like the whole");
+ }
+
+ /**
+ * The standard library enumerates every continuation byte by slicing one literal and keeps them
+ * apart by hash, so all 64 have to be distinct.
+ */
+ @Test
+ public void everyContinuationByteHashesApart() {
+ java.util.Set