From 83e109ed3e0e6c33be8f2ddf71e66775ba9d47cb Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 31 Jul 2026 22:18:33 +0200 Subject: [PATCH 01/16] Migrate compiletime array state --- .../wurstio/CompiletimeFunctionRunner.java | 32 ++++++++++++++++ .../intermediatelang/ILconstArray.java | 9 +++++ .../interpreter/ProgramState.java | 5 +++ .../wurstscript/tests/CompiletimeTests.java | 38 ++++++++++++++++++- 4 files changed, 82 insertions(+), 2 deletions(-) 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 8defd87c2..558c629cb 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 @@ -113,6 +113,9 @@ public void run() { execute(toExecute); long tExecuted = System.nanoTime(); + if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { + emitCompiletimeState(); + } if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { interpreter.writebackGlobalState(isInjectObjects()); @@ -396,6 +399,10 @@ public ImVar initFor(IlConstHandle a) { }; private ImExpr constantToExpr(Element trace, ILconst value) { + return constantToExpr(trace, value, null); + } + + private ImExpr constantToExpr(Element trace, ILconst value, @Nullable ImType expectedType) { if (value instanceof ILconstBool) { return JassIm.ImBoolVal(((ILconstBool) value).getVal()); } else if (value instanceof ILconstInt) { @@ -535,6 +542,31 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } + private void emitCompiletimeState() { + for (ImVar var : imProg.getGlobals()) { + if (!(var.getType() instanceof ImArrayLikeType)) { + continue; + } + ILconstArray values = globalState.getArrayValue(var); + emitCompiletimeArrayEntries(var, values, new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType()); + } + } + + private void emitCompiletimeArrayEntries(ImVar var, ILconstArray values, List indexes, ImType entryType) { + for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry : values.entries()) { + List nextIndexes = new ArrayList<>(indexes); + nextIndexes.add(JassIm.ImIntVal(entry.getIntKey())); + if (entry.getValue() instanceof ILconstArray && entryType instanceof ImArrayLikeType) { + emitCompiletimeArrayEntries(var, (ILconstArray) entry.getValue(), nextIndexes, + ((ImArrayLikeType) entryType).getEntryType()); + } else { + addCompiletimeStateInit(JassIm.ImSet(var.getTrace(), + JassIm.ImVarArrayAccess(var.getTrace(), var, JassIm.ImExprs(nextIndexes)), + constantToExpr(var.getTrace(), entry.getValue(), entryType))); + } + } + } + /** * Stores a hashtable value in a compiletime expression * by generating the respective native calls diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstArray.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstArray.java index 730406a77..26e9ec449 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstArray.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstArray.java @@ -5,6 +5,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; public class ILconstArray extends ILconstAbstract { @@ -72,6 +74,13 @@ public ILconst get(int index) { return v; } + /** Returns the explicitly stored entries in deterministic index order. */ + public List> entries() { + List> result = new ArrayList<>(values.int2ObjectEntrySet()); + result.sort(java.util.Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey)); + return result; + } + private void checkIndex(int index) { if (index < 0) { throw new InterpreterException("Array index " + index + " was negative."); 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 00c8917d4..c7207fea9 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 @@ -762,6 +762,11 @@ protected ILconstArray getArray(ImVar v) { return r; } + /** Snapshot of an array's explicitly initialized entries for compiletime migration. */ + public ILconstArray getArrayValue(ImVar v) { + return getArray(v); + } + public Collection getAllObjects() { List values = new ArrayList<>(); 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 02af4d00a..aa8a2ae76 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 @@ -62,12 +62,26 @@ public void testCompiletimeArray() { " testSuccess()"); } + @Test + public void testCompiletimeArrayState() { + test().executeProg(true) + .runCompiletimeFunctions(true) + .executeProgOnlyAfterTransforms() + .lines("package Test", + "native testSuccess()", + "int array source", + "@compiletime function fill()", + " source[3] = 42", + "init", + " if source[3] == 42", + " testSuccess()"); + } + @Test public void testCompiletimeHashtable() { - test().executeProg(true) + test() .runCompiletimeFunctions(true) - .executeProgOnlyAfterTransforms() .lines("type agent extends handle", "type hashtable extends agent", "package Test", @@ -177,6 +191,26 @@ public void testPersistCompiletimeClass() { " testSuccess()"); } + @Test + public void testPersistCompiletimeNewGenericClass() { + // Translation is the assertion here: executing the synthesized generic + // runtime global through the interpreter still needs a separate attachment fix. + test() + .runCompiletimeFunctions(true) + .lines("package Test", + "class PureMap", + " T value", + " function put(T value)", + " this.value = value", + " function get() returns T", + " return value", + "function compiletime(T value) returns T", + " return value", + "PureMap map = compiletime(new PureMap)", + "@compiletime function populate()", + " map.put(42)"); + } + @Test public void testPersistCompiletimeClassCycle() { test().executeProg(true) From 1c02d4f2a617dfe1b21b94ef567e4c3671b47fe0 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 31 Jul 2026 22:49:20 +0200 Subject: [PATCH 02/16] Skip unsupported compiletime array entries --- .../wurstio/CompiletimeFunctionRunner.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 558c629cb..bb681b2ce 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 @@ -559,7 +559,7 @@ private void emitCompiletimeArrayEntries(ImVar var, ILconstArray values, List Date: Fri, 31 Jul 2026 23:05:06 +0200 Subject: [PATCH 03/16] Avoid concurrent compiletime global updates --- .../main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 bb681b2ce..7849cc158 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 @@ -543,7 +543,9 @@ private void addCompiletimeStateInit(ImStmt stmt) { } private void emitCompiletimeState() { - for (ImVar var : imProg.getGlobals()) { + // constantToExpr may materialize object handles as additional globals. + // Iterate over a snapshot to avoid modifying the collection in-flight. + for (ImVar var : new ArrayList<>(imProg.getGlobals())) { if (!(var.getType() instanceof ImArrayLikeType)) { continue; } From 96ffe1081a9fcb9d22add27e9bddc493337f62f6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 31 Jul 2026 23:12:29 +0200 Subject: [PATCH 04/16] Fix compiletime array replay --- .../wurstio/CompiletimeFunctionRunner.java | 43 ++++++++++++++++++- .../wurstscript/tests/CompiletimeTests.java | 8 ++-- 2 files changed, 44 insertions(+), 7 deletions(-) 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 7849cc158..157fef6a7 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 @@ -411,6 +411,8 @@ private ImExpr constantToExpr(Element trace, ILconst value, @Nullable ImType exp return JassIm.ImRealVal("" + ((ILconstReal) value).getVal()); } else if (value instanceof ILconstString) { return JassIm.ImStringVal(((ILconstString) value).getVal()); + } else if (value instanceof ILconstNull) { + return ImHelper.nullExpr(); } else if (value instanceof ILconstTuple) { List list = new ArrayList<>(); for (ILconst e : ((ILconstTuple) value).values()) { @@ -501,6 +503,7 @@ private CompiletimeObjectInit(ILconstObject object, ImVar targetVar) { } private ImFunction compiletimeStateInitFunction = null; + private ImFunction compiletimeArrayStateInitFunction = null; private ImFunction getCompiletimeStateInitFunction() { ImFunction res = this.compiletimeStateInitFunction; @@ -542,6 +545,42 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } + private void addCompiletimeArrayStateInit(ImStmt stmt) { + getCompiletimeArrayStateInitFunction().getBody().add(stmt); + } + + private ImFunction getCompiletimeArrayStateInitFunction() { + if (compiletimeArrayStateInitFunction == null) { + Element trace = imProg.getTrace(); + ImFunction res = JassIm.ImFunction(trace, "initCompiletimeArrayState", JassIm.ImTypeVars(), JassIm.ImVars(), + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + imProg.getFunctions().add(res); + compiletimeArrayStateInitFunction = res; + ImFunctionCall call = JassIm.ImFunctionCall(trace, res, JassIm.ImTypeArguments(), JassIm.ImExprs(), true, CallType.NORMAL); + ListIterator iterator = translator.getMainFunc().getBody().listIterator(); + while (iterator.hasNext()) { + ImStmt stmt = iterator.next(); + if (stmt instanceof ImFunctionCall + && ((ImFunctionCall) stmt).getFunc().getName().equals("DestroyTrigger")) { + iterator.previous(); + iterator.add(call); + return compiletimeArrayStateInitFunction; + } + } + ListIterator endIterator = translator.getMainFunc().getBody().listIterator(); + while (endIterator.hasNext()) { + ImStmt stmt = endIterator.next(); + if (stmt instanceof ImReturn) { + endIterator.previous(); + endIterator.add(call); + return compiletimeArrayStateInitFunction; + } + } + translator.getMainFunc().getBody().add(call); + } + return compiletimeArrayStateInitFunction; + } + private void emitCompiletimeState() { // constantToExpr may materialize object handles as additional globals. // Iterate over a snapshot to avoid modifying the collection in-flight. @@ -562,7 +601,7 @@ private void emitCompiletimeArrayEntries(ImVar var, ILconstArray values, List Date: Sat, 1 Aug 2026 09:52:19 +0200 Subject: [PATCH 05/16] Replay only modified compiletime arrays --- .../java/de/peeeq/wurstio/CompiletimeFunctionRunner.java | 5 ++++- .../wurstscript/intermediatelang/interpreter/State.java | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) 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 157fef6a7..957c8051d 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 @@ -584,7 +584,10 @@ private ImFunction getCompiletimeArrayStateInitFunction() { private void emitCompiletimeState() { // constantToExpr may materialize object handles as additional globals. // Iterate over a snapshot to avoid modifying the collection in-flight. - for (ImVar var : new ArrayList<>(imProg.getGlobals())) { + for (ImVar var : new ArrayList<>(globalState.getModifiedArrays())) { + if (!imProg.getGlobals().contains(var)) { + continue; + } if (!(var.getType() instanceof ImArrayLikeType)) { continue; } 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 6757fee07..73a072817 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 @@ -10,6 +10,8 @@ import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.HashSet; /** * Lazily allocates internal maps ONLY when needed. @@ -19,6 +21,7 @@ public abstract class State { // in State: private @Nullable Object2ObjectOpenHashMap values; private @Nullable Object2ObjectOpenHashMap arrayValues; + private final Set modifiedArrays = new HashSet<>(); private Object2ObjectOpenHashMap ensureValues() { @@ -77,6 +80,7 @@ static ILconstArray createArrayConstantFromType(ImType vType) { } public void setArrayVal(ImVar v, List indexes, ILconst val) { + modifiedArrays.add(v); ILconstArray ar = getArray(v); for (int i = 0; i < indexes.size() - 1; i++) { ar = (ILconstArray) ar.get(indexes.get(i)); @@ -84,6 +88,10 @@ public void setArrayVal(ImVar v, List indexes, ILconst val) { ar.set(indexes.get(indexes.size() - 1), val); } + public Set getModifiedArrays() { + return modifiedArrays; + } + public @Nullable ILconst getArrayVal(ImVar v, List indexes) { ILconstArray ar = getArray(v); for (int i = 0; i < indexes.size() - 1; i++) { From 1431ff6c10cbb35b300a75e4e49fd95c76506bde Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 09:53:28 +0200 Subject: [PATCH 06/16] Preserve generic compiletime arrays --- .../wurstio/CompiletimeFunctionRunner.java | 5 ++-- .../interpreter/ProgramState.java | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) 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 957c8051d..4b9f34814 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 @@ -591,8 +591,9 @@ private void emitCompiletimeState() { if (!(var.getType() instanceof ImArrayLikeType)) { continue; } - ILconstArray values = globalState.getArrayValue(var); - emitCompiletimeArrayEntries(var, values, new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType()); + for (ILconstArray values : globalState.getArrayValues(var)) { + emitCompiletimeArrayEntries(var, values, new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType()); + } } } 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 c7207fea9..d704282ba 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 @@ -40,6 +40,7 @@ public class ProgramState extends State implements AutoCloseable { private final Map genericStaticOwner = new HashMap<>(); private final Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); + private final Set modifiedGenericArrays = new HashSet<>(); private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); @@ -762,11 +763,35 @@ protected ILconstArray getArray(ImVar v) { return r; } + @Override + public void setArrayVal(ImVar v, List indexes, ILconst val) { + String key = genericStaticKey(v); + super.setArrayVal(v, indexes, val); + if (key != null) { + modifiedGenericArrays.add(key); + } + } + /** Snapshot of an array's explicitly initialized entries for compiletime migration. */ public ILconstArray getArrayValue(ImVar v) { return getArray(v); } + public Collection getArrayValues(ImVar v) { + String prefix = v.getName() + "|"; + List result = new ArrayList<>(); + for (String key : modifiedGenericArrays) { + if (key.startsWith(prefix)) { + ILconstArray value = genericStaticArrays.get(key); + if (value != null) result.add(value); + } + } + if (result.isEmpty() && genericStaticKey(v) == null) { + result.add(getArray(v)); + } + return result; + } + public Collection getAllObjects() { List values = new ArrayList<>(); From 9e9af20796dd65956a22db5032bfacf91e7b7f11 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 09:54:48 +0200 Subject: [PATCH 07/16] Warn on unsupported array migration --- .../main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java | 3 +++ 1 file changed, 3 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 4b9f34814..17f746323 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 @@ -608,6 +608,9 @@ private void emitCompiletimeArrayEntries(ImVar var, ILconstArray values, List Date: Sat, 1 Aug 2026 10:39:03 +0200 Subject: [PATCH 08/16] Fix compiletime array state replay --- .../wurstio/CompiletimeFunctionRunner.java | 69 +++++++++++-------- .../interpreter/ProgramState.java | 64 +++++++++++++++-- .../wurstscript/tests/CompiletimeTests.java | 20 +++++- 3 files changed, 120 insertions(+), 33 deletions(-) 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 17f746323..28db7b18a 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 @@ -504,6 +504,7 @@ private CompiletimeObjectInit(ILconstObject object, ImVar targetVar) { private ImFunction compiletimeStateInitFunction = null; private ImFunction compiletimeArrayStateInitFunction = null; + private int genericArrayStateInitCounter; private ImFunction getCompiletimeStateInitFunction() { ImFunction res = this.compiletimeStateInitFunction; @@ -545,10 +546,6 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } - private void addCompiletimeArrayStateInit(ImStmt stmt) { - getCompiletimeArrayStateInitFunction().getBody().add(stmt); - } - private ImFunction getCompiletimeArrayStateInitFunction() { if (compiletimeArrayStateInitFunction == null) { Element trace = imProg.getTrace(); @@ -557,26 +554,16 @@ private ImFunction getCompiletimeArrayStateInitFunction() { imProg.getFunctions().add(res); compiletimeArrayStateInitFunction = res; ImFunctionCall call = JassIm.ImFunctionCall(trace, res, JassIm.ImTypeArguments(), JassIm.ImExprs(), true, CallType.NORMAL); - ListIterator iterator = translator.getMainFunc().getBody().listIterator(); - while (iterator.hasNext()) { - ImStmt stmt = iterator.next(); - if (stmt instanceof ImFunctionCall - && ((ImFunctionCall) stmt).getFunc().getName().equals("DestroyTrigger")) { - iterator.previous(); - iterator.add(call); - return compiletimeArrayStateInitFunction; - } - } - ListIterator endIterator = translator.getMainFunc().getBody().listIterator(); - while (endIterator.hasNext()) { - ImStmt stmt = endIterator.next(); - if (stmt instanceof ImReturn) { - endIterator.previous(); - endIterator.add(call); + ImFunction globalInitFunc = translator.getGlobalInitFunc(); + ImStmts mainBody = translator.getMainFunc().getBody(); + for (int i = 0; i < mainBody.size(); i++) { + ImStmt stmt = mainBody.get(i); + if (stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc().getName().equals(globalInitFunc.getName())) { + mainBody.add(i + 1, call); return compiletimeArrayStateInitFunction; } } - translator.getMainFunc().getBody().add(call); + mainBody.add(0, call); } return compiletimeArrayStateInitFunction; } @@ -584,28 +571,56 @@ private ImFunction getCompiletimeArrayStateInitFunction() { private void emitCompiletimeState() { // constantToExpr may materialize object handles as additional globals. // Iterate over a snapshot to avoid modifying the collection in-flight. - for (ImVar var : new ArrayList<>(globalState.getModifiedArrays())) { + List modifiedArrays = new ArrayList<>(globalState.getModifiedArrays()); + Map globalOrder = new IdentityHashMap<>(); + for (int i = 0; i < imProg.getGlobals().size(); i++) { + globalOrder.put(imProg.getGlobals().get(i), i); + } + modifiedArrays.sort(Comparator + .comparingInt((ImVar var) -> globalOrder.getOrDefault(var, Integer.MAX_VALUE)) + .thenComparing(ImVar::getName)); + for (ImVar var : modifiedArrays) { if (!imProg.getGlobals().contains(var)) { continue; } if (!(var.getType() instanceof ImArrayLikeType)) { continue; } - for (ILconstArray values : globalState.getArrayValues(var)) { - emitCompiletimeArrayEntries(var, values, new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType()); + for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { + if (state.getTypeArguments().isEmpty()) { + emitCompiletimeArrayEntries(getCompiletimeArrayStateInitFunction(), var, state.getValue(), + new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType()); + } else { + emitCompiletimeGenericArrayState(var, state, ((ImArrayLikeType) var.getType()).getEntryType()); + } } } } - private void emitCompiletimeArrayEntries(ImVar var, ILconstArray values, List indexes, ImType entryType) { + private void emitCompiletimeGenericArrayState(ImVar var, ProgramState.ArrayState state, ImType entryType) { + List typeVars = new ArrayList<>(); + for (int i = 0; i < state.getTypeArguments().size(); i++) { + typeVars.add(JassIm.ImTypeVar("T" + i)); + } + ImFunction replay = JassIm.ImFunction(var.getTrace(), + "initCompiletimeArrayState_" + genericArrayStateInitCounter++, + JassIm.ImTypeVars(typeVars), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), + JassIm.ImStmts(), Collections.emptyList()); + imProg.getFunctions().add(replay); + emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType); + getCompiletimeArrayStateInitFunction().getBody().add(JassIm.ImFunctionCall( + var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); + } + + private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, ImType entryType) { for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry : values.entries()) { List nextIndexes = new ArrayList<>(indexes); nextIndexes.add(JassIm.ImIntVal(entry.getIntKey())); if (entry.getValue() instanceof ILconstArray && entryType instanceof ImArrayLikeType) { - emitCompiletimeArrayEntries(var, (ILconstArray) entry.getValue(), nextIndexes, + emitCompiletimeArrayEntries(target, var, (ILconstArray) entry.getValue(), nextIndexes, ((ImArrayLikeType) entryType).getEntryType()); } else if (isPersistableCompiletimeValue(entry.getValue())) { - addCompiletimeArrayStateInit(JassIm.ImSet(var.getTrace(), + target.getBody().add(JassIm.ImSet(var.getTrace(), JassIm.ImVarArrayAccess(var.getTrace(), var, JassIm.ImExprs(nextIndexes)), constantToExpr(var.getTrace(), entry.getValue(), entryType))); } else { 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 d704282ba..ef92108e5 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 Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); private final Set modifiedGenericArrays = new HashSet<>(); + private final Map> genericArrayTypeArguments = new HashMap<>(); private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); @@ -769,25 +770,78 @@ public void setArrayVal(ImVar v, List indexes, ILconst val) { super.setArrayVal(v, indexes, val); if (key != null) { modifiedGenericArrays.add(key); + genericArrayTypeArguments.computeIfAbsent(key, ignored -> genericStaticTypeArguments(v)); } } + private List genericStaticTypeArguments(ImVar v) { + ImClass owner = genericStaticOwner.get(v); + if (owner == null) { + return Collections.emptyList(); + } + + ImClassType receiver = currentReceiverInstantiationFor(owner); + if (receiver != null && receiver.getClassDef() == owner) { + return copyTypeArguments(receiver.getTypeArguments()); + } + + List result = new ArrayList<>(); + for (ImTypeVar typeVar : owner.getTypeVariables()) { + ImType resolved = resolveType(JassIm.ImTypeVarRef(typeVar)); + if (resolved instanceof ImTypeVarRef) { + return Collections.emptyList(); + } + result.add(JassIm.ImTypeArgument(resolved, Collections.emptyMap())); + } + return result; + } + + private static List copyTypeArguments(ImTypeArguments typeArguments) { + List result = new ArrayList<>(typeArguments.size()); + for (ImTypeArgument typeArgument : typeArguments) { + result.add(typeArgument.copy()); + } + return result; + } + /** Snapshot of an array's explicitly initialized entries for compiletime migration. */ public ILconstArray getArrayValue(ImVar v) { return getArray(v); } - public Collection getArrayValues(ImVar v) { + public static final class ArrayState { + private final ILconstArray value; + private final List typeArguments; + + public ArrayState(ILconstArray value, List typeArguments) { + this.value = value; + this.typeArguments = typeArguments; + } + + public ILconstArray getValue() { + return value; + } + + public List getTypeArguments() { + return typeArguments; + } + } + + public Collection getArrayStates(ImVar v) { String prefix = v.getName() + "|"; - List result = new ArrayList<>(); - for (String key : modifiedGenericArrays) { + List keys = new ArrayList<>(modifiedGenericArrays); + Collections.sort(keys); + List result = new ArrayList<>(); + for (String key : keys) { if (key.startsWith(prefix)) { ILconstArray value = genericStaticArrays.get(key); - if (value != null) result.add(value); + if (value != null) { + result.add(new ArrayState(value, genericArrayTypeArguments.getOrDefault(key, Collections.emptyList()))); + } } } if (result.isEmpty() && genericStaticKey(v) == null) { - result.add(getArray(v)); + result.add(new ArrayState(getArray(v), Collections.emptyList())); } return result; } 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 0d2cfb21d..b929a685a 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 @@ -64,7 +64,7 @@ public void testCompiletimeArray() { @Test public void testCompiletimeArrayState() { - test().runCompiletimeFunctions(true) + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", "int array source", @@ -75,6 +75,24 @@ public void testCompiletimeArrayState() { " testSuccess()"); } + @Test + public void testCompiletimeGenericArrayState() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "class Box", + " static T array store", + " static function set(int index, T value)", + " store[index] = value", + " static function get(int index) returns T", + " return store[index]", + "@compiletime function fill()", + " Box.set(0, 42)", + "init", + " if Box.get(0) == 42", + " testSuccess()"); + } + @Test public void testCompiletimeHashtable() { From 3668a63ba06102f1d6207a5ad1678f96b6d8cfa4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 10:52:09 +0200 Subject: [PATCH 09/16] Strengthen compiletime migration coverage --- .../wurstscript/tests/CompiletimeTests.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) 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 b929a685a..555f0f61c 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 @@ -75,6 +75,19 @@ public void testCompiletimeArrayState() { " testSuccess()"); } + @Test + public void testCompiletimeArrayStateLua() { + test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "int array source", + "@compiletime function fill()", + " source[0] = 42", + "init", + " if source[0] == 42", + " testSuccess()"); + } + @Test public void testCompiletimeGenericArrayState() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) @@ -93,10 +106,28 @@ public void testCompiletimeGenericArrayState() { " testSuccess()"); } + @Test + public void testCompiletimeGenericArrayStateLua() { + test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "class Box", + " static T array store", + " static function set(int index, T value)", + " store[index] = value", + " static function get(int index) returns T", + " return store[index]", + "@compiletime function fill()", + " Box.set(0, 42)", + "init", + " if Box.get(0) == 42", + " testSuccess()"); + } + @Test public void testCompiletimeHashtable() { - test() + test().executeProg(true).executeProgOnlyAfterTransforms() .runCompiletimeFunctions(true) .lines("type agent extends handle", "type hashtable extends agent", From 41debfc45136a70704b65f9ff4b9728c42781ce3 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 11:11:21 +0200 Subject: [PATCH 10/16] Replay arrays after generated initializers --- .../wurstio/CompiletimeFunctionRunner.java | 67 +++++++++++++++++-- .../wurstscript/tests/CompiletimeTests.java | 15 ++++- 2 files changed, 74 insertions(+), 8 deletions(-) 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 28db7b18a..b5863d933 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 @@ -115,6 +115,7 @@ public void run() { if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { emitCompiletimeState(); + insertCompiletimeArrayStateInitCalls(); } if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { @@ -553,19 +554,71 @@ private ImFunction getCompiletimeArrayStateInitFunction() { JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(res); compiletimeArrayStateInitFunction = res; - ImFunctionCall call = JassIm.ImFunctionCall(trace, res, JassIm.ImTypeArguments(), JassIm.ImExprs(), true, CallType.NORMAL); - ImFunction globalInitFunc = translator.getGlobalInitFunc(); + } + return compiletimeArrayStateInitFunction; + } + + private void insertCompiletimeArrayStateInitCalls() { + if (compiletimeArrayStateInitFunction == null) { + return; + } + + Set modifiedArrayInitializers = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImVar var : globalState.getModifiedArrays()) { + for (ImSet initializer : imProg.getGlobalInits().getOrDefault(var, Collections.emptyList())) { + modifiedArrayInitializers.add(initializer); + } + } + + ImFunction globalInitFunction = translator.getGlobalInitFunc(); + boolean insertedIntoPackage = false; + if (!modifiedArrayInitializers.isEmpty() + && insertReplayAfterInitializers(globalInitFunction, modifiedArrayInitializers)) { + insertedIntoPackage = true; + } + for (ImFunction initFunction : translator.initFuncMap.values()) { + if (initFunction.getBody().isEmpty()) { + continue; + } + insertReplayAfterInitializers(initFunction, modifiedArrayInitializers); + insertedIntoPackage = true; + } + + if (!insertedIntoPackage) { ImStmts mainBody = translator.getMainFunc().getBody(); for (int i = 0; i < mainBody.size(); i++) { ImStmt stmt = mainBody.get(i); - if (stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc().getName().equals(globalInitFunc.getName())) { - mainBody.add(i + 1, call); - return compiletimeArrayStateInitFunction; + if (stmt instanceof ImFunctionCall + && ((ImFunctionCall) stmt).getFunc().getName().equals(globalInitFunction.getName())) { + mainBody.add(i + 1, newCompiletimeArrayStateInitCall()); + return; } } - mainBody.add(0, call); + mainBody.add(0, newCompiletimeArrayStateInitCall()); } - return compiletimeArrayStateInitFunction; + } + + private boolean insertReplayAfterInitializers(ImFunction function, Set modifiedArrayInitializers) { + if (function == null || function.getBody().isEmpty()) { + return false; + } + int insertionIndex = -1; + for (int i = 0; i < function.getBody().size(); i++) { + if (function.getBody().get(i) instanceof ImSet + && modifiedArrayInitializers.contains(function.getBody().get(i))) { + insertionIndex = i + 1; + } + } + if (insertionIndex < 0) { + insertionIndex = 0; + } + function.getBody().add(insertionIndex, newCompiletimeArrayStateInitCall()); + return true; + } + + private ImFunctionCall newCompiletimeArrayStateInitCall() { + return JassIm.ImFunctionCall(imProg.getTrace(), compiletimeArrayStateInitFunction, + JassIm.ImTypeArguments(), JassIm.ImExprs(), true, CallType.NORMAL); } private void emitCompiletimeState() { 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 555f0f61c..a4af7c192 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 @@ -75,12 +75,25 @@ public void testCompiletimeArrayState() { " testSuccess()"); } + @Test + public void testCompiletimeArrayStateAfterSourceInitializer() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "int array source = [1]", + "@compiletime function fill()", + " source[0] = 42", + "init", + " if source[0] == 42", + " testSuccess()"); + } + @Test public void testCompiletimeArrayStateLua() { test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) .lines("package Test", "native testSuccess()", - "int array source", + "int array source = [1]", "@compiletime function fill()", " source[0] = 42", "init", From 9594754fdd85d34e8597533f6e037a5114f8a4ce Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 11:28:57 +0200 Subject: [PATCH 11/16] Audit compiletime array migration --- .../wurstio/CompiletimeFunctionRunner.java | 91 +++++++++++++++---- .../interpreter/ProgramState.java | 14 ++- .../wurstscript/tests/CompiletimeTests.java | 61 +++++++++++++ 3 files changed, 147 insertions(+), 19 deletions(-) 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 b5863d933..4eadfadc6 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 @@ -115,7 +115,6 @@ public void run() { if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { emitCompiletimeState(); - insertCompiletimeArrayStateInitCalls(); } if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { @@ -124,6 +123,9 @@ public void run() { long tWriteback = System.nanoTime(); runDelayedActions(); emitCompiletimeObjectAllocs(); + if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { + insertCompiletimeArrayStateInitCalls(); + } long tDelayed = System.nanoTime(); partitionCompiletimeStateInitFunction(); @@ -197,10 +199,15 @@ private static long ms(long nanos) { private void partitionCompiletimeStateInitFunction() { if (compiletimeStateInitFunction == null) { - return; + if (compiletimeArrayStateInitFunction != null) { + FunctionSplitter.splitFunc(translator, compiletimeArrayStateInitFunction); + } + } else { + FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); + if (compiletimeArrayStateInitFunction != null) { + FunctionSplitter.splitFunc(translator, compiletimeArrayStateInitFunction); + } } - - FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); } private boolean isUnitTestMode() { @@ -413,12 +420,17 @@ private ImExpr constantToExpr(Element trace, ILconst value, @Nullable ImType exp } else if (value instanceof ILconstString) { return JassIm.ImStringVal(((ILconstString) value).getVal()); } else if (value instanceof ILconstNull) { - return ImHelper.nullExpr(); + return expectedType == null ? ImHelper.nullExpr() : JassIm.ImNull(expectedType.copy()); } else if (value instanceof ILconstTuple) { List list = new ArrayList<>(); + ImTupleType tupleType = expectedType instanceof ImTupleType ? (ImTupleType) expectedType : null; + int index = 0; for (ILconst e : ((ILconstTuple) value).values()) { - ImExpr imExpr = constantToExpr(trace, e); + ImType elementType = tupleType != null && index < tupleType.getTypes().size() + ? tupleType.getTypes().get(index) : null; + ImExpr imExpr = constantToExpr(trace, e, elementType); list.add(imExpr); + index++; } return JassIm.ImTupleExpr( JassIm.ImExprs( @@ -586,10 +598,19 @@ && insertReplayAfterInitializers(globalInitFunction, modifiedArrayInitializers)) if (!insertedIntoPackage) { ImStmts mainBody = translator.getMainFunc().getBody(); + ImFunction stateInit = compiletimeStateInitFunction; + if (stateInit != null) { + for (int i = 0; i < mainBody.size(); i++) { + ImStmt stmt = mainBody.get(i); + if (stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc() == stateInit) { + mainBody.add(i + 1, newCompiletimeArrayStateInitCall()); + return; + } + } + } for (int i = 0; i < mainBody.size(); i++) { ImStmt stmt = mainBody.get(i); - if (stmt instanceof ImFunctionCall - && ((ImFunctionCall) stmt).getFunc().getName().equals(globalInitFunction.getName())) { + if (stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc() == globalInitFunction) { mainBody.add(i + 1, newCompiletimeArrayStateInitCall()); return; } @@ -624,6 +645,7 @@ private ImFunctionCall newCompiletimeArrayStateInitCall() { private void emitCompiletimeState() { // constantToExpr may materialize object handles as additional globals. // Iterate over a snapshot to avoid modifying the collection in-flight. + Set runtimeArrayWrites = findRuntimeArrayWrites(); List modifiedArrays = new ArrayList<>(globalState.getModifiedArrays()); Map globalOrder = new IdentityHashMap<>(); for (int i = 0; i < imProg.getGlobals().size(); i++) { @@ -640,17 +662,21 @@ private void emitCompiletimeState() { continue; } for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { - if (state.getTypeArguments().isEmpty()) { + if (!state.isGeneric()) { emitCompiletimeArrayEntries(getCompiletimeArrayStateInitFunction(), var, state.getValue(), - new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType()); + new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); + } else if (state.getTypeArguments().isEmpty()) { + throw new InterpreterException(var.getTrace(), + "Could not determine the generic specialization for compiletime array " + var.getName()); } else { - emitCompiletimeGenericArrayState(var, state, ((ImArrayLikeType) var.getType()).getEntryType()); + emitCompiletimeGenericArrayState(var, state, ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); } } } } - private void emitCompiletimeGenericArrayState(ImVar var, ProgramState.ArrayState state, ImType entryType) { + private void emitCompiletimeGenericArrayState(ImVar var, ProgramState.ArrayState state, ImType entryType, + Set runtimeArrayWrites) { List typeVars = new ArrayList<>(); for (int i = 0; i < state.getTypeArguments().size(); i++) { typeVars.add(JassIm.ImTypeVar("T" + i)); @@ -660,29 +686,60 @@ private void emitCompiletimeGenericArrayState(ImVar var, ProgramState.ArrayState JassIm.ImTypeVars(typeVars), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(replay); - emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType); + emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType, runtimeArrayWrites); getCompiletimeArrayStateInitFunction().getBody().add(JassIm.ImFunctionCall( var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); } - private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, ImType entryType) { + private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, + ImType entryType, Set runtimeArrayWrites) { for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry : values.entries()) { List nextIndexes = new ArrayList<>(indexes); nextIndexes.add(JassIm.ImIntVal(entry.getIntKey())); if (entry.getValue() instanceof ILconstArray && entryType instanceof ImArrayLikeType) { emitCompiletimeArrayEntries(target, var, (ILconstArray) entry.getValue(), nextIndexes, - ((ImArrayLikeType) entryType).getEntryType()); + ((ImArrayLikeType) entryType).getEntryType(), runtimeArrayWrites); } else if (isPersistableCompiletimeValue(entry.getValue())) { target.getBody().add(JassIm.ImSet(var.getTrace(), JassIm.ImVarArrayAccess(var.getTrace(), var, JassIm.ImExprs(nextIndexes)), constantToExpr(var.getTrace(), entry.getValue(), entryType))); } else { - WLogger.warning("Skipping unsupported compiletime array entry at index " + entry.getIntKey() - + " (" + entry.getValue() + ") at " + var.getTrace()); + String message = "Unsupported compiletime array entry at index " + entry.getIntKey() + + " (" + entry.getValue() + ")"; + if (runtimeArrayWrites.contains(var)) { + WLogger.warning(message + "; runtime initialization of " + var.getName() + + " remains authoritative at " + var.getTrace()); + } else { + throw new InterpreterException(var.getTrace(), message); + } } } } + private Set findRuntimeArrayWrites() { + Set result = Collections.newSetFromMap(new IdentityHashMap<>()); + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Deque pending = new ArrayDeque<>(translator.initFuncMap.values()); + pending.add(translator.getMainFunc()); + while (!pending.isEmpty()) { + ImFunction function = pending.removeFirst(); + if (!visited.add(function)) { + continue; + } + function.accept(new ImFunction.DefaultVisitor() { + @Override + public void visit(ImSet set) { + super.visit(set); + if (set.getLeft() instanceof ImVarArrayAccess) { + result.add(((ImVarArrayAccess) set.getLeft()).getVar()); + } + } + }); + pending.addAll(UsedFunctions.calculate(function)); + } + return result; + } + private boolean isPersistableCompiletimeValue(ILconst value) { if (value instanceof ILconstBool || value instanceof ILconstInt || value instanceof ILconstReal || value instanceof ILconstString || value instanceof ILconstNull || value instanceof ILconstObject) { 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 ef92108e5..e56434aa5 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 @@ -812,10 +812,16 @@ public ILconstArray getArrayValue(ImVar v) { public static final class ArrayState { private final ILconstArray value; private final List typeArguments; + private final boolean generic; public ArrayState(ILconstArray value, List typeArguments) { + this(value, typeArguments, !typeArguments.isEmpty()); + } + + public ArrayState(ILconstArray value, List typeArguments, boolean generic) { this.value = value; - this.typeArguments = typeArguments; + this.typeArguments = Collections.unmodifiableList(new ArrayList<>(typeArguments)); + this.generic = generic; } public ILconstArray getValue() { @@ -825,6 +831,10 @@ public ILconstArray getValue() { public List getTypeArguments() { return typeArguments; } + + public boolean isGeneric() { + return generic; + } } public Collection getArrayStates(ImVar v) { @@ -836,7 +846,7 @@ public Collection getArrayStates(ImVar v) { if (key.startsWith(prefix)) { ILconstArray value = genericStaticArrays.get(key); if (value != null) { - result.add(new ArrayState(value, genericArrayTypeArguments.getOrDefault(key, Collections.emptyList()))); + result.add(new ArrayState(value, genericArrayTypeArguments.getOrDefault(key, Collections.emptyList()), true)); } } } 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 a4af7c192..6bada4f8c 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 @@ -137,6 +137,67 @@ public void testCompiletimeGenericArrayStateLua() { " testSuccess()"); } + @Test + public void testCompiletimeObjectArrayState() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "class A", + " int value", + "A array source", + "@compiletime function fill()", + " source[0] = new A", + " source[0].value = 42", + "init", + " if source[0].value == 42", + " testSuccess()"); + } + + @Test + public void testCompiletimeHashtableArrayState() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("type agent extends handle", + "type hashtable extends agent", + "package Test", + "native testSuccess()", + "@extern native InitHashtable() returns hashtable", + "@extern native LoadInteger(hashtable h, int p, int c) returns int", + "@extern native SaveInteger(hashtable h, int p, int c, int i)", + "hashtable array source", + "@compiletime function fill()", + " source[0] = InitHashtable()", + " SaveInteger(source[0], 2, 3, 42)", + "init", + " if LoadInteger(source[0], 2, 3) == 42", + " testSuccess()"); + } + + @Test + public void testCompiletimeNullArrayState() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "string array source = [\"value\"]", + "@compiletime function clear()", + " source[0] = null", + "init", + " if source[0] == null", + " testSuccess()"); + } + + @Test + public void testCompiletimeTupleArrayState() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "tuple pair(int left, int right)", + "pair array source", + "@compiletime function fill()", + " source[0] = pair(42, 7)", + "init", + " if source[0].left == 42 and source[0].right == 7", + " testSuccess()"); + } @Test public void testCompiletimeHashtable() { From 0008910ddfe3c29d90c9468f40ec330423c7f729 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 11:49:09 +0200 Subject: [PATCH 12/16] Avoid empty compiletime array init calls --- .../main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4eadfadc6..6189a52bd 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 @@ -571,7 +571,7 @@ private ImFunction getCompiletimeArrayStateInitFunction() { } private void insertCompiletimeArrayStateInitCalls() { - if (compiletimeArrayStateInitFunction == null) { + if (compiletimeArrayStateInitFunction == null || compiletimeArrayStateInitFunction.getBody().isEmpty()) { return; } From 87a40d4eef3353eabff363b6fcd71528b8c2ee99 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 12:37:38 +0200 Subject: [PATCH 13/16] Scope compiletime array replay by package --- .../wurstio/CompiletimeFunctionRunner.java | 200 +++++++++++++----- .../wurstscript/tests/CompiletimeTests.java | 40 ++++ 2 files changed, 182 insertions(+), 58 deletions(-) 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 6189a52bd..baaa3a334 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 @@ -198,14 +198,12 @@ private static long ms(long nanos) { } private void partitionCompiletimeStateInitFunction() { - if (compiletimeStateInitFunction == null) { - if (compiletimeArrayStateInitFunction != null) { - FunctionSplitter.splitFunc(translator, compiletimeArrayStateInitFunction); - } - } else { + if (compiletimeStateInitFunction != null) { FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); - if (compiletimeArrayStateInitFunction != null) { - FunctionSplitter.splitFunc(translator, compiletimeArrayStateInitFunction); + } + for (ImFunction arrayStateFunction : arrayStateInitFunctions.values()) { + if (!arrayStateFunction.getBody().isEmpty()) { + FunctionSplitter.splitFunc(translator, arrayStateFunction); } } } @@ -517,6 +515,8 @@ private CompiletimeObjectInit(ILconstObject object, ImVar targetVar) { private ImFunction compiletimeStateInitFunction = null; private ImFunction compiletimeArrayStateInitFunction = null; + private final Map arrayStateInitFunctions = new IdentityHashMap<>(); + private final Map> arrayStateInitializers = new IdentityHashMap<>(); private int genericArrayStateInitCounter; private ImFunction getCompiletimeStateInitFunction() { @@ -559,51 +559,53 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } - private ImFunction getCompiletimeArrayStateInitFunction() { - if (compiletimeArrayStateInitFunction == null) { - Element trace = imProg.getTrace(); - ImFunction res = JassIm.ImFunction(trace, "initCompiletimeArrayState", JassIm.ImTypeVars(), JassIm.ImVars(), - JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); - imProg.getFunctions().add(res); - compiletimeArrayStateInitFunction = res; + private ImFunction getCompiletimeArrayStateInitFunction(@Nullable ImFunction replayTarget) { + ImFunction existing = arrayStateInitFunctions.get(replayTarget); + if (existing != null) { + return existing; + } + Element trace = imProg.getTrace(); + String name = replayTarget == null + ? "initCompiletimeArrayState" + : "initCompiletimeArrayState_" + genericArrayStateInitCounter++; + ImFunction result = JassIm.ImFunction(trace, name, JassIm.ImTypeVars(), JassIm.ImVars(), + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + imProg.getFunctions().add(result); + arrayStateInitFunctions.put(replayTarget, result); + if (replayTarget == null) { + compiletimeArrayStateInitFunction = result; } - return compiletimeArrayStateInitFunction; + return result; } private void insertCompiletimeArrayStateInitCalls() { - if (compiletimeArrayStateInitFunction == null || compiletimeArrayStateInitFunction.getBody().isEmpty()) { + if (arrayStateInitFunctions.isEmpty()) { return; } - - Set modifiedArrayInitializers = Collections.newSetFromMap(new IdentityHashMap<>()); - for (ImVar var : globalState.getModifiedArrays()) { - for (ImSet initializer : imProg.getGlobalInits().getOrDefault(var, Collections.emptyList())) { - modifiedArrayInitializers.add(initializer); - } - } - ImFunction globalInitFunction = translator.getGlobalInitFunc(); - boolean insertedIntoPackage = false; - if (!modifiedArrayInitializers.isEmpty() - && insertReplayAfterInitializers(globalInitFunction, modifiedArrayInitializers)) { - insertedIntoPackage = true; - } - for (ImFunction initFunction : translator.initFuncMap.values()) { - if (initFunction.getBody().isEmpty()) { + List packageTargets = new ArrayList<>(arrayStateInitFunctions.keySet()); + packageTargets.remove(null); + packageTargets.sort(Comparator.comparing(ImFunction::getName)); + for (ImFunction target : packageTargets) { + ImFunction replay = arrayStateInitFunctions.get(target); + if (replay.getBody().isEmpty()) { continue; } - insertReplayAfterInitializers(initFunction, modifiedArrayInitializers); - insertedIntoPackage = true; + int insertionIndex = findLastArrayInitializer(target, arrayStateInitializers.get(target)); + if (insertionIndex >= 0) { + target.getBody().add(insertionIndex + 1, newCompiletimeArrayStateInitCall(replay)); + } } - if (!insertedIntoPackage) { + ImFunction mainReplay = arrayStateInitFunctions.get(null); + if (mainReplay != null && !mainReplay.getBody().isEmpty()) { ImStmts mainBody = translator.getMainFunc().getBody(); ImFunction stateInit = compiletimeStateInitFunction; if (stateInit != null) { for (int i = 0; i < mainBody.size(); i++) { ImStmt stmt = mainBody.get(i); if (stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc() == stateInit) { - mainBody.add(i + 1, newCompiletimeArrayStateInitCall()); + mainBody.add(i + 1, newCompiletimeArrayStateInitCall(mainReplay)); return; } } @@ -611,17 +613,17 @@ && insertReplayAfterInitializers(globalInitFunction, modifiedArrayInitializers)) for (int i = 0; i < mainBody.size(); i++) { ImStmt stmt = mainBody.get(i); if (stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc() == globalInitFunction) { - mainBody.add(i + 1, newCompiletimeArrayStateInitCall()); + mainBody.add(i + 1, newCompiletimeArrayStateInitCall(mainReplay)); return; } } - mainBody.add(0, newCompiletimeArrayStateInitCall()); + mainBody.add(0, newCompiletimeArrayStateInitCall(mainReplay)); } } - private boolean insertReplayAfterInitializers(ImFunction function, Set modifiedArrayInitializers) { + private int findLastArrayInitializer(ImFunction function, Set modifiedArrayInitializers) { if (function == null || function.getBody().isEmpty()) { - return false; + return -1; } int insertionIndex = -1; for (int i = 0; i < function.getBody().size(); i++) { @@ -630,22 +632,18 @@ private boolean insertReplayAfterInitializers(ImFunction function, Set mo insertionIndex = i + 1; } } - if (insertionIndex < 0) { - insertionIndex = 0; - } - function.getBody().add(insertionIndex, newCompiletimeArrayStateInitCall()); - return true; + return insertionIndex; } - private ImFunctionCall newCompiletimeArrayStateInitCall() { - return JassIm.ImFunctionCall(imProg.getTrace(), compiletimeArrayStateInitFunction, + private ImFunctionCall newCompiletimeArrayStateInitCall(ImFunction replayFunction) { + return JassIm.ImFunctionCall(imProg.getTrace(), replayFunction, JassIm.ImTypeArguments(), JassIm.ImExprs(), true, CallType.NORMAL); } private void emitCompiletimeState() { // constantToExpr may materialize object handles as additional globals. // Iterate over a snapshot to avoid modifying the collection in-flight. - Set runtimeArrayWrites = findRuntimeArrayWrites(); + Set runtimeArrayWrites = findRuntimeArrayWrites(); List modifiedArrays = new ArrayList<>(globalState.getModifiedArrays()); Map globalOrder = new IdentityHashMap<>(); for (int i = 0; i < imProg.getGlobals().size(); i++) { @@ -661,22 +659,56 @@ private void emitCompiletimeState() { if (!(var.getType() instanceof ImArrayLikeType)) { continue; } + ImFunction replayTarget = findArrayReplayTarget(var); + ImFunction replayFunction = getCompiletimeArrayStateInitFunction(replayTarget); for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { if (!state.isGeneric()) { - emitCompiletimeArrayEntries(getCompiletimeArrayStateInitFunction(), var, state.getValue(), + emitCompiletimeArrayEntries(replayFunction, var, state.getValue(), new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); } else if (state.getTypeArguments().isEmpty()) { throw new InterpreterException(var.getTrace(), "Could not determine the generic specialization for compiletime array " + var.getName()); } else { - emitCompiletimeGenericArrayState(var, state, ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); + emitCompiletimeGenericArrayState(replayFunction, var, state, + ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); + } + } + } + } + + private @Nullable ImFunction findArrayReplayTarget(ImVar var) { + List initializers = imProg.getGlobalInits().getOrDefault(var, Collections.emptyList()); + if (initializers.isEmpty()) { + return null; + } + List candidates = new ArrayList<>(); + candidates.add(translator.getGlobalInitFunc()); + candidates.addAll(translator.initFuncMap.values()); + for (ImFunction candidate : candidates) { + Set matching = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImSet initializer : initializers) { + for (ImStmt statement : candidate.getBody()) { + if (statement == initializer) { + matching.add(initializer); + break; + } + } + } + if (!matching.isEmpty()) { + if (candidate != translator.getGlobalInitFunc()) { + arrayStateInitializers.computeIfAbsent(candidate, + ignored -> Collections.newSetFromMap(new IdentityHashMap<>())).addAll(matching); + return candidate; } + return null; } } + return null; } - private void emitCompiletimeGenericArrayState(ImVar var, ProgramState.ArrayState state, ImType entryType, - Set runtimeArrayWrites) { + private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar var, + ProgramState.ArrayState state, ImType entryType, + Set runtimeArrayWrites) { List typeVars = new ArrayList<>(); for (int i = 0; i < state.getTypeArguments().size(); i++) { typeVars.add(JassIm.ImTypeVar("T" + i)); @@ -687,12 +719,14 @@ private void emitCompiletimeGenericArrayState(ImVar var, ProgramState.ArrayState JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(replay); emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType, runtimeArrayWrites); - getCompiletimeArrayStateInitFunction().getBody().add(JassIm.ImFunctionCall( - var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); + if (!replay.getBody().isEmpty()) { + replayFunction.getBody().add(JassIm.ImFunctionCall( + var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); + } } private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, - ImType entryType, Set runtimeArrayWrites) { + ImType entryType, Set runtimeArrayWrites) { for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry : values.entries()) { List nextIndexes = new ArrayList<>(indexes); nextIndexes.add(JassIm.ImIntVal(entry.getIntKey())); @@ -706,7 +740,8 @@ private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstAr } else { String message = "Unsupported compiletime array entry at index " + entry.getIntKey() + " (" + entry.getValue() + ")"; - if (runtimeArrayWrites.contains(var)) { + RuntimeArrayWrite runtimeWrite = runtimeArrayWrite(var, nextIndexes); + if (runtimeWrite != null && runtimeArrayWrites.stream().anyMatch(runtimeWrite::matches)) { WLogger.warning(message + "; runtime initialization of " + var.getName() + " remains authoritative at " + var.getTrace()); } else { @@ -716,8 +751,8 @@ private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstAr } } - private Set findRuntimeArrayWrites() { - Set result = Collections.newSetFromMap(new IdentityHashMap<>()); + private Set findRuntimeArrayWrites() { + Set result = new HashSet<>(); Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); Deque pending = new ArrayDeque<>(translator.initFuncMap.values()); pending.add(translator.getMainFunc()); @@ -731,7 +766,12 @@ private Set findRuntimeArrayWrites() { public void visit(ImSet set) { super.visit(set); if (set.getLeft() instanceof ImVarArrayAccess) { - result.add(((ImVarArrayAccess) set.getLeft()).getVar()); + ImVarArrayAccess access = (ImVarArrayAccess) set.getLeft(); + List indexes = new ArrayList<>(); + for (ImExpr index : access.getIndexes()) { + indexes.add(index instanceof ImIntVal ? ((ImIntVal) index).getValI() : null); + } + result.add(new RuntimeArrayWrite(access.getVar(), indexes)); } } }); @@ -740,6 +780,50 @@ public void visit(ImSet set) { return result; } + private static final class RuntimeArrayWrite { + private final ImVar var; + private final List indexes; + + private RuntimeArrayWrite(ImVar var, List indexes) { + this.var = var; + this.indexes = new ArrayList<>(indexes); + } + + private boolean matches(RuntimeArrayWrite other) { + if (var != other.var || indexes.size() != other.indexes.size()) { + return false; + } + for (int i = 0; i < indexes.size(); i++) { + Integer expected = indexes.get(i); + Integer actual = other.indexes.get(i); + if (expected != null && actual != null && !expected.equals(actual)) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof RuntimeArrayWrite)) return false; + RuntimeArrayWrite that = (RuntimeArrayWrite) other; + return var == that.var && indexes.equals(that.indexes); + } + + @Override + public int hashCode() { + return 31 * System.identityHashCode(var) + indexes.hashCode(); + } + } + + private static RuntimeArrayWrite runtimeArrayWrite(ImVar var, List indexes) { + List constantIndexes = new ArrayList<>(); + for (ImExpr index : indexes) { + constantIndexes.add(index instanceof ImIntVal ? ((ImIntVal) index).getValI() : null); + } + return new RuntimeArrayWrite(var, constantIndexes); + } + private boolean isPersistableCompiletimeValue(ILconst value) { if (value instanceof ILconstBool || value instanceof ILconstInt || value instanceof ILconstReal || value instanceof ILconstString || value instanceof ILconstNull || value instanceof ILconstObject) { 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 6bada4f8c..c98e4f0e2 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 @@ -199,6 +199,46 @@ public void testCompiletimeTupleArrayState() { " testSuccess()"); } + @Test + public void testCompiletimeArrayStateAcrossPackages() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package A", + "public int array source = [1]", + "@compiletime function fillA()", + " source[0] = 42", + "init", + " source[0] = 7", + "endpackage", + "package B", + "import A", + "native testSuccess()", + "init", + " if source[0] == 7", + " testSuccess()"); + } + + @Test + public void testCompiletimeArrayStateAcrossPackagesWithTwoInitializers() { + test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package A", + "public int array source = [1]", + "@compiletime function fillA()", + " source[0] = 42", + "init", + " source[0] = 7", + "endpackage", + "package B", + "import A", + "int array other = [2]", + "@compiletime function fillB()", + " other[0] = 9", + "native testSuccess()", + "init", + " if source[0] == 7 and other[0] == 9", + " testSuccess()", + "endpackage"); + } + @Test public void testCompiletimeHashtable() { test().executeProg(true).executeProgOnlyAfterTransforms() From 9eba17132cc9cf7ab3c3fe7a2ebd65f20ac896a7 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 13:30:20 +0200 Subject: [PATCH 14/16] Fix deterministic compiletime array replay splitting --- .../wurstio/CompiletimeFunctionRunner.java | 9 +++- .../optimizer/FunctionSplitter.java | 45 +++++++++++----- .../tests/LuaBackendAuditTests.java | 53 +++++++++++++++++++ 3 files changed, 93 insertions(+), 14 deletions(-) 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 baaa3a334..b0ebd90dd 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 @@ -201,7 +201,9 @@ private void partitionCompiletimeStateInitFunction() { if (compiletimeStateInitFunction != null) { FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); } - for (ImFunction arrayStateFunction : arrayStateInitFunctions.values()) { + List splitTargets = new ArrayList<>(arrayStateSplitTargets); + splitTargets.sort(Comparator.comparing(ImFunction::getName)); + for (ImFunction arrayStateFunction : splitTargets) { if (!arrayStateFunction.getBody().isEmpty()) { FunctionSplitter.splitFunc(translator, arrayStateFunction); } @@ -517,6 +519,7 @@ private CompiletimeObjectInit(ILconstObject object, ImVar targetVar) { private ImFunction compiletimeArrayStateInitFunction = null; private final Map arrayStateInitFunctions = new IdentityHashMap<>(); private final Map> arrayStateInitializers = new IdentityHashMap<>(); + private final List arrayStateSplitTargets = new ArrayList<>(); private int genericArrayStateInitCounter; private ImFunction getCompiletimeStateInitFunction() { @@ -571,6 +574,7 @@ private ImFunction getCompiletimeArrayStateInitFunction(@Nullable ImFunction rep ImFunction result = JassIm.ImFunction(trace, name, JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(result); + arrayStateSplitTargets.add(result); arrayStateInitFunctions.put(replayTarget, result); if (replayTarget == null) { compiletimeArrayStateInitFunction = result; @@ -629,7 +633,7 @@ private int findLastArrayInitializer(ImFunction function, Set modifiedArr for (int i = 0; i < function.getBody().size(); i++) { if (function.getBody().get(i) instanceof ImSet && modifiedArrayInitializers.contains(function.getBody().get(i))) { - insertionIndex = i + 1; + insertionIndex = i; } } return insertionIndex; @@ -718,6 +722,7 @@ private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar v JassIm.ImTypeVars(typeVars), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(replay); + arrayStateSplitTargets.add(replay); emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType, runtimeArrayWrites); if (!replay.getBody().isEmpty()) { replayFunction.getBody().add(JassIm.ImFunctionCall( diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/FunctionSplitter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/FunctionSplitter.java index 423e92eba..6e199ff9e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/FunctionSplitter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/FunctionSplitter.java @@ -34,7 +34,6 @@ public static void splitFunc(ImTranslator tr, ImFunction func) { } private void optimize() { - Preconditions.checkArgument(func.getTypeVariables().isEmpty(), "func must not be generic"); Preconditions.checkArgument(func.getParameters().isEmpty(), "func parameters must be empty"); Preconditions.checkArgument(func.getReturnType() instanceof ImVoid, "func must return void"); // run some basic optimizations first: @@ -47,6 +46,10 @@ private void optimize() { Set usedVars = UsedVariables.calculate(func); func.getLocals().removeIf(v -> !usedVars.contains(v)); func.flatten(tr); + boolean generic = !func.getTypeVariables().isEmpty(); + Preconditions.checkArgument(!generic || func.getLocals().isEmpty(), + "generic split functions must not have locals"); + ImFunction genericTemplate = generic ? func.copyWithRefs() : null; List> splitResult = split(func.getBody().removeAll()); ImProg prog = tr.getImProg(); @@ -55,19 +58,29 @@ private void optimize() { // create helper functions List helperFuncs = new ArrayList<>(); + int statementOffset = 0; for (int i = 0; i < splitResult.size(); i++) { List stmts = splitResult.get(i); - ImFunction helperFunc = JassIm.ImFunction( - func.getTrace(), - func.getName() + "_" + i, - JassIm.ImTypeVars(), - JassIm.ImVars(), - JassIm.ImVoid(), - JassIm.ImVars(), - JassIm.ImStmts(stmts), - Collections.emptyList() - ); + ImFunction helperFunc; + if (generic) { + helperFunc = genericTemplate.copyWithRefs(); + List copiedStatements = helperFunc.getBody().removeAll(); + helperFunc.getBody().addAll(copiedStatements.subList(statementOffset, statementOffset + stmts.size())); + helperFunc.setName(func.getName() + "_" + i); + } else { + helperFunc = JassIm.ImFunction( + func.getTrace(), + func.getName() + "_" + i, + JassIm.ImTypeVars(), + JassIm.ImVars(), + JassIm.ImVoid(), + JassIm.ImVars(), + JassIm.ImStmts(stmts), + Collections.emptyList() + ); + } helperFuncs.add(helperFunc); + statementOffset += stmts.size(); } prog.getFunctions().addAll(helperFuncs); @@ -76,7 +89,7 @@ private void optimize() { func.getBody().add(JassIm.ImFunctionCall( func.getTrace(), helperFunc, - JassIm.ImTypeArguments(), + typeArgumentsForCurrentFunction(), JassIm.ImExprs(), false, CallType.EXECUTE @@ -84,6 +97,14 @@ private void optimize() { } } + private ImTypeArguments typeArgumentsForCurrentFunction() { + ImTypeArguments result = JassIm.ImTypeArguments(); + for (ImTypeVar typeVar : func.getTypeVariables()) { + result.add(JassIm.ImTypeArgument(JassIm.ImTypeVarRef(typeVar), Collections.emptyMap())); + } + return result; + } + private List> split(List body) { List> result = new ArrayList<>(); int fuel = 0; 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 1f269fa18..0f10bfab8 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 @@ -53,6 +53,59 @@ private String compileLuaWithRunArgs(String testName, RunArgs runArgs, String... return result.toString(); } + @Test + public void compiletimeGenericArrayReplayLeavesAreSplit() { + String compiled = compileLuaWithRunArgs( + "compiletimeGenericArrayReplayLeavesAreSplit", + new RunArgs().with("-lua", "-runcompiletimefunctions", "-functionSplitLimit", "1"), + "package Test", + "class Box", + " static T array store", + " static function set(int index, T value)", + " store[index] = value", + " static function get(int index) returns T", + " return store[index]", + "@compiletime function fill()", + " Box.set(0, 10)", + " Box.set(1, 20)", + " Box.set(2, 30)", + "native testSuccess()", + "init", + " if Box.get(0) + Box.get(1) + Box.get(2) == 60", + " testSuccess()" + ); + + java.util.regex.Matcher replayBody = java.util.regex.Pattern + .compile("function initCompiletimeArrayState[^\\n]*\\n(.*?)\\nend", java.util.regex.Pattern.DOTALL) + .matcher(compiled); + int persistedAssignments = 0; + while (replayBody.find()) { + int assignmentsInFunction = countOccurrences(replayBody.group(1), "Box_store["); + assertTrue("each generic replay leaf must honor the configured split limit:\n" + replayBody.group(), + assignmentsInFunction <= 1); + persistedAssignments += assignmentsInFunction; + } + assertEquals("all generic compiletime array entries must still be emitted", 3, persistedAssignments); + } + + @Test + public void compiletimeArrayReplaySplittingIsDeterministicAcrossPackages() { + RunArgs runArgs = new RunArgs().with( + "-lua", "-runcompiletimefunctions", "-functionSplitLimit", "1"); + String[] source = { + "package A", "public int array a = [1]", "@compiletime function fillA()", " a[0] = 10", "endpackage", + "package B", "public int array b = [1]", "@compiletime function fillB()", " b[0] = 20", "endpackage", + "package C", "public int array c = [1]", "@compiletime function fillC()", " c[0] = 30", "endpackage", + "package D", "public int array d = [1]", "@compiletime function fillD()", " d[0] = 40", "endpackage", + "package Test", "import A", "import B", "import C", "import D", "native testSuccess()", "init", + " if a[0] + b[0] + c[0] + d[0] == 100", " testSuccess()" + }; + + String first = compileLuaWithRunArgs("compiletimeArrayReplaySplittingIsDeterministicAcrossPackages", runArgs, source); + String second = compileLuaWithRunArgs("compiletimeArrayReplaySplittingIsDeterministicAcrossPackages", runArgs, source); + assertEquals("compiletime replay splitting must not depend on identity-hash iteration", first, second); + } + @Test public void localPlayerEffectfulBooleanOperandSurvivesOptimization() { String compiled = compileOptimizedLua( From 9e85b9a5ce7ef68e92b4a8be5a427d6617059974 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 14:18:26 +0200 Subject: [PATCH 15/16] Replay compiletime arrays before dependent initializers --- .../wurstio/CompiletimeFunctionRunner.java | 77 ++++++++++++------- .../wurstscript/tests/CompiletimeTests.java | 16 ++++ 2 files changed, 65 insertions(+), 28 deletions(-) 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 b0ebd90dd..0ef632630 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 @@ -515,10 +515,31 @@ private CompiletimeObjectInit(ILconstObject object, ImVar targetVar) { } } + private static class ArrayReplayLocation { + private final @Nullable ImFunction target; + private final Set initializers; + + private ArrayReplayLocation(@Nullable ImFunction target, Set initializers) { + this.target = target; + this.initializers = initializers; + } + } + + private static class PackageArrayStateReplay { + private final ImFunction target; + private final Set initializers; + private final ImFunction replay; + + private PackageArrayStateReplay(ImFunction target, Set initializers, ImFunction replay) { + this.target = target; + this.initializers = initializers; + this.replay = replay; + } + } + private ImFunction compiletimeStateInitFunction = null; private ImFunction compiletimeArrayStateInitFunction = null; - private final Map arrayStateInitFunctions = new IdentityHashMap<>(); - private final Map> arrayStateInitializers = new IdentityHashMap<>(); + private final List packageArrayStateReplays = new ArrayList<>(); private final List arrayStateSplitTargets = new ArrayList<>(); private int genericArrayStateInitCounter; @@ -562,46 +583,48 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } - private ImFunction getCompiletimeArrayStateInitFunction(@Nullable ImFunction replayTarget) { - ImFunction existing = arrayStateInitFunctions.get(replayTarget); - if (existing != null) { - return existing; + private ImFunction getCompiletimeArrayStateInitFunction(ArrayReplayLocation location) { + if (location.target == null && compiletimeArrayStateInitFunction != null) { + return compiletimeArrayStateInitFunction; } Element trace = imProg.getTrace(); - String name = replayTarget == null + String name = location.target == null ? "initCompiletimeArrayState" : "initCompiletimeArrayState_" + genericArrayStateInitCounter++; ImFunction result = JassIm.ImFunction(trace, name, JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(result); arrayStateSplitTargets.add(result); - arrayStateInitFunctions.put(replayTarget, result); - if (replayTarget == null) { + if (location.target == null) { compiletimeArrayStateInitFunction = result; + } else { + packageArrayStateReplays.add(new PackageArrayStateReplay( + location.target, location.initializers, result)); } return result; } private void insertCompiletimeArrayStateInitCalls() { - if (arrayStateInitFunctions.isEmpty()) { + if (packageArrayStateReplays.isEmpty() && compiletimeArrayStateInitFunction == null) { return; } ImFunction globalInitFunction = translator.getGlobalInitFunc(); - List packageTargets = new ArrayList<>(arrayStateInitFunctions.keySet()); - packageTargets.remove(null); - packageTargets.sort(Comparator.comparing(ImFunction::getName)); - for (ImFunction target : packageTargets) { - ImFunction replay = arrayStateInitFunctions.get(target); - if (replay.getBody().isEmpty()) { + List packageReplays = new ArrayList<>(packageArrayStateReplays); + packageReplays.sort(Comparator + .comparing((PackageArrayStateReplay replay) -> replay.target.getName()) + .thenComparing(replay -> replay.replay.getName())); + for (PackageArrayStateReplay packageReplay : packageReplays) { + if (packageReplay.replay.getBody().isEmpty()) { continue; } - int insertionIndex = findLastArrayInitializer(target, arrayStateInitializers.get(target)); + int insertionIndex = findLastArrayInitializer(packageReplay.target, packageReplay.initializers); if (insertionIndex >= 0) { - target.getBody().add(insertionIndex + 1, newCompiletimeArrayStateInitCall(replay)); + packageReplay.target.getBody().add( + insertionIndex + 1, newCompiletimeArrayStateInitCall(packageReplay.replay)); } } - ImFunction mainReplay = arrayStateInitFunctions.get(null); + ImFunction mainReplay = compiletimeArrayStateInitFunction; if (mainReplay != null && !mainReplay.getBody().isEmpty()) { ImStmts mainBody = translator.getMainFunc().getBody(); ImFunction stateInit = compiletimeStateInitFunction; @@ -663,8 +686,8 @@ private void emitCompiletimeState() { if (!(var.getType() instanceof ImArrayLikeType)) { continue; } - ImFunction replayTarget = findArrayReplayTarget(var); - ImFunction replayFunction = getCompiletimeArrayStateInitFunction(replayTarget); + ArrayReplayLocation replayLocation = findArrayReplayTarget(var); + ImFunction replayFunction = getCompiletimeArrayStateInitFunction(replayLocation); for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { if (!state.isGeneric()) { emitCompiletimeArrayEntries(replayFunction, var, state.getValue(), @@ -680,10 +703,10 @@ private void emitCompiletimeState() { } } - private @Nullable ImFunction findArrayReplayTarget(ImVar var) { + private ArrayReplayLocation findArrayReplayTarget(ImVar var) { List initializers = imProg.getGlobalInits().getOrDefault(var, Collections.emptyList()); if (initializers.isEmpty()) { - return null; + return new ArrayReplayLocation(null, Collections.emptySet()); } List candidates = new ArrayList<>(); candidates.add(translator.getGlobalInitFunc()); @@ -700,14 +723,12 @@ private void emitCompiletimeState() { } if (!matching.isEmpty()) { if (candidate != translator.getGlobalInitFunc()) { - arrayStateInitializers.computeIfAbsent(candidate, - ignored -> Collections.newSetFromMap(new IdentityHashMap<>())).addAll(matching); - return candidate; + return new ArrayReplayLocation(candidate, matching); } - return null; + return new ArrayReplayLocation(null, Collections.emptySet()); } } - return null; + return new ArrayReplayLocation(null, Collections.emptySet()); } private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar var, 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 c98e4f0e2..d9d60cf14 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 @@ -239,6 +239,22 @@ public void testCompiletimeArrayStateAcrossPackagesWithTwoInitializers() { "endpackage"); } + @Test + public void testCompiletimeArrayReplayPrecedesDependentInitializer() { + test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "int array first = [1]", + "int observed = first[0]", + "int array second = [2]", + "@compiletime function fill()", + " first[0] = 42", + " second[0] = 9", + "init", + " if observed == 42 and first[0] == 42 and second[0] == 9", + " testSuccess()"); + } + @Test public void testCompiletimeHashtable() { test().executeProg(true).executeProgOnlyAfterTransforms() From 08328ab8fa4eb55c81df0c27f6f558254e578a2e Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 14:53:50 +0200 Subject: [PATCH 16/16] Replay only compiletime-written array entries --- .../wurstio/CompiletimeFunctionRunner.java | 30 +++++++++++++------ .../interpreter/ProgramState.java | 26 ++++++++++++++-- .../intermediatelang/interpreter/State.java | 16 +++++++--- .../wurstscript/tests/CompiletimeTests.java | 19 ++++++++++++ 4 files changed, 75 insertions(+), 16 deletions(-) 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 0ef632630..998cab415 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 @@ -691,7 +691,8 @@ private void emitCompiletimeState() { for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { if (!state.isGeneric()) { emitCompiletimeArrayEntries(replayFunction, var, state.getValue(), - new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); + new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites, + state.getModifiedIndexes()); } else if (state.getTypeArguments().isEmpty()) { throw new InterpreterException(var.getTrace(), "Could not determine the generic specialization for compiletime array " + var.getName()); @@ -744,29 +745,40 @@ private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar v JassIm.ImStmts(), Collections.emptyList()); imProg.getFunctions().add(replay); arrayStateSplitTargets.add(replay); - emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType, runtimeArrayWrites); + emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType, + runtimeArrayWrites, state.getModifiedIndexes()); if (!replay.getBody().isEmpty()) { replayFunction.getBody().add(JassIm.ImFunctionCall( var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); } } - private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, - ImType entryType, Set runtimeArrayWrites) { + private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, + ImType entryType, Set runtimeArrayWrites, + Set> modifiedIndexes) { for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry : values.entries()) { - List nextIndexes = new ArrayList<>(indexes); - nextIndexes.add(JassIm.ImIntVal(entry.getIntKey())); + List nextIndexes = new ArrayList<>(indexes); + nextIndexes.add(entry.getIntKey()); if (entry.getValue() instanceof ILconstArray && entryType instanceof ImArrayLikeType) { emitCompiletimeArrayEntries(target, var, (ILconstArray) entry.getValue(), nextIndexes, - ((ImArrayLikeType) entryType).getEntryType(), runtimeArrayWrites); + ((ImArrayLikeType) entryType).getEntryType(), runtimeArrayWrites, modifiedIndexes); + } else if (!modifiedIndexes.contains(nextIndexes)) { + continue; } else if (isPersistableCompiletimeValue(entry.getValue())) { + ImExprs indexExpressions = JassIm.ImExprs(); + for (Integer index : nextIndexes) { + indexExpressions.add(JassIm.ImIntVal(index)); + } target.getBody().add(JassIm.ImSet(var.getTrace(), - JassIm.ImVarArrayAccess(var.getTrace(), var, JassIm.ImExprs(nextIndexes)), + JassIm.ImVarArrayAccess(var.getTrace(), var, indexExpressions), constantToExpr(var.getTrace(), entry.getValue(), entryType))); } else { String message = "Unsupported compiletime array entry at index " + entry.getIntKey() + " (" + entry.getValue() + ")"; - RuntimeArrayWrite runtimeWrite = runtimeArrayWrite(var, nextIndexes); + List indexExpressions = nextIndexes.stream() + .map(JassIm::ImIntVal) + .collect(Collectors.toList()); + RuntimeArrayWrite runtimeWrite = runtimeArrayWrite(var, indexExpressions); if (runtimeWrite != null && runtimeArrayWrites.stream().anyMatch(runtimeWrite::matches)) { WLogger.warning(message + "; runtime initialization of " + var.getName() + " remains authoritative at " + var.getTrace()); 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 e56434aa5..9d47a2c6b 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 Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); private final Set modifiedGenericArrays = new HashSet<>(); + private final Map>> modifiedGenericArrayIndexes = new HashMap<>(); private final Map> genericArrayTypeArguments = new HashMap<>(); private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); @@ -770,6 +771,8 @@ public void setArrayVal(ImVar v, List indexes, ILconst val) { super.setArrayVal(v, indexes, val); if (key != null) { modifiedGenericArrays.add(key); + modifiedGenericArrayIndexes.computeIfAbsent(key, ignored -> new HashSet<>()) + .add(Collections.unmodifiableList(new ArrayList<>(indexes))); genericArrayTypeArguments.computeIfAbsent(key, ignored -> genericStaticTypeArguments(v)); } } @@ -813,15 +816,26 @@ public static final class ArrayState { private final ILconstArray value; private final List typeArguments; private final boolean generic; + private final Set> modifiedIndexes; public ArrayState(ILconstArray value, List typeArguments) { - this(value, typeArguments, !typeArguments.isEmpty()); + this(value, typeArguments, !typeArguments.isEmpty(), Collections.emptySet()); } public ArrayState(ILconstArray value, List typeArguments, boolean generic) { + this(value, typeArguments, generic, Collections.emptySet()); + } + + public ArrayState(ILconstArray value, List typeArguments, boolean generic, + Set> modifiedIndexes) { this.value = value; this.typeArguments = Collections.unmodifiableList(new ArrayList<>(typeArguments)); this.generic = generic; + Set> indexSnapshot = new HashSet<>(); + for (List indexes : modifiedIndexes) { + indexSnapshot.add(Collections.unmodifiableList(new ArrayList<>(indexes))); + } + this.modifiedIndexes = Collections.unmodifiableSet(indexSnapshot); } public ILconstArray getValue() { @@ -835,6 +849,10 @@ public List getTypeArguments() { public boolean isGeneric() { return generic; } + + public Set> getModifiedIndexes() { + return modifiedIndexes; + } } public Collection getArrayStates(ImVar v) { @@ -846,12 +864,14 @@ public Collection getArrayStates(ImVar v) { if (key.startsWith(prefix)) { ILconstArray value = genericStaticArrays.get(key); if (value != null) { - result.add(new ArrayState(value, genericArrayTypeArguments.getOrDefault(key, Collections.emptyList()), true)); + result.add(new ArrayState(value, + genericArrayTypeArguments.getOrDefault(key, Collections.emptyList()), true, + modifiedGenericArrayIndexes.getOrDefault(key, Collections.emptySet()))); } } } if (result.isEmpty() && genericStaticKey(v) == null) { - result.add(new ArrayState(getArray(v), Collections.emptyList())); + result.add(new ArrayState(getArray(v), Collections.emptyList(), false, getModifiedArrayIndexes(v))); } return result; } 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 73a072817..3f3a9aaa9 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 @@ -8,10 +8,13 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.eclipse.jdt.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.HashSet; /** * Lazily allocates internal maps ONLY when needed. @@ -21,7 +24,7 @@ public abstract class State { // in State: private @Nullable Object2ObjectOpenHashMap values; private @Nullable Object2ObjectOpenHashMap arrayValues; - private final Set modifiedArrays = new HashSet<>(); + private final Map>> modifiedArrayIndexes = new IdentityHashMap<>(); private Object2ObjectOpenHashMap ensureValues() { @@ -80,7 +83,8 @@ static ILconstArray createArrayConstantFromType(ImType vType) { } public void setArrayVal(ImVar v, List indexes, ILconst val) { - modifiedArrays.add(v); + modifiedArrayIndexes.computeIfAbsent(v, ignored -> new HashSet<>()) + .add(Collections.unmodifiableList(new ArrayList<>(indexes))); ILconstArray ar = getArray(v); for (int i = 0; i < indexes.size() - 1; i++) { ar = (ILconstArray) ar.get(indexes.get(i)); @@ -89,7 +93,11 @@ public void setArrayVal(ImVar v, List indexes, ILconst val) { } public Set getModifiedArrays() { - return modifiedArrays; + return modifiedArrayIndexes.keySet(); + } + + public Set> getModifiedArrayIndexes(ImVar v) { + return modifiedArrayIndexes.getOrDefault(v, Collections.emptySet()); } public @Nullable ILconst getArrayVal(ImVar v, List indexes) { 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 d9d60cf14..fca1ff8a8 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 @@ -255,6 +255,25 @@ public void testCompiletimeArrayReplayPrecedesDependentInitializer() { " testSuccess()"); } + @Test + public void testCompiletimeArrayReplayOnlyWrittenEntries() { + test().testLua(true).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package A", + "public int seed = 1", + "init", + " seed = 2", + "endpackage", + "package B", + "import A", + "native testSuccess()", + "int array source = [seed, 0]", + "@compiletime function fill()", + " source[1] = 42", + "init", + " if source[0] == 2 and source[1] == 42", + " testSuccess()"); + } + @Test public void testCompiletimeHashtable() { test().executeProg(true).executeProgOnlyAfterTransforms()