diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 37551d3da..e2b7e9243 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -90,6 +90,7 @@ public CompiletimeFunctionRunner( this.translator = tr; this.imProg = imProg; globalState = new ProgramStateIO(mapFile, mpqEditor, gui, imProg, true); + initializeBackendConstants(); this.interpreter = new ILInterpreter(imProg, gui, mapFile, globalState); interpreter.addNativeProvider(new CompiletimeNatives(globalState, projectConfigData, isProd)); @@ -98,6 +99,14 @@ public CompiletimeFunctionRunner( this.functionFlag = flag; } + private void initializeBackendConstants() { + for (ImVar global : imProg.getGlobals()) { + if (global.getName().equals("MagicFunctions_isLua")) { + globalState.setValUntracked(global, ILconstBool.instance(translator.isLuaTarget())); + return; + } + } + } public void run() { try { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 28ede72fb..3b3b54844 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -136,7 +136,7 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l if (r == null) { List initExpr = globalState.getProg().getGlobalInits().get(var); if (initExpr != null) { - r = initExpr.get(0).getRight().evaluate(globalState, localState); + r = globalState.evaluateUntracked(initExpr.get(0).getRight(), localState); } else { throw new InterpreterException(globalState, "Variable " + var.getName() + " is not initialized."); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java index ba3a5000a..0772d8224 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java @@ -48,6 +48,7 @@ public class ProgramState extends State implements AutoCloseable { private final Map> genericArrayTypeArguments = new HashMap<>(); private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); + private int untrackedWriteDepth; private static boolean containsTypeVariable(ImType type) { return type.match(new ImType.Matcher() { @@ -684,13 +685,18 @@ private static String vid(ImVar v) { @Override public void setVal(ImVar v, ILconst val) { - modifiedScalars.add(v); + boolean trackWrite = untrackedWriteDepth == 0; + if (trackWrite) { + modifiedScalars.add(v); + } String key = genericStaticKey(v); if (key != null) { WLogger.trace(() -> "[GENSTATIC] set " + key + " = " + val); genericStaticScalarVals.put(key, val); - modifiedGenericScalars.add(key); - genericScalarTypeArguments.computeIfAbsent(key, ignored -> genericStaticTypeArguments(v)); + if (trackWrite) { + modifiedGenericScalars.add(key); + genericScalarTypeArguments.computeIfAbsent(key, ignored -> genericStaticTypeArguments(v)); + } return; } super.setVal(v, val); @@ -719,7 +725,7 @@ public void setValUntracked(ImVar v, ILconst val) { // lazy init from global inits (e.g. foo = 1) List inits = prog.getGlobalInits().get(v); if (inits != null && !inits.isEmpty()) { - ILconst initVal = inits.get(inits.size() - 1).getRight().evaluate(this, EMPTY_LOCAL_STATE); + ILconst initVal = evaluateUntracked(inits.get(inits.size() - 1).getRight(), EMPTY_LOCAL_STATE); genericStaticScalarVals.put(key, initVal); WLogger.trace(() -> "[GENSTATIC] get " + key + " -> (init) " + initVal); return initVal; @@ -733,6 +739,21 @@ public void setValUntracked(ImVar v, ILconst val) { return super.getVal(v); } + /** + * Evaluates a lazy global initializer without recording its side effects for state migration. + * Runtime repeats initializer execution, so replaying those writes would duplicate them. + * Compiletime-only side effects hidden inside an initializer are intentionally unsupported; + * persistent mutations must be performed by an explicit compiletime function instead. + */ + public ILconst evaluateUntracked(ImExpr expr, LocalState localState) { + untrackedWriteDepth++; + try { + return expr.evaluate(this, localState); + } finally { + untrackedWriteDepth--; + } + } + public boolean isCompiletime() { return isCompiletime; @@ -755,7 +776,7 @@ protected ILconstArray getArray(ImVar v) { if (inits != null && !inits.isEmpty()) { final LocalState ls = EMPTY_LOCAL_STATE; for (int i = 0; i < inits.size(); i++) { - ILconst val = inits.get(i).getRight().evaluate(this, ls); + ILconst val = evaluateUntracked(inits.get(i).getRight(), ls); r.set(i, val); } } @@ -774,7 +795,7 @@ protected ILconstArray getArray(ImVar v) { if (inits != null && !inits.isEmpty()) { final LocalState ls = EMPTY_LOCAL_STATE; for (int i = 0; i < inits.size(); i++) { - ILconst val = inits.get(i).getRight().evaluate(this, ls); + ILconst val = evaluateUntracked(inits.get(i).getRight(), ls); r.set(i, val); } } @@ -783,6 +804,10 @@ protected ILconstArray getArray(ImVar v) { @Override public void setArrayVal(ImVar v, List indexes, ILconst val) { + if (untrackedWriteDepth > 0) { + setArrayValUntracked(v, indexes, val); + return; + } String key = genericStaticKey(v); super.setArrayVal(v, indexes, val); if (key != null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java index 3f3a9aaa9..002cdaf14 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java @@ -85,6 +85,10 @@ static ILconstArray createArrayConstantFromType(ImType vType) { public void setArrayVal(ImVar v, List indexes, ILconst val) { modifiedArrayIndexes.computeIfAbsent(v, ignored -> new HashSet<>()) .add(Collections.unmodifiableList(new ArrayList<>(indexes))); + setArrayValUntracked(v, indexes, val); + } + + protected void setArrayValUntracked(ImVar v, List indexes, ILconst val) { ILconstArray ar = getArray(v); for (int i = 0; i < indexes.size() - 1; i++) { ar = (ILconstArray) ar.get(indexes.get(i)); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index 3571a06f7..dfba3b6e8 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java @@ -155,6 +155,25 @@ public void testCompiletimeScalarReplayOnlyWrittenValues() { " testSuccess()"); } + @Test + public void testLazyScalarInitializerSideEffectsAreNotReplayed() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "int counter = 0", + "function bump() returns int", + " counter++", + " return counter", + "int observed = bump()", + "int migrated", + "@compiletime function fill()", + " let _snapshot = observed", + " migrated = 42", + "init", + " if counter == 1 and observed == 1 and migrated == 42", + " testSuccess()"); + } + @Test public void testCompiletimeScalarRuntimeWriteRemainsAuthoritative() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) 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 96ec963c3..adb2fbdce 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 @@ -139,6 +139,34 @@ public void compiletimeScalarReplaySplittingIsDeterministicAcrossPackages() { assertEquals("all compiletime scalar values must still be emitted", 4, persistedAssignments); } + @Test + public void compiletimeInterpreterSeesLuaTarget() { + String compiled = compileLuaWithRunArgs( + "compiletimeInterpreterSeesLuaTarget", + new RunArgs().with("-lua", "-runcompiletimefunctions"), + "package MagicFunctions", + "public constant isLua = false", + "endpackage", + "package Test", + "import MagicFunctions", + "int observedBackend", + "@compiletime function detectBackend()", + " if isLua", + " observedBackend = 1", + " else", + " observedBackend = 2", + "native testSuccess()", + "init", + " if observedBackend == 1", + " testSuccess()" + ); + + assertTrue("compiletime execution must take the Lua branch:\n" + compiled, + compiled.contains("Test_observedBackend = 1")); + assertFalse("compiletime execution must not persist the Jass branch:\n" + compiled, + compiled.contains("Test_observedBackend = 2")); + } + @Test public void localPlayerEffectfulBooleanOperandSurvivesOptimization() { String compiled = compileOptimizedLua(