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*\\(");