diff --git a/AGENTS.md b/AGENTS.md
index db7ed68ad..864d99705 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -59,40 +59,10 @@ Inside `de.peeeq.wurstscript`:
## 2. Agent Expectations
-Changes made by agents must follow these principles:
-
-### Compatibility first
-
-* **All existing tests must continue to pass.**
-* If behavior changes intentionally, provide **new tests** that define the updated semantics.
-
-### Minimal, well-scoped edits
-
-Agents should:
-
-* Fix concrete bugs with **small, local patches**.
-* Add missing null-checks, defensive checks, or diagnostics where appropriate.
-* Add tests when resolving issues or implementing requested features.
-
-Agents should avoid:
-
-* Large refactors (renaming packages, structural moves, mass rewrites).
-* Modifying deprecated folders unless explicitly instructed.
-* Altering public semantics or language rules without tests demonstrating the intended outcome.
-
-### Test-driven
-
-* Any new behavior requires tests showing failure before the change and success after.
-* Use existing test style and harnesses.
-
-### Generated code
-
-* Do **not** modify files generated by `:gen`.
-* If modifying `.parseq` files or grammars, regenerate via:
-
- ```
- ./gradlew :gen
- ```
+* **All existing tests must continue to pass.** The authoritative behavior is defined by the existing test suite.
+* **Test-driven**: new behavior requires tests showing failure before the change and success after, in the existing test style. Bug fixes start with a failing repro.
+* **Minimal, well-scoped edits**: small local patches; no large refactors (renames, structural moves, mass rewrites), no changes to deprecated folders, no altered language semantics without tests demonstrating the intended outcome, no new external dependencies unless requested.
+* **Generated code**: never modify files generated by `:gen`; change the `.parseq` specs or grammars and regenerate (`./gradlew :gen`). Adding a node type to a `.parseq` sum type breaks every exhaustive matcher at compile time — fix those compile errors, they are the complete checklist.
---
@@ -122,75 +92,26 @@ Use the conventions already present in the file you edit. Avoid introducing new
---
-## 4. Allowed vs. Disallowed Changes
-
-### Allowed
-
-* Fix a crash or incorrect behavior in a specific compiler pass.
-* Add a regression test that demonstrates a known issue.
-* Improve clarity of error messages.
-* Add a small new feature when fully specified by the user and backed by tests.
-* Update Gradle/JDK usage only if part of a requested task.
-
-### Disallowed
-
-* Unsolicited rewrites of ANTLR grammars.
-* Modifying deprecated folders.
-* Changing code generation semantics without explicit tests.
-* Changing IM behavior without test coverage.
-* Introducing new external dependencies unless requested.
-
----
-
-## 5. How to Run Tests and Code Generation
-
-Inside
+## 4. How to Run Tests and Code Generation
-```
-de.peeeq.wurstscript/
-```
-
-run:
-
-### Run all tests
-
-```
-./gradlew test
-```
-
-### Run a specific test
+The Gradle wrapper lives inside `de.peeeq.wurstscript/` (not the repo root); run all commands from there.
```
-./gradlew test --tests "tests.wurstscript.tests.GenericsWithTypeclassesTests.identity"
+./gradlew test # all tests
+./gradlew test --tests "tests.wurstscript.tests.SomeTestClass.someMethod" # one test
+./gradlew :gen # regenerate AST (parseq) + ANTLR
+./gradlew build # build the compiler
```
-### Generate AST code via ANTLR & abstractsyntaxgen
-
-```
-./gradlew :gen
-```
+### Runtime-executing tests
-### Build the compiler
-
-```
-./gradlew build
-```
+* `test().executeProg()` runs the compiled program in the interpreter and requires a `testSuccess()` call.
+* `test().testLua(true).executeProg()` additionally syntax-checks the emitted Lua with luac and executes it with a real Lua 5.3 interpreter against the WC3 runtime in `src/test/resources/luaruntime/` (wc3shim + Reforged `common.j.lua`/`blizzard.j.lua` dumps). Interpreter discovery: bundled `src/test/resources/lua.exe` on Windows, `lua53` on Linux, else PATH; tests skip visibly when none is found.
+* Use `LuaBackendAuditTests` as the reference style for backend regression repros.
---
-## 6. Summary for Agents
-
-* Keep changes **minimal**, **compatible**, and **tested**.
-* The authoritative behavior is defined by the **existing test suite**.
-* The compiler architecture relies on CST → AST → IM → Backend; treat each stage carefully.
-* Never modify generated files; modify the sources that generate them instead.
-* New behavior must be documented through tests.
-
-
-
----
-
-## 7. LSP Structure and Build Pipelines
+## 5. LSP Structure and Build Pipelines
This repository has multiple entry points that may trigger compilation/build behavior:
@@ -244,7 +165,7 @@ This pipeline handles:
---
-## 8. Shared Project Config, Patch Targets, and Run Behavior
+## 6. Shared Project Config, Patch Targets, and Run Behavior
Recent Grill/compiler integration work moved `wurst.build` parsing rules into a tiny shared dependency. Keep compiler behavior aligned with that shared model.
@@ -287,7 +208,7 @@ For config and run-pipeline changes, prefer these focused checks before broader
---
-## 9. Backend Parity and Lua Guardrails
+## 7. Backend Parity and Lua Guardrails
Recent fixes established additional rules for backend work. Follow these for all future changes:
@@ -299,6 +220,11 @@ Recent fixes established additional rules for backend work. Follow these for all
* the reason is backend/runtime-specific, and
* the difference is documented in tests.
+### Reference semantics for arithmetic
+
+* Integer/real division and modulo semantics are centralized: `WurstOperator.moduloInteger/moduloReal` implement the Blizzard.j formula (truncated remainder, plus divisor if negative) and Jass `div` truncates toward zero.
+* The Lua polyfills (`intDiv`/`wurstMod`), the interpreter's `MathProvider` mocks, and constant folding (`SimpleRewrites`, `ConstantAndCopyPropagation`) must all stay consistent with those helpers — never reimplement div/mod locally.
+
### Error behavior parity expectations
* Prefer matching Jass behavior semantically in Lua output.
@@ -331,7 +257,7 @@ Recent fixes established additional rules for backend work. Follow these for all
---
-## 10. Virtual Slot Binding and Determinism (New Generics + Lua)
+## 8. Virtual Slot Binding and Determinism (New Generics + Lua)
Recent regressions showed that virtual-slot binding can silently degrade to base/no-op implementations in generated Lua while still compiling. Follow these rules for all related changes:
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java
index bd559408e..281f76e27 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java
@@ -891,7 +891,7 @@ public LuaCompilationUnit transformProgToLua() {
// Lower Lua-specific native calls into IM-level wrappers before optimization,
// so the optimizer can inline and eliminate the nil-safety checks and remapped stubs.
beginPhase(4, "lua native lowering");
- LuaNativeLowering.transform(imProg);
+ LuaNativeLowering.transform(imProg, imTranslator2);
timeTaker.endPhase();
// inliner
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 3c5c2668c..6fe1720ff 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
@@ -1,31 +1,59 @@
package de.peeeq.wurstio.jassinterpreter.providers;
-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.interpreter.AbstractInterpreter;
+/**
+ * Interpreter-side implementations of the raw Lua-primitive leaves that the
+ * portable Lua ensureType/div/mod helpers delegate to (see
+ * de.peeeq.wurstscript.translation.imtranslation.LuaEnsureFunctions and
+ * LuaNativeLowering#lowerDivMod). Those helpers are plain, non-native IM
+ * functions now - the interpreter runs their body directly - so only the
+ * handful of leaves with no IM equivalent (Lua's {@code //} floor division,
+ * {@code math.fmod}, {@code tonumber}, {@code math.tointeger}, string
+ * concatenation) still need a native implementation here.
+ *
+ *
The interpreter's own type system already guarantees these are only
+ * ever called with a genuinely-typed, non-null argument (the callers' nil
+ * checks run as regular interpreted control flow first), so unlike the real
+ * Lua runtime these can be simple, non-defensive passthroughs.
+ */
public class LuaEnsureTypeProvider extends Provider {
public LuaEnsureTypeProvider(AbstractInterpreter interpreter) {
super(interpreter);
}
- public ILconstInt intEnsure(ILconstInt x) {
+ public ILconstInt __wurst_rawToNumberInt(ILconstInt x) {
return x;
}
- public ILconstString stringEnsure(ILconstString x) {
+ public ILconstInt __wurst_rawToInteger(ILconstInt x) {
return x;
}
- public ILconstBool boolEnsure(ILconstBool x) {
+ public ILconstReal __wurst_rawToNumberReal(ILconstReal x) {
return x;
}
- public ILconstReal realEnsure(ILconstReal x) {
+ public ILconstString __wurst_rawToString(ILconstString x) {
return x;
}
- public ILconstString stringConcat(ILconstString x, ILconstString y) { return new ILconstString(x.getVal() + y.getVal()); }
+ public ILconstString __wurst_rawConcat(ILconstString x, ILconstString y) {
+ return new ILconstString(x.getVal() + y.getVal());
+ }
+
+ public ILconstInt __wurst_rawFloorDivInt(ILconstInt a, ILconstInt b) {
+ return ILconstInt.create(Math.floorDiv(a.getVal(), b.getVal()));
+ }
+
+ public ILconstInt __wurst_rawFmodInt(ILconstInt a, ILconstInt b) {
+ return ILconstInt.create(a.getVal() % b.getVal());
+ }
+
+ public ILconstReal __wurst_rawFmodReal(ILconstReal a, ILconstReal b) {
+ return ILconstReal.create(a.getVal() % b.getVal());
+ }
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java
index 592a982f8..8551a3f8c 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java
@@ -200,16 +200,14 @@ public ImProg translateProg() {
false)), ImVoid(), ImVars(), ImStmts(), flags(IS_NATIVE, IS_BJ));
if(isLuaTarget()) {
- ensureIntFunc = JassIm.ImFunction(emptyTrace, "intEnsure", ImTypeVars(), ImVars(JassIm.ImVar(wurstProg, WurstTypeInt.instance().imTranslateType(this), "x", false)), WurstTypeInt.instance().imTranslateType(this), ImVars(), ImStmts(), flags(IS_NATIVE, IS_BJ));
- ensureBoolFunc = JassIm.ImFunction(emptyTrace, "boolEnsure", ImTypeVars(), ImVars(JassIm.ImVar(wurstProg, WurstTypeBool.instance().imTranslateType(this), "x", false)), WurstTypeBool.instance().imTranslateType(this), ImVars(), ImStmts(), flags(IS_NATIVE, IS_BJ));
- ensureRealFunc = JassIm.ImFunction(emptyTrace, "realEnsure", ImTypeVars(), ImVars(JassIm.ImVar(wurstProg, WurstTypeReal.instance().imTranslateType(this), "x", false)), WurstTypeReal.instance().imTranslateType(this), ImVars(), ImStmts(), flags(IS_NATIVE, IS_BJ));
- ensureStrFunc = JassIm.ImFunction(emptyTrace, "stringEnsure", ImTypeVars(), ImVars(JassIm.ImVar(wurstProg, WurstTypeString.instance().imTranslateType(this), "x", false)), WurstTypeString.instance().imTranslateType(this), ImVars(), ImStmts(), flags(IS_NATIVE, IS_BJ));
- stringConcatFunc =JassIm.ImFunction(emptyTrace, "stringConcat", ImTypeVars(), ImVars(JassIm.ImVar(wurstProg, WurstTypeString.instance().imTranslateType(this), "x", false),JassIm.ImVar(wurstProg, WurstTypeString.instance().imTranslateType(this), "y", false)), WurstTypeString.instance().imTranslateType(this), ImVars(), ImStmts(), flags(IS_NATIVE, IS_BJ));
- addFunction(ensureIntFunc);
- addFunction(ensureBoolFunc);
- addFunction(ensureRealFunc);
- addFunction(ensureStrFunc);
- addFunction(stringConcatFunc);
+ // Portable IM bodies (not IS_NATIVE stubs) - see LuaEnsureFunctions for why.
+ List luaHelperFunctions = new ArrayList<>();
+ ensureIntFunc = LuaEnsureFunctions.buildEnsureInt(luaHelperFunctions);
+ ensureBoolFunc = LuaEnsureFunctions.buildEnsureBool(luaHelperFunctions);
+ ensureRealFunc = LuaEnsureFunctions.buildEnsureReal(luaHelperFunctions);
+ ensureStrFunc = LuaEnsureFunctions.buildEnsureStr(luaHelperFunctions);
+ stringConcatFunc = LuaEnsureFunctions.buildStringConcat(luaHelperFunctions);
+ luaHelperFunctions.forEach(this::addFunction);
}
calculateCompiletimeOrder();
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java
new file mode 100644
index 000000000..a7467b44e
--- /dev/null
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaEnsureFunctions.java
@@ -0,0 +1,202 @@
+package de.peeeq.wurstscript.translation.imtranslation;
+
+import de.peeeq.wurstscript.WurstOperator;
+import de.peeeq.wurstscript.jassIm.*;
+import de.peeeq.wurstscript.types.TypesHelper;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Builds portable IM-level bodies for the Lua-target ensureType/stringConcat
+ * helpers ({@link ImTranslator#ensureIntFunc} and friends).
+ *
+ * These used to be {@code IS_NATIVE} stubs with an empty IM body; their
+ * actual Lua implementation was supplied only by an unrelated, separately
+ * created Lua-AST-level polyfill (in {@code LuaTranslator}/{@code
+ * LuaPolyfillSetup}) that happened to pick the exact same Lua global name.
+ * Nothing enforced that link - it worked only because the polyfill was
+ * always emitted first and native functions skip Wurst's own unique-name
+ * mangling (see {@code LuaTranslator.luaFunc}), so the guarded-redefinition
+ * Lua code these natives print ("if intEnsure then ... else intEnsure =
+ * function ... error('not implemented') ... end end") always found the slot
+ * already taken and never actually ran its own (broken) body. This is the
+ * same class of bug already found and fixed once for stringConcat's call
+ * sites specifically (see the identity-based dispatch in {@code
+ * lua.translation.ExprTranslation#translate(ImFunctionCall, ...)} and the
+ * {@code userFunctionNamedStringConcatDoesNotBreakConcatenation} regression
+ * test) - this removes the coincidence at the source instead of routing
+ * around it.
+ *
+ *
Giving these functions real, portable IM bodies also makes them
+ * inlinable and dead-code-eliminable like any other Wurst-internal
+ * function, instead of always-emitted opaque Lua source - the same
+ * treatment already applied to div/mod (see {@link LuaNativeLowering}).
+ */
+final class LuaEnsureFunctions {
+ private static final de.peeeq.wurstscript.ast.Element TRACE = de.peeeq.wurstscript.ast.Ast.NoExpr();
+
+ private LuaEnsureFunctions() {
+ }
+
+ /** local n = rawToNumberInt(x); local result = 0; if n ~= nil then local i = rawToInteger(n); if i ~= nil then result = i end end; return result */
+ static ImFunction buildEnsureInt(List out) {
+ ImType intType = TypesHelper.imInt();
+ ImFunction rawToNumber = rawNative("__wurst_rawToNumberInt", intType, 1);
+ ImFunction rawToInteger = rawNative("__wurst_rawToInteger", intType, 1);
+ out.add(rawToNumber);
+ out.add(rawToInteger);
+
+ ImVar x = JassIm.ImVar(TRACE, intType.copy(), "x", false);
+ ImVar n = JassIm.ImVar(TRACE, intType.copy(), "n", false);
+ ImVar i = JassIm.ImVar(TRACE, intType.copy(), "i", false);
+ ImVar result = JassIm.ImVar(TRACE, intType.copy(), "result", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(n), call(rawToNumber, JassIm.ImVarAccess(x))),
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImIntVal(0)),
+ JassIm.ImIf(TRACE, notNull(n),
+ JassIm.ImStmts(
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(i), call(rawToInteger, JassIm.ImVarAccess(n))),
+ JassIm.ImIf(TRACE, notNull(i),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(i))),
+ JassIm.ImStmts())
+ ),
+ JassIm.ImStmts()),
+ JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result))
+ );
+ ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureInt", JassIm.ImTypeVars(), JassIm.ImVars(x), intType.copy(),
+ JassIm.ImVars(n, i, result), body, Collections.emptyList());
+ out.add(f);
+ return f;
+ }
+
+ /** local result = false; if x ~= nil then result = x end; return result */
+ static ImFunction buildEnsureBool(List out) {
+ ImType boolType = TypesHelper.imBool();
+ ImVar x = JassIm.ImVar(TRACE, boolType.copy(), "x", false);
+ ImVar result = JassIm.ImVar(TRACE, boolType.copy(), "result", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImBoolVal(false)),
+ JassIm.ImIf(TRACE, notNull(x),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(x))),
+ JassIm.ImStmts()),
+ JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result))
+ );
+ ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureBool", JassIm.ImTypeVars(), JassIm.ImVars(x), boolType.copy(),
+ JassIm.ImVars(result), body, Collections.emptyList());
+ out.add(f);
+ return f;
+ }
+
+ /** local n = rawToNumberReal(x); local result = 0.0; if n ~= nil then result = n end; return result */
+ static ImFunction buildEnsureReal(List out) {
+ ImType realType = TypesHelper.imReal();
+ ImFunction rawToNumber = rawNative("__wurst_rawToNumberReal", realType, 1);
+ out.add(rawToNumber);
+
+ ImVar x = JassIm.ImVar(TRACE, realType.copy(), "x", false);
+ ImVar n = JassIm.ImVar(TRACE, realType.copy(), "n", false);
+ ImVar result = JassIm.ImVar(TRACE, realType.copy(), "result", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(n), call(rawToNumber, JassIm.ImVarAccess(x))),
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImRealVal("0.")),
+ JassIm.ImIf(TRACE, notNull(n),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(n))),
+ JassIm.ImStmts()),
+ JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result))
+ );
+ ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureReal", JassIm.ImTypeVars(), JassIm.ImVars(x), realType.copy(),
+ JassIm.ImVars(n, result), body, Collections.emptyList());
+ out.add(f);
+ return f;
+ }
+
+ /** local result = ""; if x ~= nil then result = rawToString(x) end; return result */
+ static ImFunction buildEnsureStr(List out) {
+ ImType stringType = TypesHelper.imString();
+ ImFunction rawToString = rawNative("__wurst_rawToString", stringType, 1);
+ out.add(rawToString);
+
+ ImVar x = JassIm.ImVar(TRACE, stringType.copy(), "x", false);
+ ImVar result = JassIm.ImVar(TRACE, stringType.copy(), "result", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImStringVal("")),
+ JassIm.ImIf(TRACE, notNull(x),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), call(rawToString, JassIm.ImVarAccess(x)))),
+ JassIm.ImStmts()),
+ JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result))
+ );
+ ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureStr", JassIm.ImTypeVars(), JassIm.ImVars(x), stringType.copy(),
+ JassIm.ImVars(result), body, Collections.emptyList());
+ out.add(f);
+ return f;
+ }
+
+ /**
+ * if x ~= nil then
+ * if y ~= nil then result = rawConcat(x, y) else result = x end
+ * else result = y end
+ * return result
+ */
+ static ImFunction buildStringConcat(List out) {
+ ImType stringType = TypesHelper.imString();
+ ImFunction rawConcat = rawNative("__wurst_rawConcat", stringType, 2);
+ out.add(rawConcat);
+
+ ImVar x = JassIm.ImVar(TRACE, stringType.copy(), "x", false);
+ ImVar y = JassIm.ImVar(TRACE, stringType.copy(), "y", false);
+ ImVar result = JassIm.ImVar(TRACE, stringType.copy(), "result", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImIf(TRACE, notNull(x),
+ JassIm.ImStmts(JassIm.ImIf(TRACE, notNull(y),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), call(rawConcat, JassIm.ImVarAccess(x), JassIm.ImVarAccess(y)))),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(x))))),
+ JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(y)))),
+ JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result))
+ );
+ ImFunction f = JassIm.ImFunction(TRACE, "__wurst_stringConcat", JassIm.ImTypeVars(), JassIm.ImVars(x, y), stringType.copy(),
+ JassIm.ImVars(result), body, Collections.emptyList());
+ out.add(f);
+ return f;
+ }
+
+ /** A native leaf with {@code paramCount} params and a return, all of the same type. Body supplied by LuaNatives. */
+ private static ImFunction rawNative(String name, ImType type, int paramCount) {
+ String[] names = {"x", "y"};
+ ImVars params = JassIm.ImVars();
+ for (int i = 0; i < paramCount; i++) {
+ params.add(JassIm.ImVar(TRACE, type.copy(), names[i], false));
+ }
+ return JassIm.ImFunction(TRACE, name, JassIm.ImTypeVars(), params, type.copy(),
+ JassIm.ImVars(), JassIm.ImStmts(), Collections.singletonList(FunctionFlagEnum.IS_NATIVE));
+ }
+
+ /**
+ * {@code v ~= nil}, tagged as a null of {@link ImAnyType} rather than
+ * v's own declared type. This matters specifically for string: {@code
+ * EliminateLocalTypes#transformProgram} runs after these functions are
+ * built and unconditionally rewrites every string-typed {@code ImNull}
+ * node in the program to {@code ImStringVal("")} (Wurst's "null string
+ * == empty string" convention for ordinary user code) - tagging with the
+ * declared type here would silently turn this into an
+ * {@code x ~= ""} check, so a genuinely-nil Lua value (e.g. an
+ * uninitialized bound-generic string field) would read as "not nil" and
+ * skip normalization. An ImAnyType-tagged null is exempt from that
+ * rewrite while still printing as plain Lua {@code nil} either way (see
+ * {@code lua.translation.ExprTranslation#translate(ImNull, ...)}), and
+ * the comparison's Lua translation only ever looks at the *left*
+ * operand's type, so this has no effect on the emitted code.
+ */
+ private static ImExpr notNull(ImVar v) {
+ return JassIm.ImOperatorCall(WurstOperator.NOTEQ, JassIm.ImExprs(JassIm.ImVarAccess(v), JassIm.ImNull(JassIm.ImAnyType())));
+ }
+
+ private static ImFunctionCall call(ImFunction f, ImExpr... args) {
+ return JassIm.ImFunctionCall(TRACE, f, JassIm.ImTypeArguments(), JassIm.ImExprs(args), false, CallType.NORMAL);
+ }
+}
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 e7f582cbb..490cdec6f 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
@@ -2,6 +2,7 @@
import de.peeeq.wurstscript.WurstOperator;
import de.peeeq.wurstscript.jassIm.*;
+import de.peeeq.wurstscript.types.TypesHelper;
import java.util.*;
@@ -109,7 +110,7 @@ private LuaNativeLowering() {}
* creating wrappers for every BJ function in the IM (common.j declares hundreds of
* functions, most of which are unreachable in any given program).
*/
- public static void transform(ImProg prog) {
+ public static void transform(ImProg prog, ImTranslator translator) {
// Replace all reads of MagicFunctions_isLua with true.
// This must happen before any optimizer passes so that dead-code elimination
// can remove Jass-only branches at compile time.
@@ -124,6 +125,9 @@ public static void transform(ImProg prog) {
}
}
+ lowerDivMod(prog);
+ lowerPrimitiveArrayEnsure(prog, translator);
+
// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
// Populated lazily during the traversal.
Map replacements = new LinkedHashMap<>();
@@ -213,6 +217,284 @@ private ImFunction computeReplacement(ImFunction bj) {
prog.getFunctions().addAll(deferredAdditions);
}
+ private static final de.peeeq.wurstscript.ast.Element SYNTHETIC_TRACE = de.peeeq.wurstscript.ast.Ast.NoExpr();
+
+ /**
+ * Rewrites {@code DIV_INT}/{@code MOD_INT}/{@code MOD_REAL} operator calls
+ * into calls against small, portable IM functions (not natives), instead
+ * of them being lowered directly to opaque, always-emitted Lua helper
+ * functions at Lua-emission time (after {@code ImOptimizer} has already
+ * run). Because these functions now live in the IM tree before inlining
+ * and dead-code elimination, non-constant div/mod at a hot call site can
+ * actually get inlined, and the helpers disappear entirely from programs
+ * that never use div/mod - both of which were previously impossible.
+ *
+ * Constant-constant div/mod already folds away earlier (see
+ * {@code SimpleRewrites}/{@code ConstantAndCopyPropagation}) and never
+ * reaches this rewrite; this only applies to runtime-value operands.
+ *
+ *
Exception: {@code I2S(1 div 0)} is ErrorHandling's deliberate,
+ * always-non-constant-looking crash trap - {@code
+ * lua.translation.ExprTranslation#translate(ImFunctionCall, ...)} pattern
+ * matches that exact {@code ImOperatorCall(DIV_INT, [1, 0])} shape as the
+ * sole argument of an {@code I2S} call and turns it into the {@code
+ * __wurst_abort_thread} sentinel every callback xpcall handler ignores.
+ * Lowering it here first would replace that shape with a call to the
+ * portable {@code __wurst_intDiv} helper, which the sentinel check does
+ * not recognize - the trap would then raise a real Lua {@code n//0}
+ * runtime error instead of the sentinel, breaking every callback error
+ * handler's "was this an intentional abort" check. Leave that one
+ * expression untouched so the existing recognition still fires.
+ */
+ private static void lowerDivMod(ImProg prog) {
+ DivModFunctions funcs = new DivModFunctions();
+ prog.accept(new Element.DefaultVisitor() {
+ @Override
+ public void visit(ImOperatorCall call) {
+ super.visit(call);
+ if (call.getArguments().size() != 2) {
+ return;
+ }
+ ImFunction target;
+ if (call.getOp() == WurstOperator.DIV_INT) {
+ if (isIntentionalThreadAbortDivByZero(call)) {
+ return;
+ }
+ target = funcs.intDiv();
+ } else if (call.getOp() == WurstOperator.MOD_INT) {
+ target = funcs.modInt();
+ } else if (call.getOp() == WurstOperator.MOD_REAL) {
+ target = funcs.modReal();
+ } else {
+ return;
+ }
+ List args = call.getArguments().removeAll();
+ call.replaceBy(JassIm.ImFunctionCall(call.attrTrace(), target,
+ JassIm.ImTypeArguments(), JassIm.ImExprs(args), false, CallType.NORMAL));
+ }
+ });
+ // Added only after the traversal completes - prog.getFunctions() is
+ // being iterated by the accept() call above, same reasoning as
+ // deferredAdditions in transform().
+ prog.getFunctions().addAll(funcs.createdFunctions());
+ }
+
+ /**
+ * True for exactly the {@code ImOperatorCall(DIV_INT, [1, 0])} shape that
+ * is the sole argument of a call to the native {@code I2S} - mirrors
+ * {@code lua.translation.ExprTranslation#isIntentionalThreadAbortCall}
+ * from the operator-call side, so both stay in agreement about what
+ * counts as the deliberate crash trap.
+ */
+ private static boolean isIntentionalThreadAbortDivByZero(ImOperatorCall call) {
+ ImExpr left = call.getArguments().get(0);
+ ImExpr right = call.getArguments().get(1);
+ if (!(left instanceof ImIntVal) || ((ImIntVal) left).getValI() != 1) {
+ return false;
+ }
+ if (!(right instanceof ImIntVal) || ((ImIntVal) right).getValI() != 0) {
+ return false;
+ }
+ // call's direct parent is the ImExprs argument-list container, not
+ // the ImFunctionCall itself - go up one more level to reach it (the
+ // same double getParent() pattern this codebase uses elsewhere for
+ // list-contained IM/AST elements).
+ Element argsList = call.getParent();
+ Element parent = argsList == null ? null : argsList.getParent();
+ if (!(parent instanceof ImFunctionCall)) {
+ return false;
+ }
+ ImFunctionCall parentCall = (ImFunctionCall) parent;
+ return parentCall.getArguments().size() == 1
+ && parentCall.getArguments().get(0) == call
+ && "I2S".equals(parentCall.getFunc().getName());
+ }
+
+ /**
+ * Rewrites reads (never writes - see {@link LValues#isUsedAsLValue}) of
+ * primitive-typed ({@code int}/{@code bool}/{@code real}/{@code string})
+ * array slots into calls against the portable {@code ensureXxx} IM
+ * functions ({@link ImTranslator#ensureIntFunc} and friends), instead of
+ * that normalization being applied later as opaque, always-emitted Lua
+ * source at Lua-emission time. Same treatment as {@link #lowerDivMod}:
+ * this makes a hot read optimizable (inlinable, foldable) instead of a
+ * fixed per-read function-call cost, and lets the helper disappear
+ * entirely from programs whose arrays are never read this way.
+ *
+ * The shared per-type array-default metatable (see {@code
+ * LuaTranslator#newDefaultArray}) already guarantees a typed, non-nil
+ * default on every miss, so this remains defensive hardening against
+ * values written from outside typed Wurst code, not a correctness
+ * requirement for pure Wurst-authored programs.
+ */
+ private static void lowerPrimitiveArrayEnsure(ImProg prog, ImTranslator translator) {
+ prog.accept(new Element.DefaultVisitor() {
+ @Override
+ public void visit(ImVarArrayAccess access) {
+ super.visit(access);
+ if (LValues.isUsedAsLValue(access)) {
+ return;
+ }
+ ImFunction ensureFunc = ensureFunctionFor(access.attrTyp(), translator);
+ if (ensureFunc == null) {
+ return;
+ }
+ access.replaceBy(JassIm.ImFunctionCall(access.attrTrace(), ensureFunc,
+ JassIm.ImTypeArguments(), JassIm.ImExprs(access.copy()), false, CallType.NORMAL));
+ }
+ });
+ }
+
+ private static ImFunction ensureFunctionFor(ImType type, ImTranslator translator) {
+ if (TypesHelper.isIntType(type)) {
+ return translator.ensureIntFunc;
+ } else if (TypesHelper.isBoolType(type)) {
+ return translator.ensureBoolFunc;
+ } else if (TypesHelper.isRealType(type)) {
+ return translator.ensureRealFunc;
+ } else if (TypesHelper.isStringType(type)) {
+ return translator.ensureStrFunc;
+ }
+ return null;
+ }
+
+ /**
+ * Lazily builds (and memoizes) the div/mod helper functions and the tiny
+ * raw-Lua-primitive natives they delegate to (Wurst's IM has no
+ * floor-division/fmod operator of its own).
+ */
+ private static final class DivModFunctions {
+ private final List created = new ArrayList<>();
+ private ImFunction rawFloorDivInt;
+ private ImFunction rawFmodInt;
+ private ImFunction rawFmodReal;
+ private ImFunction intDiv;
+ private ImFunction modInt;
+ private ImFunction modReal;
+
+ List createdFunctions() {
+ return created;
+ }
+
+ ImFunction intDiv() {
+ if (intDiv == null) {
+ intDiv = buildIntDiv(rawFloorDivInt());
+ created.add(intDiv);
+ }
+ return intDiv;
+ }
+
+ ImFunction modInt() {
+ if (modInt == null) {
+ modInt = buildMod("__wurst_modInt", TypesHelper.imInt(), JassIm.ImIntVal(0), rawFmodInt());
+ created.add(modInt);
+ }
+ return modInt;
+ }
+
+ ImFunction modReal() {
+ if (modReal == null) {
+ modReal = buildMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), rawFmodReal());
+ created.add(modReal);
+ }
+ return modReal;
+ }
+
+ private ImFunction rawFloorDivInt() {
+ if (rawFloorDivInt == null) {
+ rawFloorDivInt = rawNative("__wurst_rawFloorDivInt", TypesHelper.imInt());
+ created.add(rawFloorDivInt);
+ }
+ return rawFloorDivInt;
+ }
+
+ private ImFunction rawFmodInt() {
+ if (rawFmodInt == null) {
+ rawFmodInt = rawNative("__wurst_rawFmodInt", TypesHelper.imInt());
+ created.add(rawFmodInt);
+ }
+ return rawFmodInt;
+ }
+
+ private ImFunction rawFmodReal() {
+ if (rawFmodReal == null) {
+ rawFmodReal = rawNative("__wurst_rawFmodReal", TypesHelper.imReal());
+ created.add(rawFmodReal);
+ }
+ return rawFmodReal;
+ }
+
+ /** A native leaf with two params and a return, all of the same primitive type. Body supplied by LuaNatives. */
+ private static ImFunction rawNative(String name, ImType numType) {
+ ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false);
+ ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "b", false);
+ return JassIm.ImFunction(SYNTHETIC_TRACE, name, JassIm.ImTypeVars(), JassIm.ImVars(a, b), numType.copy(),
+ JassIm.ImVars(), JassIm.ImStmts(), Collections.singletonList(FunctionFlagEnum.IS_NATIVE));
+ }
+
+ /**
+ * local q = rawFloorDiv(a, b)
+ * if q < 0 and q * b ~= a then q = q + 1 end
+ * return q
+ * (Lua's // truncates toward -inf; Jass integer division truncates toward zero.)
+ */
+ private static ImFunction buildIntDiv(ImFunction rawFloorDiv) {
+ ImType intType = TypesHelper.imInt();
+ ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, intType.copy(), "a", false);
+ ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, intType.copy(), "b", false);
+ ImVar q = JassIm.ImVar(SYNTHETIC_TRACE, intType.copy(), "q", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(q), call(rawFloorDiv, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b))),
+ JassIm.ImIf(SYNTHETIC_TRACE,
+ JassIm.ImOperatorCall(WurstOperator.AND, JassIm.ImExprs(
+ JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(q), JassIm.ImIntVal(0))),
+ JassIm.ImOperatorCall(WurstOperator.NOTEQ, JassIm.ImExprs(
+ JassIm.ImOperatorCall(WurstOperator.MULT, JassIm.ImExprs(JassIm.ImVarAccess(q), JassIm.ImVarAccess(b))),
+ JassIm.ImVarAccess(a)
+ ))
+ )),
+ JassIm.ImStmts(JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(q),
+ JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImVarAccess(q), JassIm.ImIntVal(1))))),
+ JassIm.ImStmts()
+ ),
+ JassIm.ImReturn(SYNTHETIC_TRACE, JassIm.ImVarAccess(q))
+ );
+ return JassIm.ImFunction(SYNTHETIC_TRACE, "__wurst_intDiv", JassIm.ImTypeVars(), JassIm.ImVars(a, b), intType.copy(),
+ JassIm.ImVars(q), body, Collections.emptyList());
+ }
+
+ /**
+ * local r = rawFmod(a, b)
+ * if r < 0 then r = r + b end
+ * return r
+ * (Lua's % is floored; Wurst mod follows Blizzard.j's ModuloInteger/ModuloReal:
+ * truncated remainder, plus the divisor when the remainder is negative.)
+ */
+ private static ImFunction buildMod(String name, ImType numType, ImExpr zeroLiteral, ImFunction rawFmod) {
+ ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false);
+ ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "b", false);
+ ImVar r = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "r", false);
+
+ ImStmts body = JassIm.ImStmts(
+ JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), call(rawFmod, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b))),
+ JassIm.ImIf(SYNTHETIC_TRACE,
+ JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(r), zeroLiteral)),
+ JassIm.ImStmts(JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r),
+ JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImVarAccess(r), JassIm.ImVarAccess(b))))),
+ JassIm.ImStmts()
+ ),
+ JassIm.ImReturn(SYNTHETIC_TRACE, JassIm.ImVarAccess(r))
+ );
+ return JassIm.ImFunction(SYNTHETIC_TRACE, name, JassIm.ImTypeVars(), JassIm.ImVars(a, b), numType.copy(),
+ JassIm.ImVars(r), body, Collections.emptyList());
+ }
+
+ private static ImFunctionCall call(ImFunction f, ImExpr... args) {
+ return JassIm.ImFunctionCall(SYNTHETIC_TRACE, f, JassIm.ImTypeArguments(), JassIm.ImExprs(args), false, CallType.NORMAL);
+ }
+ }
+
/**
* Creates a new IS_NATIVE (non-BJ) IM function stub with the same signature as
* {@code original}. The Lua translator will fill in the body via
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java
index 6814e0b06..61071e042 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java
@@ -109,11 +109,10 @@ static String callErrorFunc(LuaTranslator tr, String msg) {
public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
// String concatenation is lowered to imTr.stringConcatFunc at the IM level
- // (see EliminateLocalTypes); route it explicitly to the Lua polyfill instead
- // of relying on both sides happening to print the same function name.
- if (e.getFunc() == tr.imTr.stringConcatFunc) {
- return LuaAst.LuaExprFunctionCall(tr.stringConcatFunction, tr.translateExprList(e.getArguments()));
- }
+ // (see EliminateLocalTypes). It is a plain, portable, uniquely-named IM
+ // function now (see LuaEnsureFunctions), so it needs no special-casing
+ // here - the generic function-call translation below handles it, the same
+ // way it handles any other Wurst-internal function.
String tcFunc = tr.getTypeCastingFunctionName(e.getFunc());
if (tcFunc != null && !e.getArguments().isEmpty()) {
LuaExpr arg = e.getArguments().get(0).translateToLua(tr);
@@ -220,15 +219,17 @@ public static LuaExpr translate(ImOperatorCall e, LuaTranslator tr) {
} else if (e.getOp() == WurstOperator.NOTEQ) {
return LuaAst.LuaExprUnary(LuaAst.LuaOpNot(), translateEquals(left, right, tr));
}
+ if (e.getOp() == WurstOperator.MOD_INT || e.getOp() == WurstOperator.MOD_REAL || e.getOp() == WurstOperator.DIV_INT) {
+ // LuaNativeLowering.lowerDivMod rewrites every DIV_INT/MOD_INT/MOD_REAL
+ // into a call against a portable IM function before the optimizer runs
+ // (so it can be inlined/constant-folded there). It should never survive
+ // to here - falling through to the default binary-op path below would
+ // silently use Lua's floored // and % instead of the truncating/
+ // Blizzard.j semantics Jass requires.
+ throw new Error("unexpected " + e.getOp() + " in Lua backend - should have been lowered by LuaNativeLowering.lowerDivMod");
+ }
LuaExpr leftExpr = left.translateToLua(tr);
LuaExpr rightExpr = right.translateToLua(tr);
- if (e.getOp() == WurstOperator.MOD_INT || e.getOp() == WurstOperator.MOD_REAL) {
- // Lua's % is floored; Wurst mod follows Blizzard.j's ModuloInteger/ModuloReal.
- return LuaAst.LuaExprFunctionCall(tr.modFunction, LuaAst.LuaExprlist(leftExpr, rightExpr));
- } else if (e.getOp() == WurstOperator.DIV_INT) {
- // Lua's // is floored; Jass integer division truncates toward zero.
- return LuaAst.LuaExprFunctionCall(tr.intDivFunction, LuaAst.LuaExprlist(leftExpr, rightExpr));
- }
LuaOpBinary op = e.getOp().luaTranslateBinary();
return LuaAst.LuaExprBinary(leftExpr, op, rightExpr);
} else if (e.getArguments().size() == 1) {
@@ -427,9 +428,18 @@ public static LuaExpr translate(ImVarAccess e, LuaTranslator tr) {
return LuaAst.LuaExprVarAccess(tr.luaVar.getFor(e.getVar()));
}
+ /**
+ * Primitive-typed array reads are wrapped in a type-normalizing helper
+ * call at the IM level, before the optimizer runs (see
+ * LuaNativeLowering#lowerPrimitiveArrayEnsure), by rewriting the read into
+ * a call against ImTranslator#ensureIntFunc and friends - so by the time
+ * an ImVarArrayAccess reaches this method, it is already either a
+ * genuine lvalue/raw access or an access whose type never needed
+ * wrapping (e.g. class/handle-typed arrays, which default to nil the
+ * same way an untouched Lua table key already does).
+ */
public static LuaExpr translate(ImVarArrayAccess e, LuaTranslator tr) {
- LuaExpr access = translateArrayAccessRaw(e, tr);
- return ensureByType(access, e.attrTyp(), tr);
+ return translateArrayAccessRaw(e, tr);
}
public static LuaExpr translateArrayAccessRaw(ImVarArrayAccess e, LuaTranslator tr) {
@@ -440,31 +450,6 @@ public static LuaExpr translateArrayAccessRaw(ImVarArrayAccess e, LuaTranslator
return LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(tr.luaVar.getFor(e.getVar())), indexes);
}
- /**
- * Wraps primitive-typed array reads in a type-normalizing helper call.
- * NOTE (perf): the defaultArray metatable already guarantees a typed,
- * non-nil default on every miss, so this is defensive hardening against
- * values written from outside typed Wurst code. It costs a function call
- * per array read; if that ever shows up in profiles, this is the place to
- * reconsider (a regression test asserts the current behavior:
- * LuaTranslationTests.stringArrayReadIsEnsured).
- */
- private static LuaExpr ensureByType(LuaExpr expr, ImType type, LuaTranslator tr) {
- if (TypesHelper.isStringType(type)) {
- return LuaAst.LuaExprFunctionCall(tr.ensureStrFunction, LuaAst.LuaExprlist(expr));
- }
- if (TypesHelper.isIntType(type)) {
- return LuaAst.LuaExprFunctionCall(tr.ensureIntFunction, LuaAst.LuaExprlist(expr));
- }
- if (TypesHelper.isBoolType(type)) {
- return LuaAst.LuaExprFunctionCall(tr.ensureBoolFunction, LuaAst.LuaExprlist(expr));
- }
- if (TypesHelper.isRealType(type)) {
- return LuaAst.LuaExprFunctionCall(tr.ensureRealFunction, LuaAst.LuaExprlist(expr));
- }
- return expr;
- }
-
public static LuaExpr translate(ImGetStackTrace e, LuaTranslator tr) {
// return LuaAst.LuaLiteral("debug.traceback()");
return LuaAst.LuaLiteral("\"$Stacktrace$\"");
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 157b72d41..f4e9ef9c1 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
@@ -138,6 +138,45 @@ public class LuaNatives {
f.getBody().add(LuaAst.LuaLiteral("return math.ceil(x)"));
});
+ addNative("__wurst_rawFloorDivInt", f -> {
+ f.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr()));
+ f.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return a // b"));
+ });
+
+ addNative("__wurst_rawFmodInt", f -> {
+ f.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr()));
+ f.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return math.fmod(a, b)"));
+ });
+
+ addNative("__wurst_rawFmodReal", f -> {
+ f.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr()));
+ f.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return math.fmod(a, b)"));
+ });
+
+ addNative(Arrays.asList("__wurst_rawToNumberInt", "__wurst_rawToNumberReal"), f -> {
+ f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return tonumber(x)"));
+ });
+
+ addNative("__wurst_rawToInteger", f -> {
+ f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return math.tointeger(x)"));
+ });
+
+ addNative("__wurst_rawToString", f -> {
+ f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return tostring(x)"));
+ });
+
+ addNative("__wurst_rawConcat", f -> {
+ f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
+ f.getParams().add(LuaAst.LuaVariable("y", LuaAst.LuaNoExpr()));
+ f.getBody().add(LuaAst.LuaLiteral("return x .. y"));
+ });
+
addNative("__wurst_GetEnumPlayer", f -> {
// Prefer the native enum player when inside an active native ForForce callback.
// This preserves Jass semantics for nested enumerations.
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java
index c1277c0a7..31520595b 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java
@@ -12,92 +12,6 @@ class LuaPolyfillSetup {
private LuaPolyfillSetup() {}
- static void createArrayInitFunction(LuaTranslator tr) {
- // Table-typed defaults (nested arrays, tuples) need a fresh value per
- // slot that is stored on first read, so slot identity is stable.
- // Immutable defaults (numbers, strings, booleans, nil) are returned
- // without storing: storing would permanently materialize an entry for
- // every slot that is merely READ, growing sparse arrays unboundedly.
- String[] code = {
- "local t = {}",
- "local dv = d()",
- "local mt",
- "if type(dv) == \"table\" then",
- " mt = {__index = function (table, key)",
- " local v = d()",
- " table[key] = v",
- " return v",
- " end}",
- "else",
- " mt = {__index = function (table, key)",
- " return dv",
- " end}",
- "end",
- "setmetatable(t, mt)",
- "return t"
- };
-
- tr.arrayInitFunction.getParams().add(LuaAst.LuaVariable("d", LuaAst.LuaNoExpr()));
- for (String c : code) {
- tr.arrayInitFunction.getBody().add(LuaAst.LuaLiteral(c));
- }
- tr.luaModel.add(tr.arrayInitFunction);
- }
-
- /**
- * Integer division and modulo helpers matching the Jass runtime:
- * div truncates toward zero (JASS integer division) and mod follows
- * Blizzard.j's ModuloInteger/ModuloReal (truncated remainder, plus
- * divisor when the remainder is negative). Lua's native {@code //} and
- * {@code %} are floored and disagree for negative operands.
- */
- static void createDivModFunctions(LuaTranslator tr) {
- tr.intDivFunction.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr()));
- tr.intDivFunction.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr()));
- String[] divCode = {
- "local q = a // b",
- "if q < 0 and q * b ~= a then",
- " q = q + 1",
- "end",
- "return q"
- };
- for (String c : divCode) {
- tr.intDivFunction.getBody().add(LuaAst.LuaLiteral(c));
- }
- tr.luaModel.add(tr.intDivFunction);
-
- tr.modFunction.getParams().add(LuaAst.LuaVariable("a", LuaAst.LuaNoExpr()));
- tr.modFunction.getParams().add(LuaAst.LuaVariable("b", LuaAst.LuaNoExpr()));
- String[] modCode = {
- "local r = math.fmod(a, b)",
- "if r < 0 then",
- " r = r + b",
- "end",
- "return r"
- };
- for (String c : modCode) {
- tr.modFunction.getBody().add(LuaAst.LuaLiteral(c));
- }
- tr.luaModel.add(tr.modFunction);
- }
-
- static void createStringConcatFunction(LuaTranslator tr) {
- String[] code = {
- "if x then",
- " if y then return x .. y else return x end",
- "else",
- " return y",
- "end"
- };
-
- tr.stringConcatFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
- tr.stringConcatFunction.getParams().add(LuaAst.LuaVariable("y", LuaAst.LuaNoExpr()));
- for (String c : code) {
- tr.stringConcatFunction.getBody().add(LuaAst.LuaLiteral(c));
- }
- tr.luaModel.add(tr.stringConcatFunction);
- }
-
static void createInstanceOfFunction(LuaTranslator tr) {
tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
tr.instanceOfFunction.getParams().add(LuaAst.LuaVariable("A", LuaAst.LuaNoExpr()));
@@ -237,29 +151,4 @@ static void createStringIndexFunctions(LuaTranslator tr) {
}
}
- static void createEnsureTypeFunctions(LuaTranslator tr) {
- tr.ensureIntFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
- tr.ensureIntFunction.getBody().add(LuaAst.LuaLiteral("local n = tonumber(x)"));
- tr.ensureIntFunction.getBody().add(LuaAst.LuaLiteral("if n == nil then return 0 end"));
- tr.ensureIntFunction.getBody().add(LuaAst.LuaLiteral("local i = math.tointeger(n)"));
- tr.ensureIntFunction.getBody().add(LuaAst.LuaLiteral("if i == nil then return 0 end"));
- tr.ensureIntFunction.getBody().add(LuaAst.LuaLiteral("return i"));
- tr.luaModel.add(tr.ensureIntFunction);
-
- tr.ensureBoolFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
- tr.ensureBoolFunction.getBody().add(LuaAst.LuaLiteral("if x == nil then return false end"));
- tr.ensureBoolFunction.getBody().add(LuaAst.LuaLiteral("return x"));
- tr.luaModel.add(tr.ensureBoolFunction);
-
- tr.ensureRealFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
- tr.ensureRealFunction.getBody().add(LuaAst.LuaLiteral("local n = tonumber(x)"));
- tr.ensureRealFunction.getBody().add(LuaAst.LuaLiteral("if n == nil then return 0.0 end"));
- tr.ensureRealFunction.getBody().add(LuaAst.LuaLiteral("return n"));
- tr.luaModel.add(tr.ensureRealFunction);
-
- tr.ensureStrFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr()));
- tr.ensureStrFunction.getBody().add(LuaAst.LuaLiteral("if x == nil then return \"\" end"));
- tr.ensureStrFunction.getBody().add(LuaAst.LuaLiteral("return tostring(x)"));
- tr.luaModel.add(tr.ensureStrFunction);
- }
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java
index 8541d51da..db51b68f7 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
@@ -82,6 +82,22 @@ private ImProg getProg() {
List tupleEqualsFuncs = new ArrayList<>();
List tupleCopyFuncs = new ArrayList<>();
+
+ // Array-default infrastructure (metatables/helper functions) shared across
+ // every array of a given entry type, instead of allocated per array
+ // instance - see newDefaultArray().
+ private final Map primitiveArrayMetatables = new HashMap<>();
+ private final List lazyArrayDefaults = new ArrayList<>();
+
+ private static final class LazyArrayDefault {
+ final ImType entryType;
+ final LuaVariable metatableVar;
+
+ LazyArrayDefault(ImType entryType, LuaVariable metatableVar) {
+ this.entryType = entryType;
+ this.metatableVar = metatableVar;
+ }
+ }
GetAForB luaVar = new GetAForB() {
@Override
public LuaVariable initFor(ImVar a) {
@@ -145,10 +161,6 @@ public LuaMethod initFor(ImClass a) {
}
};
- LuaFunction arrayInitFunction = LuaAst.LuaFunction(uniqueName("defaultArray"), LuaAst.LuaParams(), LuaAst.LuaStatements());
-
- LuaFunction stringConcatFunction = LuaAst.LuaFunction(uniqueName("stringConcat"), LuaAst.LuaParams(), LuaAst.LuaStatements());
-
LuaFunction toIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements());
LuaFunction fromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements());
@@ -157,14 +169,6 @@ public LuaMethod initFor(ImClass a) {
LuaFunction instanceOfFunction = LuaAst.LuaFunction(uniqueName("isInstanceOf"), LuaAst.LuaParams(), LuaAst.LuaStatements());
- LuaFunction intDivFunction = LuaAst.LuaFunction(uniqueName("intDiv"), LuaAst.LuaParams(), LuaAst.LuaStatements());
- LuaFunction modFunction = LuaAst.LuaFunction(uniqueName("wurstMod"), LuaAst.LuaParams(), LuaAst.LuaStatements());
-
- LuaFunction ensureIntFunction = LuaAst.LuaFunction(uniqueName("intEnsure"), LuaAst.LuaParams(), LuaAst.LuaStatements());
- LuaFunction ensureStrFunction = LuaAst.LuaFunction(uniqueName("stringEnsure"), LuaAst.LuaParams(), LuaAst.LuaStatements());
- LuaFunction ensureBoolFunction = LuaAst.LuaFunction(uniqueName("boolEnsure"), LuaAst.LuaParams(), LuaAst.LuaStatements());
- LuaFunction ensureRealFunction = LuaAst.LuaFunction(uniqueName("realEnsure"), LuaAst.LuaParams(), LuaAst.LuaStatements());
-
private final Lazy errorFunc = Lazy.create(() ->
this.getProg().getFunctions().stream()
.flatMap(f -> {
@@ -216,13 +220,9 @@ public LuaCompilationUnit translate() {
// NormalizeNames.normalizeNames(prog);
- createArrayInitFunction();
- createDivModFunctions();
- createStringConcatFunction();
createInstanceOfFunction();
createObjectIndexFunctions();
createStringIndexFunctions();
- createEnsureTypeFunctions();
for (ImVar v : prog.getGlobals()) {
translateGlobal(v);
@@ -409,14 +409,6 @@ private void collectMethodNames(ImClass c, Set methodNames, Set
}
}
- private void createDivModFunctions() {
- LuaPolyfillSetup.createDivModFunctions(this);
- }
-
- private void createStringConcatFunction() {
- LuaPolyfillSetup.createStringConcatFunction(this);
- }
-
private void createInstanceOfFunction() {
LuaPolyfillSetup.createInstanceOfFunction(this);
}
@@ -429,14 +421,6 @@ private void createStringIndexFunctions() {
LuaPolyfillSetup.createStringIndexFunctions(this);
}
- private void createArrayInitFunction() {
- LuaPolyfillSetup.createArrayInitFunction(this);
- }
-
- private void createEnsureTypeFunctions() {
- LuaPolyfillSetup.createEnsureTypeFunctions(this);
- }
-
private void cleanStatements() {
luaModel.accept(new LuaModel.DefaultVisitor() {
@Override
@@ -1208,14 +1192,7 @@ public LuaExpr case_ImArrayTypeMulti(ImArrayTypeMulti at) {
arraySizes.remove(0);
baseType = JassIm.ImArrayTypeMulti(at.getEntryType(), arraySizes);
}
- return LuaAst.LuaExprFunctionCall(arrayInitFunction,
- LuaAst.LuaExprlist(
- LuaAst.LuaExprFunctionAbstraction(LuaAst.LuaParams(),
- LuaAst.LuaStatements(
- LuaAst.LuaReturn(defaultValue(baseType))
- )
- )
- ));
+ return newDefaultArray(baseType);
}
@Override
@@ -1235,14 +1212,7 @@ public LuaExpr case_ImSimpleType(ImSimpleType st) {
@Override
public LuaExpr case_ImArrayType(ImArrayType imArrayType) {
ImType baseType = imArrayType.getEntryType();
- return LuaAst.LuaExprFunctionCall(arrayInitFunction,
- LuaAst.LuaExprlist(
- LuaAst.LuaExprFunctionAbstraction(LuaAst.LuaParams(),
- LuaAst.LuaStatements(
- LuaAst.LuaReturn(defaultValue(baseType))
- )
- )
- ));
+ return newDefaultArray(baseType);
}
@Override
@@ -1252,6 +1222,123 @@ public LuaExpr case_ImTypeVarRef(ImTypeVarRef imTypeVarRef) {
});
}
+ /**
+ * Builds a fresh array table whose reads of never-written slots yield the
+ * default value for {@code entryType}, matching Jass's fully-initialized
+ * arrays. The metatable/helper-function infrastructure that makes this
+ * work is shared across every array with the same entry type - only the
+ * per-array table itself (and, for table-typed defaults, each lazily
+ * materialized slot) is allocated per array instance. Mirrors the shared
+ * per-class instance metatable in translateClass().
+ */
+ private LuaExpr newDefaultArray(ImType entryType) {
+ if (isAlwaysNilDefault(entryType)) {
+ // An untouched Lua table key already reads as nil - no metatable needed.
+ return LuaAst.LuaTableConstructor(LuaAst.LuaTableFields());
+ }
+ if (entryType instanceof ImSimpleType) {
+ return setmetatableCall(getOrCreatePrimitiveArrayMetatable((ImSimpleType) entryType));
+ }
+ // Table-typed default (tuple / nested array): each slot needs its own,
+ // separately mutable default value, materialized lazily on first read.
+ return setmetatableCall(getOrCreateLazyArrayMetatable(entryType));
+ }
+
+ private LuaExpr setmetatableCall(LuaVariable metatableVar) {
+ return LuaAst.LuaExprFunctionCallByName("setmetatable", LuaAst.LuaExprlist(
+ LuaAst.LuaTableConstructor(LuaAst.LuaTableFields()),
+ LuaAst.LuaExprVarAccess(metatableVar)
+ ));
+ }
+
+ /** True for entry types whose Wurst default value is nil / not yet allocated. */
+ private boolean isAlwaysNilDefault(ImType t) {
+ if (t instanceof ImClassType || t instanceof ImAnyType || t instanceof ImTypeVarRef || t instanceof ImVoid) {
+ return true;
+ }
+ if (t instanceof ImSimpleType) {
+ // WC3 handle types (unit, player, timer, ...) are ImSimpleType too,
+ // and default to nil just like user classes - only the four true
+ // primitives below have a non-nil default.
+ ImSimpleType st = (ImSimpleType) t;
+ return !TypesHelper.isIntType(st) && !TypesHelper.isBoolType(st)
+ && !TypesHelper.isRealType(st) && !TypesHelper.isStringType(st);
+ }
+ return false;
+ }
+
+ /**
+ * One shared metatable per primitive kind (int/real/bool/string), for the
+ * whole program. The default is immutable, so reads never need to store
+ * anything back into the array table.
+ */
+ private LuaVariable getOrCreatePrimitiveArrayMetatable(ImSimpleType st) {
+ String key = TypesHelper.isIntType(st) ? "integer"
+ : TypesHelper.isBoolType(st) ? "boolean"
+ : TypesHelper.isRealType(st) ? "real"
+ : TypesHelper.isStringType(st) ? "string"
+ : null;
+ if (key == null) {
+ // Defensive fallback; isAlwaysNilDefault() should have routed anything
+ // else away from this method.
+ return getOrCreateLazyArrayMetatable(st);
+ }
+ LuaVariable existing = primitiveArrayMetatables.get(key);
+ if (existing != null) {
+ return existing;
+ }
+ LuaFunction indexFn = LuaAst.LuaFunction(uniqueName("__wurst_arrIndex_" + key),
+ LuaAst.LuaParams(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()), LuaAst.LuaVariable("k", LuaAst.LuaNoExpr())),
+ LuaAst.LuaStatements(LuaAst.LuaReturn(defaultValue(st))));
+ luaModel.add(indexFn);
+
+ LuaVariable mt = LuaAst.LuaVariable(uniqueName("__wurst_arrMt_" + key), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
+ LuaAst.LuaTableNamedField("__index", LuaAst.LuaExprFuncRef(indexFn))
+ )));
+ luaModel.add(mt);
+ primitiveArrayMetatables.put(key, mt);
+ return mt;
+ }
+
+ /**
+ * One shared metatable (and default-value factory) per distinct entry
+ * type that needs a fresh, independently mutable default per slot
+ * (tuples, nested arrays). Memoized like {@link ExprTranslation#getTupleCopyFunc}.
+ */
+ private LuaVariable getOrCreateLazyArrayMetatable(ImType entryType) {
+ for (LazyArrayDefault info : lazyArrayDefaults) {
+ if (info.entryType.equalsType(entryType)) {
+ return info.metatableVar;
+ }
+ }
+ LuaVariable mt = LuaAst.LuaVariable(uniqueName("__wurst_arrMt"), LuaAst.LuaNoExpr());
+ // Register before building the (possibly recursive, e.g. nested arrays) body.
+ lazyArrayDefaults.add(new LazyArrayDefault(entryType, mt));
+
+ LuaFunction thunk = LuaAst.LuaFunction(uniqueName("__wurst_arrDefault"), LuaAst.LuaParams(), LuaAst.LuaStatements());
+ thunk.getBody().add(LuaAst.LuaReturn(defaultValue(entryType)));
+ luaModel.add(thunk);
+
+ LuaVariable tParam = LuaAst.LuaVariable("t", LuaAst.LuaNoExpr());
+ LuaVariable kParam = LuaAst.LuaVariable("k", LuaAst.LuaNoExpr());
+ LuaVariable vLocal = LuaAst.LuaVariable("v", LuaAst.LuaExprFunctionCall(thunk, LuaAst.LuaExprlist()));
+ LuaFunction indexFn = LuaAst.LuaFunction(uniqueName("__wurst_arrIndex"), LuaAst.LuaParams(tParam, kParam),
+ LuaAst.LuaStatements(
+ vLocal,
+ LuaAst.LuaAssignment(
+ LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(tParam), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(kParam))),
+ LuaAst.LuaExprVarAccess(vLocal)),
+ LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(vLocal))
+ ));
+ luaModel.add(indexFn);
+
+ mt.setInitialValue(LuaAst.LuaTableConstructor(LuaAst.LuaTableFields(
+ LuaAst.LuaTableNamedField("__index", LuaAst.LuaExprFuncRef(indexFn))
+ )));
+ luaModel.add(mt);
+ return mt;
+ }
+
public LuaExprOpt translateOptional(ImExprOpt e) {
if (e instanceof ImExpr) {
ImExpr imExpr = (ImExpr) e;
diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java
index ed9e379e9..08739ff28 100644
--- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java
+++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java
@@ -7,6 +7,7 @@
import java.io.File;
import java.io.IOException;
+import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
@@ -184,29 +185,166 @@ public void integerDivModReferenceSemanticsInInterpreter() {
test().executeProg().lines(DIV_MOD_PROG);
}
+ /**
+ * EliminateLocalTypes#transformProgram runs after LuaEnsureFunctions
+ * builds __wurst_ensureStr's/__wurst_stringConcat's bodies, and
+ * unconditionally rewrites every string-typed ImNull node in the whole
+ * program - including inside those functions themselves - into
+ * ImStringVal(""). If their own "x ~= nil" checks were tagged with the
+ * string type, that rewrite would silently turn them into "x ~= \"\"",
+ * so a genuinely nil Lua value (e.g. an unset bound-generic string
+ * field) would read as "not nil", skip normalization, and come out as
+ * the literal string "nil" via tostring() instead of "" - or, for
+ * stringConcat, get passed straight into raw ".." concatenation.
+ * LuaEnsureFunctions#notNull tags its ImNull sentinel with ImAnyType
+ * specifically to stay exempt from that rewrite.
+ */
+ @Test
+ public void ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "string array names",
+ "function join(string a, string b) returns string",
+ " return a + b",
+ "init",
+ " if names[5] == \"\" and join(\"a\", \"b\") == \"ab\"",
+ " testSuccess()"
+ );
+ String compiled = compiledLua("ensureStrAndStringConcatNilChecksSurviveEliminateLocalTypes");
+ assertNilCheckNotCorruptedToEmptyStringCheck(compiled, "__wurst_ensureStr(");
+ assertNilCheckNotCorruptedToEmptyStringCheck(compiled, "__wurst_stringConcat(");
+ }
+
+ private void assertNilCheckNotCorruptedToEmptyStringCheck(String compiled, String functionNamePrefix) {
+ int fnStart = compiled.indexOf("function " + functionNamePrefix);
+ assertTrue("expected " + functionNamePrefix + " to be present", fnStart >= 0);
+ int fnEnd = compiled.indexOf("\nend", fnStart);
+ String fnBody = compiled.substring(fnStart, fnEnd);
+ assertTrue(functionNamePrefix + " must check for real nil", fnBody.contains("== nil"));
+ assertFalse(functionNamePrefix + "'s nil check must not be corrupted into an empty-string check",
+ fnBody.contains("== \"\""));
+ }
+
/**
* The Lua backend used to emit floored {@code //} for div and
* {@code math.floor(a % b)} for mod, which disagree with the Jass
* backend and the interpreter for negative operands
* (e.g. -7 div 2 was -4 instead of -3, and 7 mod -2 was -1 instead of 1).
+ *
+ * Div/mod are now lowered to portable IM functions before the optimizer
+ * runs (see LuaNativeLowering#lowerDivMod), so calls with constant
+ * arguments - like the ones below - may get inlined away entirely rather
+ * than showing up as a helper call in the output. The floor-div/fmod
+ * *native* they delegate to (Wurst has no such IM operator) always
+ * survives somewhere in the output, inlined or not, so checking for it
+ * is robust regardless of the inliner's decision.
*/
@Test
public void integerDivModMatchJassSemanticsInLua() throws IOException {
test().testLua(true).executeProg().lines(DIV_MOD_PROG);
String compiled = compiledLua("integerDivModMatchJassSemanticsInLua");
- assertTrue("div must go through the truncating intDiv helper",
- compiled.contains("return intDiv("));
- assertTrue("mod must go through the ModuloInteger-compatible wurstMod helper",
- compiled.contains("return wurstMod("));
- assertFalse("mod must not use math.floor over Lua's floored %",
+ assertTrue("div must go through the truncating floor-div correction",
+ compiled.contains("__wurst_rawFloorDivInt("));
+ assertTrue("mod must go through the ModuloInteger-compatible fmod correction",
+ compiled.contains("__wurst_rawFmodInt("));
+ assertFalse("mod/div must not use math.floor directly",
compiled.contains("math.floor"));
}
/**
- * String concatenation is lowered to a synthetic stringConcat IM function;
- * the Lua polyfill and the call sites used to be linked only by both
- * happening to print the same name. This guards that a user function named
- * stringConcat neither breaks concatenation nor gets hijacked.
+ * ErrorHandling.error()'s deliberate {@code I2S(1 div 0)} crash trap
+ * relies on {@code lua.translation.ExprTranslation#isIntentionalThreadAbortCall}
+ * pattern-matching the exact {@code ImOperatorCall(DIV_INT, [1, 0])} shape
+ * as I2S's sole argument, to turn it into the {@code __wurst_abort_thread}
+ * sentinel every callback xpcall handler ignores. The div/mod lowering
+ * (LuaNativeLowering#lowerDivMod) runs before that check and rewrites
+ * every DIV_INT it sees into a __wurst_intDiv(...) call - which would
+ * destroy that exact shape and replace the sentinel with a real Lua
+ * {@code n//0} runtime error, breaking every callback error handler's
+ * "was this an intentional abort" check. This guards that the lowering
+ * carves out an exception for precisely this pattern.
+ */
+ @Test
+ public void i2sDivByZeroAbortTrapSurvivesDivModLowering() throws IOException {
+ test().testLua(true).lines(
+ "package Test",
+ "native testSuccess()",
+ "native I2S(int i) returns string",
+ "native print(string s)",
+ "function crashTrap() returns string",
+ " return I2S(1 div 0)",
+ "init",
+ " print(crashTrap())",
+ " testSuccess()"
+ );
+ String compiled = compiledLua("i2sDivByZeroAbortTrapSurvivesDivModLowering");
+ assertTrue("the abort trap must still produce the sentinel error call",
+ compiled.contains("error(\"__wurst_abort_thread\", 0)"));
+ assertFalse("the abort trap's 1 div 0 must not be lowered to a helper call",
+ compiled.contains("__wurst_intDiv(1, 0)"));
+ }
+
+ /**
+ * Div/mod helpers are real IM functions now, created only when the
+ * program actually uses div/mod, instead of unconditionally-emitted Lua
+ * source (as before) - so a program that never divides/mods must not
+ * carry any of this machinery in its output.
+ */
+ @Test
+ public void divModHelpersAreOmittedWhenUnused() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "init",
+ " if 1 + 1 == 2",
+ " testSuccess()"
+ );
+ String compiled = compiledLua("divModHelpersAreOmittedWhenUnused");
+ assertFalse("unused div helper must not be emitted",
+ compiled.contains("__wurst_intDiv"));
+ assertFalse("unused mod helpers must not be emitted",
+ compiled.contains("__wurst_mod"));
+ assertFalse("unused raw floor-div/fmod natives must not be emitted",
+ compiled.contains("__wurst_rawF"));
+ }
+
+ /**
+ * A non-constant div/mod call (parameters, not literals) has no
+ * constant-folding upside, so the inliner leaves it as a call to the
+ * shared helper - proving div/mod is optimizable at all now (it used to
+ * be opaque, always-emitted Lua source the IM optimizer never saw).
+ */
+ @Test
+ public void nonConstantDivModCallsUseSharedHelper() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "function d(int a, int b) returns int",
+ " return a div b",
+ "function m(int a, int b) returns int",
+ " return a mod b",
+ "init",
+ " int x = 7",
+ " int y = -2",
+ " if d(x, y) == -3 and m(x, y) == 1 and d(y, x) == 0 and m(y, x) == 5",
+ " testSuccess()"
+ );
+ String compiled = compiledLua("nonConstantDivModCallsUseSharedHelper");
+ assertEquals("expected exactly one shared int-div helper definition",
+ 1, countOccurrences(compiled, "function __wurst_intDiv("));
+ assertEquals("expected exactly one shared int-mod helper definition",
+ 1, countOccurrences(compiled, "function __wurst_modInt("));
+ }
+
+ /**
+ * String concatenation is lowered to a synthetic stringConcat IM function.
+ * The polyfill and its call sites used to be linked only by both happening
+ * to print the same Lua name "stringConcat" - a user function also named
+ * stringConcat could hijack it, or get renamed away by the collision. The
+ * polyfill now has its own permanently distinct internal name
+ * (__wurst_stringConcat, see LuaEnsureFunctions), so a user-defined
+ * stringConcat no longer collides with it at all and needs no renaming.
*/
@Test
public void userFunctionNamedStringConcatDoesNotBreakConcatenation() throws IOException {
@@ -222,8 +360,10 @@ public void userFunctionNamedStringConcatDoesNotBreakConcatenation() throws IOEx
" testSuccess()"
);
String compiled = compiledLua("userFunctionNamedStringConcatDoesNotBreakConcatenation");
- assertTrue("user stringConcat must be renamed away from the polyfill",
- compiled.contains("function stringConcat1("));
+ assertTrue("user stringConcat no longer collides with the internal polyfill and keeps its own name",
+ compiled.contains("function stringConcat("));
+ assertFalse("the internal polyfill must not leak the bare 'stringConcat' name",
+ compiled.contains("function __wurst_stringConcat1("));
}
/**
@@ -284,11 +424,12 @@ public void classInstancesShareOneMetatablePerClass() throws IOException {
}
/**
- * The defaultArray metatable used to store the default value on every
- * read miss, so merely probing a sparse array permanently materialized an
- * entry per probed key. Immutable defaults are now returned without
- * storing; only table-typed defaults (tuples, nested arrays) keep the
- * store-on-first-read behavior for slot identity.
+ * Reading a never-written array slot must not permanently store an entry
+ * for it - merely probing a sparse array would otherwise grow it
+ * unboundedly. Immutable (primitive) defaults are served by a dedicated
+ * index function that never writes back into the array table; only
+ * table-typed defaults (tuples, nested arrays) store on first read,
+ * because each slot needs its own, independently mutable default value.
*/
@Test
public void primitiveArrayReadsDoNotMaterializeEntries() throws IOException {
@@ -306,8 +447,78 @@ public void primitiveArrayReadsDoNotMaterializeEntries() throws IOException {
" testSuccess()"
);
String compiled = compiledLua("primitiveArrayReadsDoNotMaterializeEntries");
- assertTrue("defaultArray must branch on the default value's type",
- compiled.contains("local dv = d()"));
+ int fnStart = compiled.indexOf("function __wurst_arrIndex_integer(");
+ assertTrue("expected a dedicated index function for int array defaults", fnStart >= 0);
+ int fnEnd = compiled.indexOf("\nend", fnStart);
+ assertTrue("could not find end of int array index function", fnEnd >= 0);
+ assertFalse("primitive array default reads must not write back into the array table",
+ compiled.substring(fnStart, fnEnd).contains("="));
+
+ assertTrue("tuple array defaults must still be lazily materialized per-slot for identity",
+ compiled.contains("function __wurst_arrIndex("));
+ }
+
+ /**
+ * Array-default infrastructure (metatable + index function) is shared
+ * across every array with the same entry type, not allocated fresh per
+ * array instance - mirrors the shared per-class instance metatable
+ * ({@link #classInstancesShareOneMetatablePerClass}).
+ */
+ @Test
+ public void arraysOfSameEntryTypeShareDefaultMetatable() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "int array a",
+ "int array b",
+ "int array c",
+ "init",
+ " a[1] = 1",
+ " b[1] = 2",
+ " c[1] = 3",
+ " if a[1] + b[1] + c[1] == 6 and a[9] == 0 and b[9] == 0",
+ " testSuccess()"
+ );
+ String compiled = compiledLua("arraysOfSameEntryTypeShareDefaultMetatable");
+ assertEquals("expected exactly one shared metatable declaration for all int arrays",
+ 1, countOccurrences(compiled, "__wurst_arrMt_integer = "));
+ assertEquals("expected exactly one shared index function for all int arrays",
+ 1, countOccurrences(compiled, "function __wurst_arrIndex_integer("));
+ assertEquals("expected one setmetatable call per array instance",
+ 3, countOccurrences(compiled, "setmetatable(({}), __wurst_arrMt_integer)"));
+ }
+
+ /**
+ * Handle-typed arrays (WC3 native types, not user classes) default to
+ * nil, same as an untouched Lua table key - they must not pay for any
+ * metatable machinery at all.
+ */
+ @Test
+ public void handleTypedArraysAllocateNoMetatable() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "nativetype customHandle",
+ "customHandle array hs",
+ "init",
+ " if hs[3] == null",
+ " testSuccess()"
+ );
+ String compiled = compiledLua("handleTypedArraysAllocateNoMetatable");
+ assertFalse("a nil-default array must not allocate any array-default metatable",
+ compiled.contains("__wurst_arrMt"));
+ assertFalse("a nil-default array must not allocate any array-default index function",
+ compiled.contains("__wurst_arrIndex"));
+ }
+
+ private static int countOccurrences(String haystack, String needle) {
+ int count = 0;
+ int idx = 0;
+ while ((idx = haystack.indexOf(needle, idx)) >= 0) {
+ count++;
+ idx += needle.length();
+ }
+ return count;
}
/**
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 37cf36ad4..86924dc43 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
@@ -467,7 +467,7 @@ public void stringArrayReadIsEnsured() throws IOException {
" SetPlayerName(Player(i), playerName[i])"
);
String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsEnsured.lua"), Charsets.UTF_8);
- assertContainsRegex(compiled, "SetPlayerName\\(Player\\([^\\)]*\\),\\s*stringEnsure\\(");
+ assertContainsRegex(compiled, "SetPlayerName\\(Player\\([^\\)]*\\),\\s*__wurst_ensureStr\\(");
}
@Test
@@ -1346,7 +1346,7 @@ public void newGenericsStringFieldAssignmentRoundTripsInLua() throws IOException
);
String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsStringFieldAssignmentRoundTripsInLua.lua"), Charsets.UTF_8);
assertFunctionBodyContains(compiled, "testGenericStringField", "c.C_x = \"42\"", true);
- assertFunctionBodyContains(compiled, "testGenericStringField", "stringEnsure(c.C_x)", true);
+ assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_ensureStr(c.C_x)", true);
assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringToIndex", false);
assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringFromIndex", false);
}
@@ -2169,6 +2169,9 @@ public void i2sAbortSentinelNotBrokenByEarlierI2SCallInLua() throws IOException
// The abort must be the sentinel, not a raw //0 that Lua would crash on at runtime
assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*//\\s*0\\s*\\)");
assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*/\\s*0\\s*\\)");
+ // Nor lowered to a runtime-crashing div helper call (LuaNativeLowering#lowerDivMod
+ // must carve out an exception for this exact I2S(1 div 0) shape)
+ assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*__wurst_intDiv\\s*\\(\\s*1\\s*,\\s*0\\s*\\)\\s*\\)");
assertTrue(compiled.contains("__wurst_abort_thread"));
// tostring() must still be used for the normal I2S call
assertContainsRegex(compiled, "tostring\\s*\\(");