From db672b61f3c6b4019a751122ef3f110c85be0e0b Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 18:45:20 +0200 Subject: [PATCH 01/13] Propagate Lua target to compiletime interpreter --- .../wurstio/CompiletimeFunctionRunner.java | 9 ++++++ .../tests/LuaBackendAuditTests.java | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+) 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/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( From a47851f19631799344c969469229780ca1269fb0 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 20:14:57 +0200 Subject: [PATCH 02/13] Avoid replaying lazy initializer side effects --- .../interpreter/EvaluateExpr.java | 2 +- .../interpreter/ProgramState.java | 31 +++++++++++++++---- .../intermediatelang/interpreter/State.java | 4 +++ .../wurstscript/tests/CompiletimeTests.java | 19 ++++++++++++ 4 files changed, 49 insertions(+), 7 deletions(-) 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..b84e1312f 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,15 @@ public void setValUntracked(ImVar v, ILconst val) { return super.getVal(v); } + public ILconst evaluateUntracked(ImExpr expr, LocalState localState) { + untrackedWriteDepth++; + try { + return expr.evaluate(this, localState); + } finally { + untrackedWriteDepth--; + } + } + public boolean isCompiletime() { return isCompiletime; @@ -755,7 +770,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 +789,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 +798,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) From 293d5d7e7434cb2cb9d310220dc6d496d8ee8fcb Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 22:06:30 +0200 Subject: [PATCH 03/13] Preserve compiletime-only initializer writes --- .../interpreter/EvaluateExpr.java | 1 + .../interpreter/ProgramState.java | 33 +++++++++++++++++-- .../interpreter/RunStatement.java | 11 +++++-- .../wurstscript/tests/CompiletimeTests.java | 33 +++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) 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 3b3b54844..ad33e3570 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 @@ -130,6 +130,7 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l ImVar var = e.getVar(); if (var.isGlobal()) { if (isMagicCompiletimeConstant(var)) { + globalState.markCompiletimeConstantRead(); return ILconstBool.instance(globalState.isCompiletime()); } ILconst r = globalState.getVal(var); 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 b84e1312f..75c023caa 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 @@ -49,6 +49,8 @@ public class ProgramState extends State implements AutoCloseable { private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); private int untrackedWriteDepth; + private int trackedWriteDepth; + private long compiletimeConstantReadVersion; private static boolean containsTypeVariable(ImType type) { return type.match(new ImType.Matcher() { @@ -685,7 +687,7 @@ private static String vid(ImVar v) { @Override public void setVal(ImVar v, ILconst val) { - boolean trackWrite = untrackedWriteDepth == 0; + boolean trackWrite = writesAreTracked(); if (trackWrite) { modifiedScalars.add(v); } @@ -748,6 +750,33 @@ public ILconst evaluateUntracked(ImExpr expr, LocalState localState) { } } + void markCompiletimeConstantRead() { + compiletimeConstantReadVersion++; + } + + long getCompiletimeConstantReadVersion() { + return compiletimeConstantReadVersion; + } + + void runWithTrackedWrites(Runnable action) { + trackedWriteDepth++; + try { + action.run(); + } finally { + trackedWriteDepth--; + } + } + + private boolean writesAreTracked() { + // A compiletime-dependent branch cancels one enclosing lazy-initializer suppression scope. + // A nested lazy initializer therefore becomes untracked again until its own such branch. + return untrackedWriteDepth == 0 || trackedWriteDepth >= untrackedWriteDepth; + } + + boolean writesAreSuppressed() { + return !writesAreTracked(); + } + public boolean isCompiletime() { return isCompiletime; @@ -798,7 +827,7 @@ protected ILconstArray getArray(ImVar v) { @Override public void setArrayVal(ImVar v, List indexes, ILconst val) { - if (untrackedWriteDepth > 0) { + if (!writesAreTracked()) { setArrayValUntracked(v, indexes, val); return; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index cce66cc9a..56ffa5375 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -23,11 +23,16 @@ public static void run(ImExitwhen s, ProgramState globalState, LocalState localS } public static void run(ImIf s, ProgramState globalState, LocalState localState) { + long compiletimeReadsBefore = globalState.getCompiletimeConstantReadVersion(); ILconstBool c = (ILconstBool) s.getCondition().evaluate(globalState, localState); - if (c.getVal()) { - s.getThenBlock().runStatements(globalState, localState); + ImStmts selectedBlock = c.getVal() ? s.getThenBlock() : s.getElseBlock(); + // Runtime repeats ordinary lazy-initializer branches, but not a branch selected using + // MagicFunctions.compiletime. Preserve writes from the latter for state migration. + if (globalState.writesAreSuppressed() + && globalState.getCompiletimeConstantReadVersion() != compiletimeReadsBefore) { + globalState.runWithTrackedWrites(() -> selectedBlock.runStatements(globalState, localState)); } else { - s.getElseBlock().runStatements(globalState, localState); + selectedBlock.runStatements(globalState, localState); } } 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 dfba3b6e8..bb93b1a01 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 @@ -174,6 +174,39 @@ public void testLazyScalarInitializerSideEffectsAreNotReplayed() { " testSuccess()"); } + @Test + public void testCompiletimeOnlyLazyInitializerSideEffectsAreReplayed() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package MagicFunctions", + "public constant compiletime = false", + "endpackage", + "package Test", + "import MagicFunctions", + "native testSuccess()", + "int runtimeCounter = 0", + "int compiletimeValue = 0", + "int array compiletimeValues = [0]", + "int nestedCounter = 0", + "function initializeNested() returns int", + " nestedCounter++", + " return nestedCounter", + "int nestedObserved = initializeNested()", + "function initialize() returns int", + " runtimeCounter++", + " if compiletime", + " compiletimeValue = 42", + " compiletimeValues[0] = 7", + " return runtimeCounter", + "int observed = initialize()", + "@compiletime function fill()", + " let _snapshot = observed", + " if compiletime", + " let _nestedSnapshot = nestedObserved", + "init", + " if runtimeCounter == 1 and observed == 1 and compiletimeValue == 42 and compiletimeValues[0] == 7 and nestedCounter == 1 and nestedObserved == 1", + " testSuccess()"); + } + @Test public void testCompiletimeScalarRuntimeWriteRemainsAuthoritative() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) From 181aa190100738379285e67420fc0b814f45779e Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 10:13:23 +0200 Subject: [PATCH 04/13] review fixes --- .../de/peeeq/wurstscript/WurstOperator.java | 66 +++++++++++++++++-- .../intermediatelang/ILconstBool.java | 43 ++++++++++-- .../interpreter/EvaluateExpr.java | 3 +- .../interpreter/ProgramState.java | 9 --- .../interpreter/RunStatement.java | 4 +- .../wurstscript/tests/CompiletimeTests.java | 25 +++++-- 6 files changed, 120 insertions(+), 30 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java index d9e27d8b3..1773c9f8b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java @@ -133,15 +133,15 @@ public ILconst evaluateBinaryOperator(ILconst left, Supplier right) { switch (this) { case AND: - return ILconstBool.instance(((ILconstBool) left).getVal() && ((ILconstBool) right.get()).getVal()); + return evaluateBooleanAnd((ILconstBool) left, right); case OR: - return ILconstBool.instance(((ILconstBool) left).getVal() || ((ILconstBool) right.get()).getVal()); + return evaluateBooleanOr((ILconstBool) left, right); case DIV_INT: return new ILconstInt(((ILconstInt) left).getVal() / ((ILconstInt) right.get()).getVal()); case DIV_REAL: return new ILconstReal(getReal(left) / getReal(right.get())); case EQ: - return ILconstBool.instance(left.equals(right.get())); + return evaluateEquality(left, right.get(), false); case GREATER: return ((ILconstNum) left).greater((ILconstNum) right.get()); case GREATER_EQ: @@ -161,7 +161,7 @@ public ILconst evaluateBinaryOperator(ILconst left, case MULT: return ((ILconstNum) left).mul((ILconstNum) right.get()); case NOTEQ: - return ILconstBool.instance(!left.equals(right.get())); + return evaluateEquality(left, right.get(), true); case PLUS: return ((ILconstAddable) left).add((ILconstAddable) right.get()); case NOT: @@ -172,6 +172,64 @@ public ILconst evaluateBinaryOperator(ILconst left, } + private static ILconstBool evaluateBooleanAnd(ILconstBool left, Supplier right) { + if (!left.getVal()) { + if (left.isRuntimeValKnown() && !left.getRuntimeVal()) { + return ILconstBool.FALSE; + } + return ILconstBool.withUnknownRuntimeValue(false); + } + ILconstBool rightBool = (ILconstBool) right.get(); + boolean value = rightBool.getVal(); + if (left.isRuntimeValKnown()) { + if (!left.getRuntimeVal()) { + return ILconstBool.withRuntimeValue(value, false); + } + if (rightBool.isRuntimeValKnown()) { + return ILconstBool.withRuntimeValue(value, rightBool.getRuntimeVal()); + } + } else if (rightBool.isRuntimeValKnown() && !rightBool.getRuntimeVal()) { + return ILconstBool.withRuntimeValue(value, false); + } + return ILconstBool.withUnknownRuntimeValue(value); + } + + private static ILconstBool evaluateBooleanOr(ILconstBool left, Supplier right) { + if (left.getVal()) { + if (left.isRuntimeValKnown() && left.getRuntimeVal()) { + return ILconstBool.TRUE; + } + return ILconstBool.withUnknownRuntimeValue(true); + } + ILconstBool rightBool = (ILconstBool) right.get(); + boolean value = rightBool.getVal(); + if (left.isRuntimeValKnown()) { + if (left.getRuntimeVal()) { + return ILconstBool.withRuntimeValue(value, true); + } + if (rightBool.isRuntimeValKnown()) { + return ILconstBool.withRuntimeValue(value, rightBool.getRuntimeVal()); + } + } else if (rightBool.isRuntimeValKnown() && rightBool.getRuntimeVal()) { + return ILconstBool.withRuntimeValue(value, true); + } + return ILconstBool.withUnknownRuntimeValue(value); + } + + private static ILconstBool evaluateEquality(ILconst left, ILconst right, boolean negated) { + boolean value = left.equals(right) != negated; + if (left instanceof ILconstBool && right instanceof ILconstBool) { + ILconstBool leftBool = (ILconstBool) left; + ILconstBool rightBool = (ILconstBool) right; + if (leftBool.isRuntimeValKnown() && rightBool.isRuntimeValKnown()) { + boolean runtimeValue = (leftBool.getRuntimeVal() == rightBool.getRuntimeVal()) != negated; + return ILconstBool.withRuntimeValue(value, runtimeValue); + } + return ILconstBool.withUnknownRuntimeValue(value); + } + return ILconstBool.instance(value); + } + /** * Reference semantics for Wurst's integer {@code mod}: matches Blizzard.j's * ModuloInteger (truncated remainder, plus divisor if the remainder is diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java index 4784504d4..8759cfcde 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java @@ -8,22 +8,50 @@ public class ILconstBool extends ILconstAbstract { private final boolean val; + // Tracks the corresponding runtime value while compiletime code evaluates lazy initializers. + private final boolean runtimeVal; + private final boolean runtimeValKnown; - public final static ILconstBool FALSE = new ILconstBool(false); - public final static ILconstBool TRUE = new ILconstBool(true); + public final static ILconstBool FALSE = new ILconstBool(false, false, true); + public final static ILconstBool TRUE = new ILconstBool(true, true, true); public static ILconstBool instance(boolean value) { return value ? TRUE : FALSE; } - private ILconstBool(boolean b) { - val = b; + public static ILconstBool withRuntimeValue(boolean value, boolean runtimeValue) { + if (value == runtimeValue) { + return instance(value); + } + return new ILconstBool(value, runtimeValue, true); + } + + public static ILconstBool withUnknownRuntimeValue(boolean value) { + return new ILconstBool(value, false, false); + } + + private ILconstBool(boolean val, boolean runtimeVal, boolean runtimeValKnown) { + this.val = val; + this.runtimeVal = runtimeVal; + this.runtimeValKnown = runtimeValKnown; } public boolean getVal() { return val; } + public boolean getRuntimeVal() { + return runtimeVal; + } + + public boolean isRuntimeValKnown() { + return runtimeValKnown; + } + + public boolean canDifferAtRuntime() { + return !runtimeValKnown || val != runtimeVal; + } + @Override public String print() { return val ? "true" : "false"; @@ -35,12 +63,15 @@ public WurstType getType() { } public ILconst negate() { - return val ? FALSE : TRUE; + if (!runtimeValKnown) { + return withUnknownRuntimeValue(!val); + } + return withRuntimeValue(!val, !runtimeVal); } @Override public boolean isEqualTo(ILconst other) { - return other == this; + return other instanceof ILconstBool && val == ((ILconstBool) other).val; } @Override 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 ad33e3570..247596104 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 @@ -130,8 +130,7 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l ImVar var = e.getVar(); if (var.isGlobal()) { if (isMagicCompiletimeConstant(var)) { - globalState.markCompiletimeConstantRead(); - return ILconstBool.instance(globalState.isCompiletime()); + return ILconstBool.withRuntimeValue(globalState.isCompiletime(), false); } ILconst r = globalState.getVal(var); if (r == null) { 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 75c023caa..57777778b 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 @@ -50,7 +50,6 @@ public class ProgramState extends State implements AutoCloseable { private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); private int untrackedWriteDepth; private int trackedWriteDepth; - private long compiletimeConstantReadVersion; private static boolean containsTypeVariable(ImType type) { return type.match(new ImType.Matcher() { @@ -750,14 +749,6 @@ public ILconst evaluateUntracked(ImExpr expr, LocalState localState) { } } - void markCompiletimeConstantRead() { - compiletimeConstantReadVersion++; - } - - long getCompiletimeConstantReadVersion() { - return compiletimeConstantReadVersion; - } - void runWithTrackedWrites(Runnable action) { trackedWriteDepth++; try { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index 56ffa5375..31dbb2ba4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -23,13 +23,11 @@ public static void run(ImExitwhen s, ProgramState globalState, LocalState localS } public static void run(ImIf s, ProgramState globalState, LocalState localState) { - long compiletimeReadsBefore = globalState.getCompiletimeConstantReadVersion(); ILconstBool c = (ILconstBool) s.getCondition().evaluate(globalState, localState); ImStmts selectedBlock = c.getVal() ? s.getThenBlock() : s.getElseBlock(); // Runtime repeats ordinary lazy-initializer branches, but not a branch selected using // MagicFunctions.compiletime. Preserve writes from the latter for state migration. - if (globalState.writesAreSuppressed() - && globalState.getCompiletimeConstantReadVersion() != compiletimeReadsBefore) { + if (globalState.writesAreSuppressed() && c.canDifferAtRuntime()) { globalState.runWithTrackedWrites(() -> selectedBlock.runStatements(globalState, localState)); } else { selectedBlock.runStatements(globalState, localState); 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 bb93b1a01..b4cd4da82 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 @@ -185,25 +185,38 @@ public void testCompiletimeOnlyLazyInitializerSideEffectsAreReplayed() { "native testSuccess()", "int runtimeCounter = 0", "int compiletimeValue = 0", - "int array compiletimeValues = [0]", + "int array compiletimeValues = [0, 0, 0, 0]", "int nestedCounter = 0", + "function initializeFlag() returns boolean", + " if compiletime", + " let _compiletimeOnly = true", + " return true", + "boolean flag = initializeFlag()", "function initializeNested() returns int", - " nestedCounter++", + " if flag", + " nestedCounter++", " return nestedCounter", "int nestedObserved = initializeNested()", "function initialize() returns int", " runtimeCounter++", - " if compiletime", + " let ct = compiletime", + " if ct", " compiletimeValue = 42", + " if not not ct", " compiletimeValues[0] = 7", + " if ct and true", + " compiletimeValues[1] = 8", + " if ct or false", + " compiletimeValues[2] = 9", + " if ct == true", + " compiletimeValues[3] = 10", " return runtimeCounter", "int observed = initialize()", "@compiletime function fill()", " let _snapshot = observed", - " if compiletime", - " let _nestedSnapshot = nestedObserved", + " let _nestedSnapshot = nestedObserved", "init", - " if runtimeCounter == 1 and observed == 1 and compiletimeValue == 42 and compiletimeValues[0] == 7 and nestedCounter == 1 and nestedObserved == 1", + " if runtimeCounter == 1 and observed == 1 and compiletimeValue == 42 and compiletimeValues[0] == 7 and compiletimeValues[1] == 8 and compiletimeValues[2] == 9 and compiletimeValues[3] == 10 and nestedCounter == 1 and nestedObserved == 1", " testSuccess()"); } From 637f4ebc968e37c19ee7851881c35b62cbb3e22f Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 10:49:34 +0200 Subject: [PATCH 05/13] Preserve compiletime control-flow side effects --- .../de/peeeq/wurstscript/WurstOperator.java | 23 ++++++++-- .../intermediatelang/ILconstBool.java | 4 +- .../interpreter/EvaluateExpr.java | 22 ++++++++- .../interpreter/ProgramState.java | 30 +++++++++++++ .../interpreter/RunStatement.java | 12 ++++- .../wurstscript/tests/CompiletimeTests.java | 45 +++++++++++++++++++ 6 files changed, 127 insertions(+), 9 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java index 1773c9f8b..3f7b0c44b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java @@ -131,11 +131,16 @@ public LuaOpBinary luaTranslateBinary() { public ILconst evaluateBinaryOperator(ILconst left, Supplier right) { + return evaluateBinaryOperator(left, right, null); + } + + public ILconst evaluateBinaryOperator(ILconst left, Supplier right, + @Nullable ILconstBool shortCircuitedRight) { switch (this) { case AND: - return evaluateBooleanAnd((ILconstBool) left, right); + return evaluateBooleanAnd((ILconstBool) left, right, shortCircuitedRight); case OR: - return evaluateBooleanOr((ILconstBool) left, right); + return evaluateBooleanOr((ILconstBool) left, right, shortCircuitedRight); case DIV_INT: return new ILconstInt(((ILconstInt) left).getVal() / ((ILconstInt) right.get()).getVal()); case DIV_REAL: @@ -172,11 +177,16 @@ public ILconst evaluateBinaryOperator(ILconst left, } - private static ILconstBool evaluateBooleanAnd(ILconstBool left, Supplier right) { + private static ILconstBool evaluateBooleanAnd(ILconstBool left, Supplier right, + @Nullable ILconstBool shortCircuitedRight) { if (!left.getVal()) { if (left.isRuntimeValKnown() && !left.getRuntimeVal()) { return ILconstBool.FALSE; } + if (left.isRuntimeValKnown() && shortCircuitedRight != null + && shortCircuitedRight.isRuntimeValKnown()) { + return ILconstBool.withRuntimeValue(false, shortCircuitedRight.getRuntimeVal()); + } return ILconstBool.withUnknownRuntimeValue(false); } ILconstBool rightBool = (ILconstBool) right.get(); @@ -194,11 +204,16 @@ private static ILconstBool evaluateBooleanAnd(ILconstBool left, Supplier right) { + private static ILconstBool evaluateBooleanOr(ILconstBool left, Supplier right, + @Nullable ILconstBool shortCircuitedRight) { if (left.getVal()) { if (left.isRuntimeValKnown() && left.getRuntimeVal()) { return ILconstBool.TRUE; } + if (left.isRuntimeValKnown() && shortCircuitedRight != null + && shortCircuitedRight.isRuntimeValKnown()) { + return ILconstBool.withRuntimeValue(true, shortCircuitedRight.getRuntimeVal()); + } return ILconstBool.withUnknownRuntimeValue(true); } ILconstBool rightBool = (ILconstBool) right.get(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java index 8759cfcde..78507364c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java @@ -48,8 +48,8 @@ public boolean isRuntimeValKnown() { return runtimeValKnown; } - public boolean canDifferAtRuntime() { - return !runtimeValKnown || val != runtimeVal; + public boolean isKnownToDifferAtRuntime() { + return runtimeValKnown && val != runtimeVal; } @Override 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 247596104..5e5e0314a 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 @@ -78,7 +78,13 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin final ImExprs arguments = e.getArguments(); WurstOperator op = e.getOp(); if (arguments.size() == 2 && op.isBinaryOp()) { - return op.evaluateBinaryOperator(arguments.get(0).evaluate(globalState, localState), () -> arguments.get(1).evaluate(globalState, localState)); + ILconst left = arguments.get(0).evaluate(globalState, localState); + ImExpr right = arguments.get(1); + ILconstBool shortCircuitedRight = right instanceof ImBoolVal + ? ILconstBool.instance(((ImBoolVal) right).getValB()) + : null; + return op.evaluateBinaryOperator(left, + () -> evaluateRightOperand(op, left, right, globalState, localState), shortCircuitedRight); } else if (arguments.size() == 1 && op.isUnaryOp()) { return op.evaluateUnaryOperator(arguments.get(0).evaluate(globalState, localState)); } else { @@ -86,6 +92,20 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin } } + private static ILconst evaluateRightOperand(WurstOperator op, ILconst left, ImExpr right, + ProgramState globalState, LocalState localState) { + if (globalState.writesAreSuppressed() && left instanceof ILconstBool) { + ILconstBool leftBool = (ILconstBool) left; + boolean compiletimeOnly = leftBool.isRuntimeValKnown() + && ((op == WurstOperator.AND && leftBool.getVal() && !leftBool.getRuntimeVal()) + || (op == WurstOperator.OR && !leftBool.getVal() && leftBool.getRuntimeVal())); + if (compiletimeOnly) { + return globalState.evaluateWithTrackedWrites(() -> right.evaluate(globalState, localState)); + } + } + return right.evaluate(globalState, localState); + } + public static ILconst eval(ImRealVal e, ProgramState globalState, LocalState localState) { return new ILconstReal(e.getValR()); } 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 57777778b..006db29b5 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 @@ -19,6 +19,7 @@ import java.io.PrintStream; import java.util.*; +import java.util.function.Supplier; public class ProgramState extends State implements AutoCloseable { @@ -50,6 +51,7 @@ public class ProgramState extends State implements AutoCloseable { private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); private int untrackedWriteDepth; private int trackedWriteDepth; + private final Deque trackedLoopIterations = new ArrayDeque<>(); private static boolean containsTypeVariable(ImType type) { return type.match(new ImType.Matcher() { @@ -758,6 +760,34 @@ void runWithTrackedWrites(Runnable action) { } } + T evaluateWithTrackedWrites(Supplier action) { + trackedWriteDepth++; + try { + return action.get(); + } finally { + trackedWriteDepth--; + } + } + + void beginLoopIteration() { + trackedLoopIterations.push(false); + } + + void trackCurrentLoopIterationWrites() { + if (!trackedLoopIterations.pop()) { + trackedLoopIterations.push(true); + trackedWriteDepth++; + } else { + trackedLoopIterations.push(true); + } + } + + void endLoopIteration() { + if (trackedLoopIterations.pop()) { + trackedWriteDepth--; + } + } + private boolean writesAreTracked() { // A compiletime-dependent branch cancels one enclosing lazy-initializer suppression scope. // A nested lazy initializer therefore becomes untracked again until its own such branch. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index 31dbb2ba4..7553d672e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -20,6 +20,9 @@ public static void run(ImExitwhen s, ProgramState globalState, LocalState localS if (c.getVal()) { throw ExitwhenException.instance(); } + if (globalState.writesAreSuppressed() && c.isKnownToDifferAtRuntime()) { + globalState.trackCurrentLoopIterationWrites(); + } } public static void run(ImIf s, ProgramState globalState, LocalState localState) { @@ -27,7 +30,7 @@ public static void run(ImIf s, ProgramState globalState, LocalState localState) ImStmts selectedBlock = c.getVal() ? s.getThenBlock() : s.getElseBlock(); // Runtime repeats ordinary lazy-initializer branches, but not a branch selected using // MagicFunctions.compiletime. Preserve writes from the latter for state migration. - if (globalState.writesAreSuppressed() && c.canDifferAtRuntime()) { + if (globalState.writesAreSuppressed() && c.isKnownToDifferAtRuntime()) { globalState.runWithTrackedWrites(() -> selectedBlock.runStatements(globalState, localState)); } else { selectedBlock.runStatements(globalState, localState); @@ -41,7 +44,12 @@ public static void run(ImLoop s, ProgramState globalState, LocalState localState if (Thread.currentThread().isInterrupted()) { throw new InterpreterException(globalState, "Execution interrupted"); } - s.getBody().runStatements(globalState, localState); + globalState.beginLoopIteration(); + try { + s.getBody().runStatements(globalState, localState); + } finally { + globalState.endLoopIteration(); + } } } catch (ExitwhenException e) { // end of loop 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 b4cd4da82..1ffd53cc0 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 @@ -220,6 +220,51 @@ public void testCompiletimeOnlyLazyInitializerSideEffectsAreReplayed() { " testSuccess()"); } + @Test + public void testCompiletimeLazyInitializerControlFlowEdges() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package MagicFunctions", + "public constant compiletime = false", + "endpackage", + "package Test", + "import MagicFunctions", + "native testSuccess()", + "int conditionCounter = 0", + "function mark() returns boolean", + " conditionCounter++", + " return true", + "function initializeCondition() returns int", + " if compiletime and mark()", + " skip", + " return conditionCounter", + "int conditionObserved = initializeCondition()", + "int unresolvedCounter = 0", + "function runtimeFalse() returns boolean", + " return false", + "function initializeUnresolved() returns int", + " if not compiletime and runtimeFalse()", + " skip", + " else", + " unresolvedCounter++", + " return unresolvedCounter", + "int unresolvedObserved = initializeUnresolved()", + "int loopCounter = 0", + "function initializeLoop() returns int", + " var i = 0", + " while compiletime and i == 0", + " loopCounter++", + " i++", + " return loopCounter", + "int loopObserved = initializeLoop()", + "@compiletime function fill()", + " let _conditionSnapshot = conditionObserved", + " let _unresolvedSnapshot = unresolvedObserved", + " let _loopSnapshot = loopObserved", + "init", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and loopCounter == 1 and loopObserved == 1", + " testSuccess()"); + } + @Test public void testCompiletimeScalarRuntimeWriteRemainsAuthoritative() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) From 1f4406c84553213a16a8b026d8ba225156cb200f Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 11:15:24 +0200 Subject: [PATCH 06/13] Resolve skipped compiletime guard operands --- .../interpreter/EvaluateExpr.java | 44 ++++++++++++++++++- .../wurstscript/tests/CompiletimeTests.java | 9 +++- 2 files changed, 51 insertions(+), 2 deletions(-) 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 5e5e0314a..8c010e141 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 @@ -8,6 +8,7 @@ import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.intermediatelang.*; import de.peeeq.wurstscript.jassIm.*; +import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; import de.peeeq.wurstscript.translation.imtranslation.ImPrinter; import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.Utils; @@ -17,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -82,7 +84,7 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin ImExpr right = arguments.get(1); ILconstBool shortCircuitedRight = right instanceof ImBoolVal ? ILconstBool.instance(((ImBoolVal) right).getValB()) - : null; + : evaluateSkippedRuntimeOperand(op, left, right, globalState, localState); return op.evaluateBinaryOperator(left, () -> evaluateRightOperand(op, left, right, globalState, localState), shortCircuitedRight); } else if (arguments.size() == 1 && op.isUnaryOp()) { @@ -92,6 +94,46 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin } } + private static @Nullable ILconstBool evaluateSkippedRuntimeOperand(WurstOperator op, ILconst left, ImExpr right, + ProgramState globalState, LocalState localState) { + if (!(left instanceof ILconstBool)) { + return null; + } + ILconstBool leftBool = (ILconstBool) left; + boolean runtimeEvaluatesRight = leftBool.isRuntimeValKnown() + && ((op == WurstOperator.AND && !leftBool.getVal() && leftBool.getRuntimeVal()) + || (op == WurstOperator.OR && leftBool.getVal() && !leftBool.getRuntimeVal())); + if (!runtimeEvaluatesRight) { + return null; + } + + SideEffectAnalyzer effects = new SideEffectAnalyzer(globalState.getProg()); + if (!effects.calledNatives(right).isEmpty()) { + return null; + } + Set usedVariables = effects.usedVariables(right); + if (usedVariables.stream().anyMatch(ImVar::isGlobal)) { + return null; + } + + LocalState runtimeLocals = new LocalState(); + for (ImVar variable : usedVariables) { + ILconst value = localState.getVal(variable); + if (value == null) { + continue; + } + if (!(value instanceof ILconstBool) || !((ILconstBool) value).isRuntimeValKnown()) { + return null; + } + runtimeLocals.setVal(variable, ILconstBool.instance(((ILconstBool) value).getRuntimeVal())); + } + + try (ProgramState runtimeState = new ProgramState(globalState.getGui(), globalState.getProg(), false)) { + ILconst runtimeValue = right.evaluate(runtimeState, runtimeLocals); + return runtimeValue instanceof ILconstBool ? (ILconstBool) runtimeValue : null; + } + } + private static ILconst evaluateRightOperand(WurstOperator op, ILconst left, ImExpr right, ProgramState globalState, LocalState localState) { if (globalState.writesAreSuppressed() && left instanceof ILconstBool) { 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 1ffd53cc0..10ae56f66 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 @@ -248,6 +248,12 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " unresolvedCounter++", " return unresolvedCounter", "int unresolvedObserved = initializeUnresolved()", + "int oppositeCounter = 0", + "function initializeOpposite() returns int", + " if compiletime or runtimeFalse()", + " oppositeCounter++", + " return oppositeCounter", + "int oppositeObserved = initializeOpposite()", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -259,9 +265,10 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { "@compiletime function fill()", " let _conditionSnapshot = conditionObserved", " let _unresolvedSnapshot = unresolvedObserved", + " let _oppositeSnapshot = oppositeObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From f23f12a85197f5f9c59dd463dea51a81990281f9 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 11:43:18 +0200 Subject: [PATCH 07/13] Preserve stable locals in runtime probes --- .../interpreter/EvaluateExpr.java | 16 ++++++++++++++-- .../wurstscript/tests/CompiletimeTests.java | 9 ++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) 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 8c010e141..86f108edc 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 @@ -122,10 +122,11 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin if (value == null) { continue; } - if (!(value instanceof ILconstBool) || !((ILconstBool) value).isRuntimeValKnown()) { + ILconst runtimeValue = runtimeProbeValue(value); + if (runtimeValue == null) { return null; } - runtimeLocals.setVal(variable, ILconstBool.instance(((ILconstBool) value).getRuntimeVal())); + runtimeLocals.setVal(variable, runtimeValue); } try (ProgramState runtimeState = new ProgramState(globalState.getGui(), globalState.getProg(), false)) { @@ -134,6 +135,17 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin } } + private static @Nullable ILconst runtimeProbeValue(ILconst value) { + if (value instanceof ILconstBool) { + ILconstBool boolValue = (ILconstBool) value; + return boolValue.isRuntimeValKnown() ? ILconstBool.instance(boolValue.getRuntimeVal()) : null; + } + if (value instanceof ILconstNum || value instanceof ILconstString || value instanceof ILconstNull) { + return value; + } + return null; + } + private static ILconst evaluateRightOperand(WurstOperator op, ILconst left, ImExpr right, ProgramState globalState, LocalState localState) { if (globalState.writesAreSuppressed() && left instanceof ILconstBool) { 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 10ae56f66..1c2ca9623 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 @@ -254,6 +254,12 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " oppositeCounter++", " return oppositeCounter", "int oppositeObserved = initializeOpposite()", + "int stableLocalCounter = 0", + "function initializeStableLocal(int guard) returns int", + " if compiletime or guard == 0", + " stableLocalCounter++", + " return stableLocalCounter", + "int stableLocalObserved = initializeStableLocal(1)", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -266,9 +272,10 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " let _conditionSnapshot = conditionObserved", " let _unresolvedSnapshot = unresolvedObserved", " let _oppositeSnapshot = oppositeObserved", + " let _stableLocalSnapshot = stableLocalObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From 439c74b1fc0da0c6aa5563cec8aef5d0919295f7 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 12:10:47 +0200 Subject: [PATCH 08/13] Preserve runtime state in compiletime probes --- .../interpreter/EvaluateExpr.java | 65 +++++++++++++++++-- .../interpreter/LocalState.java | 58 ++++++++++++++++- .../interpreter/ProgramState.java | 4 ++ .../wurstscript/tests/CompiletimeTests.java | 22 ++++++- 4 files changed, 138 insertions(+), 11 deletions(-) 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 86f108edc..c4273f22b 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 @@ -15,7 +15,9 @@ import org.eclipse.jdt.annotation.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -112,17 +114,16 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin return null; } Set usedVariables = effects.usedVariables(right); - if (usedVariables.stream().anyMatch(ImVar::isGlobal)) { - return null; - } - LocalState runtimeLocals = new LocalState(); for (ImVar variable : usedVariables) { + if (variable.isGlobal()) { + continue; + } ILconst value = localState.getVal(variable); if (value == null) { continue; } - ILconst runtimeValue = runtimeProbeValue(value); + ILconst runtimeValue = runtimeProbeValue(localState.getRuntimeVal(variable)); if (runtimeValue == null) { return null; } @@ -130,12 +131,58 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin } try (ProgramState runtimeState = new ProgramState(globalState.getGui(), globalState.getProg(), false)) { + Set initializedGlobals = Collections.newSetFromMap(new IdentityHashMap<>()); + Set initializingGlobals = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImVar variable : usedVariables) { + if (variable.isGlobal() + && !prepareRuntimeProbeGlobal(variable, runtimeState, effects, + initializedGlobals, initializingGlobals)) { + return null; + } + } ILconst runtimeValue = right.evaluate(runtimeState, runtimeLocals); return runtimeValue instanceof ILconstBool ? (ILconstBool) runtimeValue : null; } } - private static @Nullable ILconst runtimeProbeValue(ILconst value) { + private static boolean prepareRuntimeProbeGlobal(ImVar variable, ProgramState runtimeState, + SideEffectAnalyzer effects, Set initialized, + Set initializing) { + if (isMagicCompiletimeConstant(variable) || initialized.contains(variable)) { + return true; + } + if (!initializing.add(variable)) { + return false; + } + List initializers = runtimeState.getProg().getGlobalInits().get(variable); + if (initializers == null || initializers.isEmpty()) { + initializing.remove(variable); + return false; + } + ImExpr initializer = initializers.get(0).getRight(); + if (!effects.calledNatives(initializer).isEmpty()) { + initializing.remove(variable); + return false; + } + for (ImVar dependency : effects.usedVariables(initializer)) { + if (dependency.isGlobal() + && !prepareRuntimeProbeGlobal(dependency, runtimeState, effects, initialized, initializing)) { + initializing.remove(variable); + return false; + } + } + ILconst value = runtimeProbeValue(initializer.evaluate(runtimeState, new LocalState())); + if (value == null) { + initializing.remove(variable); + return false; + } + runtimeState.setValUntracked(variable, value); + initializing.remove(variable); + initialized.add(variable); + return true; + } + + private static @Nullable ILconst runtimeProbeValue(@Nullable ILconst value) { if (value instanceof ILconstBool) { ILconstBool boolValue = (ILconstBool) value; return boolValue.isRuntimeValKnown() ? ILconstBool.instance(boolValue.getRuntimeVal()) : null; @@ -390,7 +437,11 @@ public static ILaddress evaluateLvalue(ImVarAccess va, ProgramState globalState, return new ILaddress() { @Override public void set(ILconst value) { - state.setVal(v, value); + if (!v.isGlobal() && globalState.isInCompiletimeOnlyPath()) { + localState.setValCompiletimeOnly(v, value); + } else { + state.setVal(v, value); + } } @Override diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java index 4f6f450f7..6de0007d0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java @@ -1,14 +1,21 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; import de.peeeq.wurstscript.intermediatelang.ILconst; +import de.peeeq.wurstscript.intermediatelang.ILconstBool; +import de.peeeq.wurstscript.jassIm.ImVar; import org.eclipse.jdt.annotation.Nullable; -/** - * Unchanged API. No eager map allocations unless you actually set/get vars/arrays. - */ +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +/** Local interpreter values plus their runtime counterparts during compiletime evaluation. */ public class LocalState extends State { private @Nullable ILconst returnVal; + private final Map runtimeValues = new IdentityHashMap<>(); + private final Set unknownRuntimeValues = Collections.newSetFromMap(new IdentityHashMap<>()); public LocalState() { // no eager allocations @@ -18,6 +25,51 @@ public LocalState(ILconst returnVal) { this.returnVal = returnVal; } + @Override + public void setVal(ImVar v, ILconst val) { + super.setVal(v, val); + if (val instanceof ILconstBool) { + ILconstBool boolVal = (ILconstBool) val; + if (!boolVal.isRuntimeValKnown()) { + runtimeValues.remove(v); + unknownRuntimeValues.add(v); + return; + } + runtimeValues.put(v, ILconstBool.instance(boolVal.getRuntimeVal())); + } else { + runtimeValues.put(v, val); + } + unknownRuntimeValues.remove(v); + } + + public void setValCompiletimeOnly(ImVar v, ILconst val) { + super.setVal(v, val); + if (!runtimeValues.containsKey(v)) { + unknownRuntimeValues.add(v); + } + } + + @Override + public @Nullable ILconst getVal(ImVar v) { + ILconst val = super.getVal(v); + if (!(val instanceof ILconstBool)) { + return val; + } + if (unknownRuntimeValues.contains(v)) { + return ILconstBool.withUnknownRuntimeValue(((ILconstBool) val).getVal()); + } + ILconst runtimeVal = runtimeValues.get(v); + if (runtimeVal instanceof ILconstBool) { + return ILconstBool.withRuntimeValue( + ((ILconstBool) val).getVal(), ((ILconstBool) runtimeVal).getVal()); + } + return val; + } + + public @Nullable ILconst getRuntimeVal(ImVar v) { + return unknownRuntimeValues.contains(v) ? null : runtimeValues.get(v); + } + public @Nullable ILconst getReturnVal() { return returnVal; } 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 006db29b5..213295f49 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 @@ -798,6 +798,10 @@ boolean writesAreSuppressed() { return !writesAreTracked(); } + boolean isInCompiletimeOnlyPath() { + return trackedWriteDepth > 0; + } + public boolean isCompiletime() { return isCompiletime; 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 1c2ca9623..1397efae3 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 @@ -260,6 +260,24 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " stableLocalCounter++", " return stableLocalCounter", "int stableLocalObserved = initializeStableLocal(1)", + "boolean stableGlobalGuard = false", + "int stableGlobalCounter = 0", + "function initializeStableGlobal() returns int", + " if compiletime or stableGlobalGuard", + " stableGlobalCounter++", + " return stableGlobalCounter", + "int stableGlobalObserved = initializeStableGlobal()", + "int divergentLocalCounter = 0", + "function compiletimeLocalGuard() returns boolean", + " var result = false", + " if compiletime", + " result = true", + " return result", + "function initializeDivergentLocal() returns int", + " if compiletimeLocalGuard()", + " divergentLocalCounter++", + " return divergentLocalCounter", + "int divergentLocalObserved = initializeDivergentLocal()", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -273,9 +291,11 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " let _unresolvedSnapshot = unresolvedObserved", " let _oppositeSnapshot = oppositeObserved", " let _stableLocalSnapshot = stableLocalObserved", + " let _stableGlobalSnapshot = stableGlobalObserved", + " let _divergentLocalSnapshot = divergentLocalObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From 0332ef4e61d89db4e7a7a34ecefceb7ccb6488b6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 13:07:00 +0200 Subject: [PATCH 09/13] Refine compiletime runtime condition probes --- .../interpreter/EvaluateExpr.java | 44 ++++++++++++++++--- .../interpreter/LocalState.java | 2 + .../interpreter/RunStatement.java | 6 +++ .../wurstscript/tests/CompiletimeTests.java | 20 ++++++++- 4 files changed, 64 insertions(+), 8 deletions(-) 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 c4273f22b..f51a4860a 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 @@ -109,11 +109,26 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin return null; } + return evaluateRuntimeBooleanExpression(right, globalState, localState); + } + + static ILconstBool refineRuntimeCondition(ImExpr condition, ILconstBool compiletimeValue, + ProgramState globalState, LocalState localState) { + ILconstBool runtimeValue = evaluateRuntimeBooleanExpression(condition, globalState, localState); + if (runtimeValue == null) { + return compiletimeValue; + } + return ILconstBool.withRuntimeValue(compiletimeValue.getVal(), runtimeValue.getVal()); + } + + private static @Nullable ILconstBool evaluateRuntimeBooleanExpression(ImExpr expression, + ProgramState globalState, + LocalState localState) { SideEffectAnalyzer effects = new SideEffectAnalyzer(globalState.getProg()); - if (!effects.calledNatives(right).isEmpty()) { + if (!effects.calledNatives(expression).isEmpty()) { return null; } - Set usedVariables = effects.usedVariables(right); + Set usedVariables = effects.usedVariables(expression); LocalState runtimeLocals = new LocalState(); for (ImVar variable : usedVariables) { if (variable.isGlobal()) { @@ -135,22 +150,32 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin Set initializingGlobals = Collections.newSetFromMap(new IdentityHashMap<>()); for (ImVar variable : usedVariables) { if (variable.isGlobal() - && !prepareRuntimeProbeGlobal(variable, runtimeState, effects, + && !prepareRuntimeProbeGlobal(variable, globalState, runtimeState, effects, initializedGlobals, initializingGlobals)) { return null; } } - ILconst runtimeValue = right.evaluate(runtimeState, runtimeLocals); + ILconst runtimeValue = expression.evaluate(runtimeState, runtimeLocals); return runtimeValue instanceof ILconstBool ? (ILconstBool) runtimeValue : null; } } - private static boolean prepareRuntimeProbeGlobal(ImVar variable, ProgramState runtimeState, + private static boolean prepareRuntimeProbeGlobal(ImVar variable, ProgramState sourceState, + ProgramState runtimeState, SideEffectAnalyzer effects, Set initialized, Set initializing) { if (isMagicCompiletimeConstant(variable) || initialized.contains(variable)) { return true; } + if (isMagicFunctionsConstant(variable, "isLua")) { + ILconst runtimeValue = runtimeProbeValue(sourceState.getVal(variable)); + if (runtimeValue == null) { + return false; + } + runtimeState.setValUntracked(variable, runtimeValue); + initialized.add(variable); + return true; + } if (!initializing.add(variable)) { return false; } @@ -166,7 +191,8 @@ private static boolean prepareRuntimeProbeGlobal(ImVar variable, ProgramState ru } for (ImVar dependency : effects.usedVariables(initializer)) { if (dependency.isGlobal() - && !prepareRuntimeProbeGlobal(dependency, runtimeState, effects, initialized, initializing)) { + && !prepareRuntimeProbeGlobal(dependency, sourceState, runtimeState, effects, + initialized, initializing)) { initializing.remove(variable); return false; } @@ -270,9 +296,13 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l } private static boolean isMagicCompiletimeConstant(ImVar var) { + return isMagicFunctionsConstant(var, "compiletime"); + } + + private static boolean isMagicFunctionsConstant(ImVar var, String name) { if (var.getTrace() instanceof VarDef) { VarDef varDef = (VarDef) var.getTrace(); - if (varDef.getName().equals("compiletime")) { + if (varDef.getName().equals(name)) { PackageOrGlobal nearestPackage = varDef.attrNearestPackage(); if (nearestPackage instanceof WPackage) { WPackage p = (WPackage) nearestPackage; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java index 6de0007d0..80793897c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java @@ -53,6 +53,8 @@ public void setValCompiletimeOnly(ImVar v, ILconst val) { public @Nullable ILconst getVal(ImVar v) { ILconst val = super.getVal(v); if (!(val instanceof ILconstBool)) { + // Other IL constants cannot carry a second value. Conditions that consume them are + // re-evaluated with getRuntimeVal() by EvaluateExpr.refineRuntimeCondition instead. return val; } if (unknownRuntimeValues.contains(v)) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index 7553d672e..6cecf9658 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -17,6 +17,9 @@ public static void run(ImExpr e, ProgramState globalState, LocalState localState public static void run(ImExitwhen s, ProgramState globalState, LocalState localState) { ILconstBool c = (ILconstBool) s.getCondition().evaluate(globalState, localState); + if (globalState.writesAreSuppressed()) { + c = EvaluateExpr.refineRuntimeCondition(s.getCondition(), c, globalState, localState); + } if (c.getVal()) { throw ExitwhenException.instance(); } @@ -27,6 +30,9 @@ public static void run(ImExitwhen s, ProgramState globalState, LocalState localS public static void run(ImIf s, ProgramState globalState, LocalState localState) { ILconstBool c = (ILconstBool) s.getCondition().evaluate(globalState, localState); + if (globalState.writesAreSuppressed()) { + c = EvaluateExpr.refineRuntimeCondition(s.getCondition(), c, globalState, localState); + } ImStmts selectedBlock = c.getVal() ? s.getThenBlock() : s.getElseBlock(); // Runtime repeats ordinary lazy-initializer branches, but not a branch selected using // MagicFunctions.compiletime. Preserve writes from the latter for state migration. 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 1397efae3..c1d8f04bb 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 @@ -225,6 +225,7 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package MagicFunctions", "public constant compiletime = false", + "public constant isLua = false", "endpackage", "package Test", "import MagicFunctions", @@ -278,6 +279,21 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " divergentLocalCounter++", " return divergentLocalCounter", "int divergentLocalObserved = initializeDivergentLocal()", + "int divergentIntCounter = 0", + "function initializeDivergentInt() returns int", + " var result = 0", + " if compiletime", + " result = 1", + " if result == 1", + " divergentIntCounter++", + " return divergentIntCounter", + "int divergentIntObserved = initializeDivergentInt()", + "int luaTargetCounter = 0", + "function initializeLuaTarget() returns int", + " if compiletime or isLua", + " luaTargetCounter++", + " return luaTargetCounter", + "int luaTargetObserved = initializeLuaTarget()", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -293,9 +309,11 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " let _stableLocalSnapshot = stableLocalObserved", " let _stableGlobalSnapshot = stableGlobalObserved", " let _divergentLocalSnapshot = divergentLocalObserved", + " let _divergentIntSnapshot = divergentIntObserved", + " let _luaTargetSnapshot = luaTargetObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From 840fcca3230d07814576d78f9965dbd37a816df4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 13:20:48 +0200 Subject: [PATCH 10/13] Keep local arrays out of runtime probes --- .../intermediatelang/interpreter/EvaluateExpr.java | 2 +- .../tests/wurstscript/tests/CompiletimeTests.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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 f51a4860a..2f3cb658d 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 @@ static ILconstBool refineRuntimeCondition(ImExpr condition, ILconstBool compilet } ILconst value = localState.getVal(variable); if (value == null) { - continue; + return null; } ILconst runtimeValue = runtimeProbeValue(localState.getRuntimeVal(variable)); if (runtimeValue == null) { 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 c1d8f04bb..23de74798 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 @@ -294,6 +294,15 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " luaTargetCounter++", " return luaTargetCounter", "int luaTargetObserved = initializeLuaTarget()", + "int localArrayCounter = 0", + "function initializeLocalArray() returns int", + " let flags = [1]", + " if flags[0] == 0", + " skip", + " else", + " localArrayCounter++", + " return localArrayCounter", + "int localArrayObserved = initializeLocalArray()", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -311,9 +320,10 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " let _divergentLocalSnapshot = divergentLocalObserved", " let _divergentIntSnapshot = divergentIntObserved", " let _luaTargetSnapshot = luaTargetObserved", + " let _localArraySnapshot = localArrayObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and localArrayCounter == 1 and localArrayObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From fdbfb27b6b01ee6fd5389c22b3dae2fb571e2050 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 13:32:00 +0200 Subject: [PATCH 11/13] Avoid probing previously written globals --- .../intermediatelang/interpreter/EvaluateExpr.java | 3 +++ .../intermediatelang/interpreter/ProgramState.java | 7 +++++++ .../tests/wurstscript/tests/CompiletimeTests.java | 11 ++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) 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 2f3cb658d..562f1a349 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 @@ -176,6 +176,9 @@ private static boolean prepareRuntimeProbeGlobal(ImVar variable, ProgramState so initialized.add(variable); return true; } + if (sourceState.wasWrittenWhileSuppressed(variable)) { + return false; + } if (!initializing.add(variable)) { return false; } 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 213295f49..2bd06cad4 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 @@ -41,6 +41,7 @@ public class ProgramState extends State implements AutoCloseable { private final Map genericStaticOwner = new HashMap<>(); private final Set modifiedScalars = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set suppressedScalarWrites = Collections.newSetFromMap(new IdentityHashMap<>()); private final Set modifiedGenericScalars = new HashSet<>(); private final Map> genericScalarTypeArguments = new HashMap<>(); private final Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); @@ -691,6 +692,8 @@ public void setVal(ImVar v, ILconst val) { boolean trackWrite = writesAreTracked(); if (trackWrite) { modifiedScalars.add(v); + } else { + suppressedScalarWrites.add(v); } String key = genericStaticKey(v); if (key != null) { @@ -798,6 +801,10 @@ boolean writesAreSuppressed() { return !writesAreTracked(); } + boolean wasWrittenWhileSuppressed(ImVar var) { + return suppressedScalarWrites.contains(var); + } + boolean isInCompiletimeOnlyPath() { return trackedWriteDepth > 0; } 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 23de74798..a3d4dc97d 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 @@ -303,6 +303,14 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " localArrayCounter++", " return localArrayCounter", "int localArrayObserved = initializeLocalArray()", + "boolean priorWriteGuard = false", + "int priorWriteCounter = 0", + "function initializePriorWrite() returns int", + " priorWriteGuard = true", + " if priorWriteGuard", + " priorWriteCounter++", + " return priorWriteCounter", + "int priorWriteObserved = initializePriorWrite()", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -321,9 +329,10 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " let _divergentIntSnapshot = divergentIntObserved", " let _luaTargetSnapshot = luaTargetObserved", " let _localArraySnapshot = localArrayObserved", + " let _priorWriteSnapshot = priorWriteObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and localArrayCounter == 1 and localArrayObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and localArrayCounter == 1 and localArrayObserved == 1 and priorWriteCounter == 1 and priorWriteObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From 077589985a8322409f2210c82c77e717e06c3359 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 13:42:42 +0200 Subject: [PATCH 12/13] Harden runtime probe dependencies --- .../intermediatelang/interpreter/EvaluateExpr.java | 2 +- .../intermediatelang/interpreter/ProgramState.java | 14 +++++++++++--- .../tests/wurstscript/tests/CompiletimeTests.java | 11 ++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) 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 562f1a349..cd8eec586 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 @@ -124,7 +124,7 @@ static ILconstBool refineRuntimeCondition(ImExpr condition, ILconstBool compilet private static @Nullable ILconstBool evaluateRuntimeBooleanExpression(ImExpr expression, ProgramState globalState, LocalState localState) { - SideEffectAnalyzer effects = new SideEffectAnalyzer(globalState.getProg()); + SideEffectAnalyzer effects = globalState.getSideEffectAnalyzer(); if (!effects.calledNatives(expression).isEmpty()) { return null; } 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 2bd06cad4..9c6b90c5c 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 @@ -8,6 +8,7 @@ import de.peeeq.datastructures.Partitions; import de.peeeq.wurstscript.gui.WurstGui; import de.peeeq.wurstscript.intermediatelang.*; +import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.parser.WPos; import de.peeeq.wurstscript.translation.imtojass.ImAttrType; @@ -31,6 +32,7 @@ public class ProgramState extends State implements AutoCloseable { private final Object2ObjectOpenHashMap nativeProviderByFunc = new Object2ObjectOpenHashMap<>(); private final Set missingNativeFuncs = new HashSet<>(); private ImProg prog; + private final SideEffectAnalyzer sideEffectAnalyzer; private final Map classKeyLookup = new HashMap<>(); private final Map objectIdSpaces = new HashMap<>(); private final Int2ObjectOpenHashMap handleMap = new Int2ObjectOpenHashMap<>(); @@ -41,7 +43,7 @@ public class ProgramState extends State implements AutoCloseable { private final Map genericStaticOwner = new HashMap<>(); private final Set modifiedScalars = Collections.newSetFromMap(new IdentityHashMap<>()); - private final Set suppressedScalarWrites = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set suppressedWrites = Collections.newSetFromMap(new IdentityHashMap<>()); private final Set modifiedGenericScalars = new HashSet<>(); private final Map> genericScalarTypeArguments = new HashMap<>(); private final Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); @@ -100,6 +102,7 @@ public ProgramState(WurstGui gui, ImProg prog, boolean isCompiletime) { this.gui = gui; this.prog = prog; this.isCompiletime = isCompiletime; + this.sideEffectAnalyzer = new SideEffectAnalyzer(prog); buildClassKeyLookup(); identifyGenericStaticGlobals(); @@ -693,7 +696,7 @@ public void setVal(ImVar v, ILconst val) { if (trackWrite) { modifiedScalars.add(v); } else { - suppressedScalarWrites.add(v); + suppressedWrites.add(v); } String key = genericStaticKey(v); if (key != null) { @@ -802,7 +805,11 @@ boolean writesAreSuppressed() { } boolean wasWrittenWhileSuppressed(ImVar var) { - return suppressedScalarWrites.contains(var); + return suppressedWrites.contains(var); + } + + SideEffectAnalyzer getSideEffectAnalyzer() { + return sideEffectAnalyzer; } boolean isInCompiletimeOnlyPath() { @@ -860,6 +867,7 @@ protected ILconstArray getArray(ImVar v) { @Override public void setArrayVal(ImVar v, List indexes, ILconst val) { if (!writesAreTracked()) { + suppressedWrites.add(v); setArrayValUntracked(v, indexes, val); return; } 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 a3d4dc97d..e07420bdf 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 @@ -311,6 +311,14 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " priorWriteCounter++", " return priorWriteCounter", "int priorWriteObserved = initializePriorWrite()", + "int array priorArrayGuard = [0]", + "int priorArrayCounter = 0", + "function initializePriorArrayWrite() returns int", + " priorArrayGuard[0] = 1", + " if priorArrayGuard[0] == 1", + " priorArrayCounter++", + " return priorArrayCounter", + "int priorArrayObserved = initializePriorArrayWrite()", "int loopCounter = 0", "function initializeLoop() returns int", " var i = 0", @@ -330,9 +338,10 @@ public void testCompiletimeLazyInitializerControlFlowEdges() { " let _luaTargetSnapshot = luaTargetObserved", " let _localArraySnapshot = localArrayObserved", " let _priorWriteSnapshot = priorWriteObserved", + " let _priorArraySnapshot = priorArrayObserved", " let _loopSnapshot = loopObserved", "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and localArrayCounter == 1 and localArrayObserved == 1 and priorWriteCounter == 1 and priorWriteObserved == 1 and loopCounter == 1 and loopObserved == 1", + " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and localArrayCounter == 1 and localArrayObserved == 1 and priorWriteCounter == 1 and priorWriteObserved == 1 and priorArrayCounter == 1 and priorArrayObserved == 1 and loopCounter == 1 and loopObserved == 1", " testSuccess()"); } From 58b564ef831c54c4acf01c2e154e1b64c7bf4807 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 2 Aug 2026 14:30:52 +0200 Subject: [PATCH 13/13] Limit migration to explicit compiletime writes --- .../de/peeeq/wurstscript/WurstOperator.java | 81 +-------- .../intermediatelang/ILconstBool.java | 43 +---- .../interpreter/EvaluateExpr.java | 166 +---------------- .../interpreter/LocalState.java | 60 +----- .../interpreter/ProgramState.java | 79 +------- .../interpreter/RunStatement.java | 25 +-- .../wurstscript/tests/CompiletimeTests.java | 171 ------------------ 7 files changed, 29 insertions(+), 596 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java index 3f7b0c44b..d9e27d8b3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstOperator.java @@ -131,22 +131,17 @@ public LuaOpBinary luaTranslateBinary() { public ILconst evaluateBinaryOperator(ILconst left, Supplier right) { - return evaluateBinaryOperator(left, right, null); - } - - public ILconst evaluateBinaryOperator(ILconst left, Supplier right, - @Nullable ILconstBool shortCircuitedRight) { switch (this) { case AND: - return evaluateBooleanAnd((ILconstBool) left, right, shortCircuitedRight); + return ILconstBool.instance(((ILconstBool) left).getVal() && ((ILconstBool) right.get()).getVal()); case OR: - return evaluateBooleanOr((ILconstBool) left, right, shortCircuitedRight); + return ILconstBool.instance(((ILconstBool) left).getVal() || ((ILconstBool) right.get()).getVal()); case DIV_INT: return new ILconstInt(((ILconstInt) left).getVal() / ((ILconstInt) right.get()).getVal()); case DIV_REAL: return new ILconstReal(getReal(left) / getReal(right.get())); case EQ: - return evaluateEquality(left, right.get(), false); + return ILconstBool.instance(left.equals(right.get())); case GREATER: return ((ILconstNum) left).greater((ILconstNum) right.get()); case GREATER_EQ: @@ -166,7 +161,7 @@ public ILconst evaluateBinaryOperator(ILconst left, Supplier right, case MULT: return ((ILconstNum) left).mul((ILconstNum) right.get()); case NOTEQ: - return evaluateEquality(left, right.get(), true); + return ILconstBool.instance(!left.equals(right.get())); case PLUS: return ((ILconstAddable) left).add((ILconstAddable) right.get()); case NOT: @@ -177,74 +172,6 @@ public ILconst evaluateBinaryOperator(ILconst left, Supplier right, } - private static ILconstBool evaluateBooleanAnd(ILconstBool left, Supplier right, - @Nullable ILconstBool shortCircuitedRight) { - if (!left.getVal()) { - if (left.isRuntimeValKnown() && !left.getRuntimeVal()) { - return ILconstBool.FALSE; - } - if (left.isRuntimeValKnown() && shortCircuitedRight != null - && shortCircuitedRight.isRuntimeValKnown()) { - return ILconstBool.withRuntimeValue(false, shortCircuitedRight.getRuntimeVal()); - } - return ILconstBool.withUnknownRuntimeValue(false); - } - ILconstBool rightBool = (ILconstBool) right.get(); - boolean value = rightBool.getVal(); - if (left.isRuntimeValKnown()) { - if (!left.getRuntimeVal()) { - return ILconstBool.withRuntimeValue(value, false); - } - if (rightBool.isRuntimeValKnown()) { - return ILconstBool.withRuntimeValue(value, rightBool.getRuntimeVal()); - } - } else if (rightBool.isRuntimeValKnown() && !rightBool.getRuntimeVal()) { - return ILconstBool.withRuntimeValue(value, false); - } - return ILconstBool.withUnknownRuntimeValue(value); - } - - private static ILconstBool evaluateBooleanOr(ILconstBool left, Supplier right, - @Nullable ILconstBool shortCircuitedRight) { - if (left.getVal()) { - if (left.isRuntimeValKnown() && left.getRuntimeVal()) { - return ILconstBool.TRUE; - } - if (left.isRuntimeValKnown() && shortCircuitedRight != null - && shortCircuitedRight.isRuntimeValKnown()) { - return ILconstBool.withRuntimeValue(true, shortCircuitedRight.getRuntimeVal()); - } - return ILconstBool.withUnknownRuntimeValue(true); - } - ILconstBool rightBool = (ILconstBool) right.get(); - boolean value = rightBool.getVal(); - if (left.isRuntimeValKnown()) { - if (left.getRuntimeVal()) { - return ILconstBool.withRuntimeValue(value, true); - } - if (rightBool.isRuntimeValKnown()) { - return ILconstBool.withRuntimeValue(value, rightBool.getRuntimeVal()); - } - } else if (rightBool.isRuntimeValKnown() && rightBool.getRuntimeVal()) { - return ILconstBool.withRuntimeValue(value, true); - } - return ILconstBool.withUnknownRuntimeValue(value); - } - - private static ILconstBool evaluateEquality(ILconst left, ILconst right, boolean negated) { - boolean value = left.equals(right) != negated; - if (left instanceof ILconstBool && right instanceof ILconstBool) { - ILconstBool leftBool = (ILconstBool) left; - ILconstBool rightBool = (ILconstBool) right; - if (leftBool.isRuntimeValKnown() && rightBool.isRuntimeValKnown()) { - boolean runtimeValue = (leftBool.getRuntimeVal() == rightBool.getRuntimeVal()) != negated; - return ILconstBool.withRuntimeValue(value, runtimeValue); - } - return ILconstBool.withUnknownRuntimeValue(value); - } - return ILconstBool.instance(value); - } - /** * Reference semantics for Wurst's integer {@code mod}: matches Blizzard.j's * ModuloInteger (truncated remainder, plus divisor if the remainder is diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java index 78507364c..4784504d4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstBool.java @@ -8,50 +8,22 @@ public class ILconstBool extends ILconstAbstract { private final boolean val; - // Tracks the corresponding runtime value while compiletime code evaluates lazy initializers. - private final boolean runtimeVal; - private final boolean runtimeValKnown; - public final static ILconstBool FALSE = new ILconstBool(false, false, true); - public final static ILconstBool TRUE = new ILconstBool(true, true, true); + public final static ILconstBool FALSE = new ILconstBool(false); + public final static ILconstBool TRUE = new ILconstBool(true); public static ILconstBool instance(boolean value) { return value ? TRUE : FALSE; } - public static ILconstBool withRuntimeValue(boolean value, boolean runtimeValue) { - if (value == runtimeValue) { - return instance(value); - } - return new ILconstBool(value, runtimeValue, true); - } - - public static ILconstBool withUnknownRuntimeValue(boolean value) { - return new ILconstBool(value, false, false); - } - - private ILconstBool(boolean val, boolean runtimeVal, boolean runtimeValKnown) { - this.val = val; - this.runtimeVal = runtimeVal; - this.runtimeValKnown = runtimeValKnown; + private ILconstBool(boolean b) { + val = b; } public boolean getVal() { return val; } - public boolean getRuntimeVal() { - return runtimeVal; - } - - public boolean isRuntimeValKnown() { - return runtimeValKnown; - } - - public boolean isKnownToDifferAtRuntime() { - return runtimeValKnown && val != runtimeVal; - } - @Override public String print() { return val ? "true" : "false"; @@ -63,15 +35,12 @@ public WurstType getType() { } public ILconst negate() { - if (!runtimeValKnown) { - return withUnknownRuntimeValue(!val); - } - return withRuntimeValue(!val, !runtimeVal); + return val ? FALSE : TRUE; } @Override public boolean isEqualTo(ILconst other) { - return other instanceof ILconstBool && val == ((ILconstBool) other).val; + return other == this; } @Override 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 cd8eec586..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 @@ -8,19 +8,15 @@ import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.intermediatelang.*; import de.peeeq.wurstscript.jassIm.*; -import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; import de.peeeq.wurstscript.translation.imtranslation.ImPrinter; import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.Utils; import org.eclipse.jdt.annotation.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -82,13 +78,7 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin final ImExprs arguments = e.getArguments(); WurstOperator op = e.getOp(); if (arguments.size() == 2 && op.isBinaryOp()) { - ILconst left = arguments.get(0).evaluate(globalState, localState); - ImExpr right = arguments.get(1); - ILconstBool shortCircuitedRight = right instanceof ImBoolVal - ? ILconstBool.instance(((ImBoolVal) right).getValB()) - : evaluateSkippedRuntimeOperand(op, left, right, globalState, localState); - return op.evaluateBinaryOperator(left, - () -> evaluateRightOperand(op, left, right, globalState, localState), shortCircuitedRight); + return op.evaluateBinaryOperator(arguments.get(0).evaluate(globalState, localState), () -> arguments.get(1).evaluate(globalState, localState)); } else if (arguments.size() == 1 && op.isUnaryOp()) { return op.evaluateUnaryOperator(arguments.get(0).evaluate(globalState, localState)); } else { @@ -96,146 +86,6 @@ public static ILconst eval(ImOperatorCall e, final ProgramState globalState, fin } } - private static @Nullable ILconstBool evaluateSkippedRuntimeOperand(WurstOperator op, ILconst left, ImExpr right, - ProgramState globalState, LocalState localState) { - if (!(left instanceof ILconstBool)) { - return null; - } - ILconstBool leftBool = (ILconstBool) left; - boolean runtimeEvaluatesRight = leftBool.isRuntimeValKnown() - && ((op == WurstOperator.AND && !leftBool.getVal() && leftBool.getRuntimeVal()) - || (op == WurstOperator.OR && leftBool.getVal() && !leftBool.getRuntimeVal())); - if (!runtimeEvaluatesRight) { - return null; - } - - return evaluateRuntimeBooleanExpression(right, globalState, localState); - } - - static ILconstBool refineRuntimeCondition(ImExpr condition, ILconstBool compiletimeValue, - ProgramState globalState, LocalState localState) { - ILconstBool runtimeValue = evaluateRuntimeBooleanExpression(condition, globalState, localState); - if (runtimeValue == null) { - return compiletimeValue; - } - return ILconstBool.withRuntimeValue(compiletimeValue.getVal(), runtimeValue.getVal()); - } - - private static @Nullable ILconstBool evaluateRuntimeBooleanExpression(ImExpr expression, - ProgramState globalState, - LocalState localState) { - SideEffectAnalyzer effects = globalState.getSideEffectAnalyzer(); - if (!effects.calledNatives(expression).isEmpty()) { - return null; - } - Set usedVariables = effects.usedVariables(expression); - LocalState runtimeLocals = new LocalState(); - for (ImVar variable : usedVariables) { - if (variable.isGlobal()) { - continue; - } - ILconst value = localState.getVal(variable); - if (value == null) { - return null; - } - ILconst runtimeValue = runtimeProbeValue(localState.getRuntimeVal(variable)); - if (runtimeValue == null) { - return null; - } - runtimeLocals.setVal(variable, runtimeValue); - } - - try (ProgramState runtimeState = new ProgramState(globalState.getGui(), globalState.getProg(), false)) { - Set initializedGlobals = Collections.newSetFromMap(new IdentityHashMap<>()); - Set initializingGlobals = Collections.newSetFromMap(new IdentityHashMap<>()); - for (ImVar variable : usedVariables) { - if (variable.isGlobal() - && !prepareRuntimeProbeGlobal(variable, globalState, runtimeState, effects, - initializedGlobals, initializingGlobals)) { - return null; - } - } - ILconst runtimeValue = expression.evaluate(runtimeState, runtimeLocals); - return runtimeValue instanceof ILconstBool ? (ILconstBool) runtimeValue : null; - } - } - - private static boolean prepareRuntimeProbeGlobal(ImVar variable, ProgramState sourceState, - ProgramState runtimeState, - SideEffectAnalyzer effects, Set initialized, - Set initializing) { - if (isMagicCompiletimeConstant(variable) || initialized.contains(variable)) { - return true; - } - if (isMagicFunctionsConstant(variable, "isLua")) { - ILconst runtimeValue = runtimeProbeValue(sourceState.getVal(variable)); - if (runtimeValue == null) { - return false; - } - runtimeState.setValUntracked(variable, runtimeValue); - initialized.add(variable); - return true; - } - if (sourceState.wasWrittenWhileSuppressed(variable)) { - return false; - } - if (!initializing.add(variable)) { - return false; - } - List initializers = runtimeState.getProg().getGlobalInits().get(variable); - if (initializers == null || initializers.isEmpty()) { - initializing.remove(variable); - return false; - } - ImExpr initializer = initializers.get(0).getRight(); - if (!effects.calledNatives(initializer).isEmpty()) { - initializing.remove(variable); - return false; - } - for (ImVar dependency : effects.usedVariables(initializer)) { - if (dependency.isGlobal() - && !prepareRuntimeProbeGlobal(dependency, sourceState, runtimeState, effects, - initialized, initializing)) { - initializing.remove(variable); - return false; - } - } - ILconst value = runtimeProbeValue(initializer.evaluate(runtimeState, new LocalState())); - if (value == null) { - initializing.remove(variable); - return false; - } - runtimeState.setValUntracked(variable, value); - initializing.remove(variable); - initialized.add(variable); - return true; - } - - private static @Nullable ILconst runtimeProbeValue(@Nullable ILconst value) { - if (value instanceof ILconstBool) { - ILconstBool boolValue = (ILconstBool) value; - return boolValue.isRuntimeValKnown() ? ILconstBool.instance(boolValue.getRuntimeVal()) : null; - } - if (value instanceof ILconstNum || value instanceof ILconstString || value instanceof ILconstNull) { - return value; - } - return null; - } - - private static ILconst evaluateRightOperand(WurstOperator op, ILconst left, ImExpr right, - ProgramState globalState, LocalState localState) { - if (globalState.writesAreSuppressed() && left instanceof ILconstBool) { - ILconstBool leftBool = (ILconstBool) left; - boolean compiletimeOnly = leftBool.isRuntimeValKnown() - && ((op == WurstOperator.AND && leftBool.getVal() && !leftBool.getRuntimeVal()) - || (op == WurstOperator.OR && !leftBool.getVal() && leftBool.getRuntimeVal())); - if (compiletimeOnly) { - return globalState.evaluateWithTrackedWrites(() -> right.evaluate(globalState, localState)); - } - } - return right.evaluate(globalState, localState); - } - public static ILconst eval(ImRealVal e, ProgramState globalState, LocalState localState) { return new ILconstReal(e.getValR()); } @@ -280,7 +130,7 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l ImVar var = e.getVar(); if (var.isGlobal()) { if (isMagicCompiletimeConstant(var)) { - return ILconstBool.withRuntimeValue(globalState.isCompiletime(), false); + return ILconstBool.instance(globalState.isCompiletime()); } ILconst r = globalState.getVal(var); if (r == null) { @@ -299,13 +149,9 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l } private static boolean isMagicCompiletimeConstant(ImVar var) { - return isMagicFunctionsConstant(var, "compiletime"); - } - - private static boolean isMagicFunctionsConstant(ImVar var, String name) { if (var.getTrace() instanceof VarDef) { VarDef varDef = (VarDef) var.getTrace(); - if (varDef.getName().equals(name)) { + if (varDef.getName().equals("compiletime")) { PackageOrGlobal nearestPackage = varDef.attrNearestPackage(); if (nearestPackage instanceof WPackage) { WPackage p = (WPackage) nearestPackage; @@ -470,11 +316,7 @@ public static ILaddress evaluateLvalue(ImVarAccess va, ProgramState globalState, return new ILaddress() { @Override public void set(ILconst value) { - if (!v.isGlobal() && globalState.isInCompiletimeOnlyPath()) { - localState.setValCompiletimeOnly(v, value); - } else { - state.setVal(v, value); - } + state.setVal(v, value); } @Override diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java index 80793897c..4f6f450f7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java @@ -1,21 +1,14 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; import de.peeeq.wurstscript.intermediatelang.ILconst; -import de.peeeq.wurstscript.intermediatelang.ILconstBool; -import de.peeeq.wurstscript.jassIm.ImVar; import org.eclipse.jdt.annotation.Nullable; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Set; - -/** Local interpreter values plus their runtime counterparts during compiletime evaluation. */ +/** + * Unchanged API. No eager map allocations unless you actually set/get vars/arrays. + */ public class LocalState extends State { private @Nullable ILconst returnVal; - private final Map runtimeValues = new IdentityHashMap<>(); - private final Set unknownRuntimeValues = Collections.newSetFromMap(new IdentityHashMap<>()); public LocalState() { // no eager allocations @@ -25,53 +18,6 @@ public LocalState(ILconst returnVal) { this.returnVal = returnVal; } - @Override - public void setVal(ImVar v, ILconst val) { - super.setVal(v, val); - if (val instanceof ILconstBool) { - ILconstBool boolVal = (ILconstBool) val; - if (!boolVal.isRuntimeValKnown()) { - runtimeValues.remove(v); - unknownRuntimeValues.add(v); - return; - } - runtimeValues.put(v, ILconstBool.instance(boolVal.getRuntimeVal())); - } else { - runtimeValues.put(v, val); - } - unknownRuntimeValues.remove(v); - } - - public void setValCompiletimeOnly(ImVar v, ILconst val) { - super.setVal(v, val); - if (!runtimeValues.containsKey(v)) { - unknownRuntimeValues.add(v); - } - } - - @Override - public @Nullable ILconst getVal(ImVar v) { - ILconst val = super.getVal(v); - if (!(val instanceof ILconstBool)) { - // Other IL constants cannot carry a second value. Conditions that consume them are - // re-evaluated with getRuntimeVal() by EvaluateExpr.refineRuntimeCondition instead. - return val; - } - if (unknownRuntimeValues.contains(v)) { - return ILconstBool.withUnknownRuntimeValue(((ILconstBool) val).getVal()); - } - ILconst runtimeVal = runtimeValues.get(v); - if (runtimeVal instanceof ILconstBool) { - return ILconstBool.withRuntimeValue( - ((ILconstBool) val).getVal(), ((ILconstBool) runtimeVal).getVal()); - } - return val; - } - - public @Nullable ILconst getRuntimeVal(ImVar v) { - return unknownRuntimeValues.contains(v) ? null : runtimeValues.get(v); - } - public @Nullable ILconst getReturnVal() { return returnVal; } 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 9c6b90c5c..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 @@ -8,7 +8,6 @@ import de.peeeq.datastructures.Partitions; import de.peeeq.wurstscript.gui.WurstGui; import de.peeeq.wurstscript.intermediatelang.*; -import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.parser.WPos; import de.peeeq.wurstscript.translation.imtojass.ImAttrType; @@ -20,7 +19,6 @@ import java.io.PrintStream; import java.util.*; -import java.util.function.Supplier; public class ProgramState extends State implements AutoCloseable { @@ -32,7 +30,6 @@ public class ProgramState extends State implements AutoCloseable { private final Object2ObjectOpenHashMap nativeProviderByFunc = new Object2ObjectOpenHashMap<>(); private final Set missingNativeFuncs = new HashSet<>(); private ImProg prog; - private final SideEffectAnalyzer sideEffectAnalyzer; private final Map classKeyLookup = new HashMap<>(); private final Map objectIdSpaces = new HashMap<>(); private final Int2ObjectOpenHashMap handleMap = new Int2ObjectOpenHashMap<>(); @@ -43,7 +40,6 @@ public class ProgramState extends State implements AutoCloseable { private final Map genericStaticOwner = new HashMap<>(); private final Set modifiedScalars = Collections.newSetFromMap(new IdentityHashMap<>()); - private final Set suppressedWrites = Collections.newSetFromMap(new IdentityHashMap<>()); private final Set modifiedGenericScalars = new HashSet<>(); private final Map> genericScalarTypeArguments = new HashMap<>(); private final Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); @@ -53,8 +49,6 @@ public class ProgramState extends State implements AutoCloseable { private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); private int untrackedWriteDepth; - private int trackedWriteDepth; - private final Deque trackedLoopIterations = new ArrayDeque<>(); private static boolean containsTypeVariable(ImType type) { return type.match(new ImType.Matcher() { @@ -102,7 +96,6 @@ public ProgramState(WurstGui gui, ImProg prog, boolean isCompiletime) { this.gui = gui; this.prog = prog; this.isCompiletime = isCompiletime; - this.sideEffectAnalyzer = new SideEffectAnalyzer(prog); buildClassKeyLookup(); identifyGenericStaticGlobals(); @@ -692,11 +685,9 @@ private static String vid(ImVar v) { @Override public void setVal(ImVar v, ILconst val) { - boolean trackWrite = writesAreTracked(); + boolean trackWrite = untrackedWriteDepth == 0; if (trackWrite) { modifiedScalars.add(v); - } else { - suppressedWrites.add(v); } String key = genericStaticKey(v); if (key != null) { @@ -748,6 +739,12 @@ 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 { @@ -757,65 +754,6 @@ public ILconst evaluateUntracked(ImExpr expr, LocalState localState) { } } - void runWithTrackedWrites(Runnable action) { - trackedWriteDepth++; - try { - action.run(); - } finally { - trackedWriteDepth--; - } - } - - T evaluateWithTrackedWrites(Supplier action) { - trackedWriteDepth++; - try { - return action.get(); - } finally { - trackedWriteDepth--; - } - } - - void beginLoopIteration() { - trackedLoopIterations.push(false); - } - - void trackCurrentLoopIterationWrites() { - if (!trackedLoopIterations.pop()) { - trackedLoopIterations.push(true); - trackedWriteDepth++; - } else { - trackedLoopIterations.push(true); - } - } - - void endLoopIteration() { - if (trackedLoopIterations.pop()) { - trackedWriteDepth--; - } - } - - private boolean writesAreTracked() { - // A compiletime-dependent branch cancels one enclosing lazy-initializer suppression scope. - // A nested lazy initializer therefore becomes untracked again until its own such branch. - return untrackedWriteDepth == 0 || trackedWriteDepth >= untrackedWriteDepth; - } - - boolean writesAreSuppressed() { - return !writesAreTracked(); - } - - boolean wasWrittenWhileSuppressed(ImVar var) { - return suppressedWrites.contains(var); - } - - SideEffectAnalyzer getSideEffectAnalyzer() { - return sideEffectAnalyzer; - } - - boolean isInCompiletimeOnlyPath() { - return trackedWriteDepth > 0; - } - public boolean isCompiletime() { return isCompiletime; @@ -866,8 +804,7 @@ protected ILconstArray getArray(ImVar v) { @Override public void setArrayVal(ImVar v, List indexes, ILconst val) { - if (!writesAreTracked()) { - suppressedWrites.add(v); + if (untrackedWriteDepth > 0) { setArrayValUntracked(v, indexes, val); return; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index 6cecf9658..cce66cc9a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -17,29 +17,17 @@ public static void run(ImExpr e, ProgramState globalState, LocalState localState public static void run(ImExitwhen s, ProgramState globalState, LocalState localState) { ILconstBool c = (ILconstBool) s.getCondition().evaluate(globalState, localState); - if (globalState.writesAreSuppressed()) { - c = EvaluateExpr.refineRuntimeCondition(s.getCondition(), c, globalState, localState); - } if (c.getVal()) { throw ExitwhenException.instance(); } - if (globalState.writesAreSuppressed() && c.isKnownToDifferAtRuntime()) { - globalState.trackCurrentLoopIterationWrites(); - } } public static void run(ImIf s, ProgramState globalState, LocalState localState) { ILconstBool c = (ILconstBool) s.getCondition().evaluate(globalState, localState); - if (globalState.writesAreSuppressed()) { - c = EvaluateExpr.refineRuntimeCondition(s.getCondition(), c, globalState, localState); - } - ImStmts selectedBlock = c.getVal() ? s.getThenBlock() : s.getElseBlock(); - // Runtime repeats ordinary lazy-initializer branches, but not a branch selected using - // MagicFunctions.compiletime. Preserve writes from the latter for state migration. - if (globalState.writesAreSuppressed() && c.isKnownToDifferAtRuntime()) { - globalState.runWithTrackedWrites(() -> selectedBlock.runStatements(globalState, localState)); + if (c.getVal()) { + s.getThenBlock().runStatements(globalState, localState); } else { - selectedBlock.runStatements(globalState, localState); + s.getElseBlock().runStatements(globalState, localState); } } @@ -50,12 +38,7 @@ public static void run(ImLoop s, ProgramState globalState, LocalState localState if (Thread.currentThread().isInterrupted()) { throw new InterpreterException(globalState, "Execution interrupted"); } - globalState.beginLoopIteration(); - try { - s.getBody().runStatements(globalState, localState); - } finally { - globalState.endLoopIteration(); - } + s.getBody().runStatements(globalState, localState); } } catch (ExitwhenException e) { // end of loop 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 e07420bdf..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 @@ -174,177 +174,6 @@ public void testLazyScalarInitializerSideEffectsAreNotReplayed() { " testSuccess()"); } - @Test - public void testCompiletimeOnlyLazyInitializerSideEffectsAreReplayed() { - test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) - .lines("package MagicFunctions", - "public constant compiletime = false", - "endpackage", - "package Test", - "import MagicFunctions", - "native testSuccess()", - "int runtimeCounter = 0", - "int compiletimeValue = 0", - "int array compiletimeValues = [0, 0, 0, 0]", - "int nestedCounter = 0", - "function initializeFlag() returns boolean", - " if compiletime", - " let _compiletimeOnly = true", - " return true", - "boolean flag = initializeFlag()", - "function initializeNested() returns int", - " if flag", - " nestedCounter++", - " return nestedCounter", - "int nestedObserved = initializeNested()", - "function initialize() returns int", - " runtimeCounter++", - " let ct = compiletime", - " if ct", - " compiletimeValue = 42", - " if not not ct", - " compiletimeValues[0] = 7", - " if ct and true", - " compiletimeValues[1] = 8", - " if ct or false", - " compiletimeValues[2] = 9", - " if ct == true", - " compiletimeValues[3] = 10", - " return runtimeCounter", - "int observed = initialize()", - "@compiletime function fill()", - " let _snapshot = observed", - " let _nestedSnapshot = nestedObserved", - "init", - " if runtimeCounter == 1 and observed == 1 and compiletimeValue == 42 and compiletimeValues[0] == 7 and compiletimeValues[1] == 8 and compiletimeValues[2] == 9 and compiletimeValues[3] == 10 and nestedCounter == 1 and nestedObserved == 1", - " testSuccess()"); - } - - @Test - public void testCompiletimeLazyInitializerControlFlowEdges() { - test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) - .lines("package MagicFunctions", - "public constant compiletime = false", - "public constant isLua = false", - "endpackage", - "package Test", - "import MagicFunctions", - "native testSuccess()", - "int conditionCounter = 0", - "function mark() returns boolean", - " conditionCounter++", - " return true", - "function initializeCondition() returns int", - " if compiletime and mark()", - " skip", - " return conditionCounter", - "int conditionObserved = initializeCondition()", - "int unresolvedCounter = 0", - "function runtimeFalse() returns boolean", - " return false", - "function initializeUnresolved() returns int", - " if not compiletime and runtimeFalse()", - " skip", - " else", - " unresolvedCounter++", - " return unresolvedCounter", - "int unresolvedObserved = initializeUnresolved()", - "int oppositeCounter = 0", - "function initializeOpposite() returns int", - " if compiletime or runtimeFalse()", - " oppositeCounter++", - " return oppositeCounter", - "int oppositeObserved = initializeOpposite()", - "int stableLocalCounter = 0", - "function initializeStableLocal(int guard) returns int", - " if compiletime or guard == 0", - " stableLocalCounter++", - " return stableLocalCounter", - "int stableLocalObserved = initializeStableLocal(1)", - "boolean stableGlobalGuard = false", - "int stableGlobalCounter = 0", - "function initializeStableGlobal() returns int", - " if compiletime or stableGlobalGuard", - " stableGlobalCounter++", - " return stableGlobalCounter", - "int stableGlobalObserved = initializeStableGlobal()", - "int divergentLocalCounter = 0", - "function compiletimeLocalGuard() returns boolean", - " var result = false", - " if compiletime", - " result = true", - " return result", - "function initializeDivergentLocal() returns int", - " if compiletimeLocalGuard()", - " divergentLocalCounter++", - " return divergentLocalCounter", - "int divergentLocalObserved = initializeDivergentLocal()", - "int divergentIntCounter = 0", - "function initializeDivergentInt() returns int", - " var result = 0", - " if compiletime", - " result = 1", - " if result == 1", - " divergentIntCounter++", - " return divergentIntCounter", - "int divergentIntObserved = initializeDivergentInt()", - "int luaTargetCounter = 0", - "function initializeLuaTarget() returns int", - " if compiletime or isLua", - " luaTargetCounter++", - " return luaTargetCounter", - "int luaTargetObserved = initializeLuaTarget()", - "int localArrayCounter = 0", - "function initializeLocalArray() returns int", - " let flags = [1]", - " if flags[0] == 0", - " skip", - " else", - " localArrayCounter++", - " return localArrayCounter", - "int localArrayObserved = initializeLocalArray()", - "boolean priorWriteGuard = false", - "int priorWriteCounter = 0", - "function initializePriorWrite() returns int", - " priorWriteGuard = true", - " if priorWriteGuard", - " priorWriteCounter++", - " return priorWriteCounter", - "int priorWriteObserved = initializePriorWrite()", - "int array priorArrayGuard = [0]", - "int priorArrayCounter = 0", - "function initializePriorArrayWrite() returns int", - " priorArrayGuard[0] = 1", - " if priorArrayGuard[0] == 1", - " priorArrayCounter++", - " return priorArrayCounter", - "int priorArrayObserved = initializePriorArrayWrite()", - "int loopCounter = 0", - "function initializeLoop() returns int", - " var i = 0", - " while compiletime and i == 0", - " loopCounter++", - " i++", - " return loopCounter", - "int loopObserved = initializeLoop()", - "@compiletime function fill()", - " let _conditionSnapshot = conditionObserved", - " let _unresolvedSnapshot = unresolvedObserved", - " let _oppositeSnapshot = oppositeObserved", - " let _stableLocalSnapshot = stableLocalObserved", - " let _stableGlobalSnapshot = stableGlobalObserved", - " let _divergentLocalSnapshot = divergentLocalObserved", - " let _divergentIntSnapshot = divergentIntObserved", - " let _luaTargetSnapshot = luaTargetObserved", - " let _localArraySnapshot = localArrayObserved", - " let _priorWriteSnapshot = priorWriteObserved", - " let _priorArraySnapshot = priorArrayObserved", - " let _loopSnapshot = loopObserved", - "init", - " if conditionCounter == 1 and conditionObserved == 1 and unresolvedCounter == 1 and unresolvedObserved == 1 and oppositeCounter == 1 and oppositeObserved == 1 and stableLocalCounter == 1 and stableLocalObserved == 1 and stableGlobalCounter == 1 and stableGlobalObserved == 1 and divergentLocalCounter == 1 and divergentLocalObserved == 1 and divergentIntCounter == 1 and divergentIntObserved == 1 and luaTargetCounter == 1 and luaTargetObserved == 1 and localArrayCounter == 1 and localArrayObserved == 1 and priorWriteCounter == 1 and priorWriteObserved == 1 and priorArrayCounter == 1 and priorArrayObserved == 1 and loopCounter == 1 and loopObserved == 1", - " testSuccess()"); - } - @Test public void testCompiletimeScalarRuntimeWriteRemainsAuthoritative() { test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true)