From 8938e81967077c5aca3e275ff1169f0fba823e32 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 1 Aug 2026 17:04:29 +0200 Subject: [PATCH] Migrate compiletime scalar state --- .../wurstio/CompiletimeFunctionRunner.java | 215 ++++++++++++++---- .../interpreter/EvaluateExpr.java | 2 +- .../interpreter/ProgramState.java | 67 ++++++ .../wurstscript/tests/CompiletimeTests.java | 144 ++++++++++++ .../tests/LuaBackendAuditTests.java | 33 +++ 5 files changed, 412 insertions(+), 49 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 998cab415..37551d3da 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 @@ -124,6 +124,7 @@ public void run() { runDelayedActions(); emitCompiletimeObjectAllocs(); if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { + insertCompiletimeScalarStateInitCalls(); insertCompiletimeArrayStateInitCalls(); } long tDelayed = System.nanoTime(); @@ -201,11 +202,12 @@ private void partitionCompiletimeStateInitFunction() { if (compiletimeStateInitFunction != null) { FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); } - List splitTargets = new ArrayList<>(arrayStateSplitTargets); + List splitTargets = new ArrayList<>(scalarStateSplitTargets); + splitTargets.addAll(arrayStateSplitTargets); splitTargets.sort(Comparator.comparing(ImFunction::getName)); - for (ImFunction arrayStateFunction : splitTargets) { - if (!arrayStateFunction.getBody().isEmpty()) { - FunctionSplitter.splitFunc(translator, arrayStateFunction); + for (ImFunction stateFunction : splitTargets) { + if (!stateFunction.getBody().isEmpty()) { + FunctionSplitter.splitFunc(translator, stateFunction); } } } @@ -341,7 +343,7 @@ public ImVar initFor(ILconstObject obj) { ImVar res = JassIm.ImVar(obj.getTrace(), obj.getType(), obj.getType() + "_compiletime", false); imProg.getGlobals().add(res); - globalState.setVal(res, obj); + globalState.setValUntracked(res, obj); registerCompiletimeObject(obj, res); @@ -394,7 +396,7 @@ public ImVar initFor(IlConstHandle a) { ImType type = TypesHelper.imHashTable(); ImVar res = JassIm.ImVar(trace, type, type + "_compiletime", false); imProg.getGlobals().add(res); - globalState.setVal(res, a); + globalState.setValUntracked(res, a); init = constantToExprHashtable(trace, res, a, map); addCompiletimeStateInitAlloc(trace, res, init); @@ -515,22 +517,22 @@ private CompiletimeObjectInit(ILconstObject object, ImVar targetVar) { } } - private static class ArrayReplayLocation { + private static class StateReplayLocation { private final @Nullable ImFunction target; private final Set initializers; - private ArrayReplayLocation(@Nullable ImFunction target, Set initializers) { + private StateReplayLocation(@Nullable ImFunction target, Set initializers) { this.target = target; this.initializers = initializers; } } - private static class PackageArrayStateReplay { + private static class PackageStateReplay { private final ImFunction target; private final Set initializers; private final ImFunction replay; - private PackageArrayStateReplay(ImFunction target, Set initializers, ImFunction replay) { + private PackageStateReplay(ImFunction target, Set initializers, ImFunction replay) { this.target = target; this.initializers = initializers; this.replay = replay; @@ -538,9 +540,13 @@ private PackageArrayStateReplay(ImFunction target, Set initializers, ImFu } private ImFunction compiletimeStateInitFunction = null; + private ImFunction compiletimeScalarStateInitFunction = null; private ImFunction compiletimeArrayStateInitFunction = null; - private final List packageArrayStateReplays = new ArrayList<>(); + private final List packageScalarStateReplays = new ArrayList<>(); + private final List packageArrayStateReplays = new ArrayList<>(); + private final List scalarStateSplitTargets = new ArrayList<>(); private final List arrayStateSplitTargets = new ArrayList<>(); + private int genericScalarStateInitCounter; private int genericArrayStateInitCounter; private ImFunction getCompiletimeStateInitFunction() { @@ -583,7 +589,28 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } - private ImFunction getCompiletimeArrayStateInitFunction(ArrayReplayLocation location) { + private ImFunction getCompiletimeScalarStateInitFunction(StateReplayLocation location) { + if (location.target == null && compiletimeScalarStateInitFunction != null) { + return compiletimeScalarStateInitFunction; + } + Element trace = imProg.getTrace(); + String name = location.target == null + ? "initCompiletimeScalarState" + : "initCompiletimeScalarState_" + genericScalarStateInitCounter++; + ImFunction result = JassIm.ImFunction(trace, name, JassIm.ImTypeVars(), JassIm.ImVars(), + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + imProg.getFunctions().add(result); + scalarStateSplitTargets.add(result); + if (location.target == null) { + compiletimeScalarStateInitFunction = result; + } else { + packageScalarStateReplays.add(new PackageStateReplay( + location.target, location.initializers, result)); + } + return result; + } + + private ImFunction getCompiletimeArrayStateInitFunction(StateReplayLocation location) { if (location.target == null && compiletimeArrayStateInitFunction != null) { return compiletimeArrayStateInitFunction; } @@ -598,33 +625,42 @@ private ImFunction getCompiletimeArrayStateInitFunction(ArrayReplayLocation loca if (location.target == null) { compiletimeArrayStateInitFunction = result; } else { - packageArrayStateReplays.add(new PackageArrayStateReplay( + packageArrayStateReplays.add(new PackageStateReplay( location.target, location.initializers, result)); } return result; } + private void insertCompiletimeScalarStateInitCalls() { + insertCompiletimeMigratedStateInitCalls(packageScalarStateReplays, compiletimeScalarStateInitFunction); + } + private void insertCompiletimeArrayStateInitCalls() { - if (packageArrayStateReplays.isEmpty() && compiletimeArrayStateInitFunction == null) { + insertCompiletimeMigratedStateInitCalls(packageArrayStateReplays, compiletimeArrayStateInitFunction); + } + + private void insertCompiletimeMigratedStateInitCalls(List packageStateReplays, + @Nullable ImFunction mainStateReplay) { + if (packageStateReplays.isEmpty() && mainStateReplay == null) { return; } ImFunction globalInitFunction = translator.getGlobalInitFunc(); - List packageReplays = new ArrayList<>(packageArrayStateReplays); + List packageReplays = new ArrayList<>(packageStateReplays); packageReplays.sort(Comparator - .comparing((PackageArrayStateReplay replay) -> replay.target.getName()) + .comparing((PackageStateReplay replay) -> replay.target.getName()) .thenComparing(replay -> replay.replay.getName())); - for (PackageArrayStateReplay packageReplay : packageReplays) { + for (PackageStateReplay packageReplay : packageReplays) { if (packageReplay.replay.getBody().isEmpty()) { continue; } - int insertionIndex = findLastArrayInitializer(packageReplay.target, packageReplay.initializers); + int insertionIndex = findLastInitializer(packageReplay.target, packageReplay.initializers); if (insertionIndex >= 0) { packageReplay.target.getBody().add( - insertionIndex + 1, newCompiletimeArrayStateInitCall(packageReplay.replay)); + insertionIndex + 1, newCompiletimeStateInitCall(packageReplay.replay)); } } - ImFunction mainReplay = compiletimeArrayStateInitFunction; + ImFunction mainReplay = mainStateReplay; if (mainReplay != null && !mainReplay.getBody().isEmpty()) { ImStmts mainBody = translator.getMainFunc().getBody(); ImFunction stateInit = compiletimeStateInitFunction; @@ -632,7 +668,7 @@ private void insertCompiletimeArrayStateInitCalls() { 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(mainReplay)); + mainBody.add(i + 1, newCompiletimeStateInitCall(mainReplay)); return; } } @@ -640,29 +676,29 @@ private void insertCompiletimeArrayStateInitCalls() { 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(mainReplay)); + mainBody.add(i + 1, newCompiletimeStateInitCall(mainReplay)); return; } } - mainBody.add(0, newCompiletimeArrayStateInitCall(mainReplay)); + mainBody.add(0, newCompiletimeStateInitCall(mainReplay)); } } - private int findLastArrayInitializer(ImFunction function, Set modifiedArrayInitializers) { + private int findLastInitializer(ImFunction function, Set modifiedInitializers) { if (function == null || function.getBody().isEmpty()) { return -1; } 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))) { + && modifiedInitializers.contains(function.getBody().get(i))) { insertionIndex = i; } } return insertionIndex; } - private ImFunctionCall newCompiletimeArrayStateInitCall(ImFunction replayFunction) { + private ImFunctionCall newCompiletimeStateInitCall(ImFunction replayFunction) { return JassIm.ImFunctionCall(imProg.getTrace(), replayFunction, JassIm.ImTypeArguments(), JassIm.ImExprs(), true, CallType.NORMAL); } @@ -670,15 +706,47 @@ private ImFunctionCall newCompiletimeArrayStateInitCall(ImFunction replayFunctio 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 runtimeScalarWrites = Collections.newSetFromMap(new IdentityHashMap<>()); + Set runtimeArrayWrites = findRuntimeWrites(runtimeScalarWrites); + List modifiedScalars = new ArrayList<>(globalState.getModifiedScalars()); 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 + Comparator stableGlobalOrder = Comparator .comparingInt((ImVar var) -> globalOrder.getOrDefault(var, Integer.MAX_VALUE)) - .thenComparing(ImVar::getName)); + .thenComparing(ImVar::getName); + modifiedScalars.sort(stableGlobalOrder); + for (ImVar var : modifiedScalars) { + if (!imProg.getGlobals().contains(var) || var.getType() instanceof ImArrayLikeType) { + continue; + } + StateReplayLocation replayLocation = findReplayTarget(var); + ImFunction replayFunction = getCompiletimeScalarStateInitFunction(replayLocation); + for (ProgramState.ScalarState state : globalState.getScalarStates(var)) { + if (!isPersistableCompiletimeValue(state.getValue())) { + String message = "Unsupported compiletime scalar value for " + var.getName() + + ": " + state.getValue(); + if (runtimeScalarWrites.contains(var)) { + WLogger.warning(message + "; runtime initialization remains authoritative (" + + sourceDiagnostic(var) + ")"); + continue; + } + throw new InterpreterException(var.getTrace(), message); + } + if (!state.isGeneric()) { + replayFunction.getBody().add(JassIm.ImSet(var.getTrace(), JassIm.ImVarAccess(var), + constantToExpr(var.getTrace(), state.getValue(), var.getType()))); + } else if (state.getTypeArguments().isEmpty()) { + throw new InterpreterException(var.getTrace(), + "Could not determine the generic specialization for compiletime scalar " + var.getName()); + } else { + emitCompiletimeGenericScalarState(replayFunction, var, state); + } + } + } + modifiedArrays.sort(stableGlobalOrder); for (ImVar var : modifiedArrays) { if (!imProg.getGlobals().contains(var)) { continue; @@ -686,28 +754,40 @@ private void emitCompiletimeState() { if (!(var.getType() instanceof ImArrayLikeType)) { continue; } - ArrayReplayLocation replayLocation = findArrayReplayTarget(var); + StateReplayLocation replayLocation = findReplayTarget(var); ImFunction replayFunction = getCompiletimeArrayStateInitFunction(replayLocation); + UnsupportedArrayEntries unsupportedEntries = new UnsupportedArrayEntries(); for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { if (!state.isGeneric()) { emitCompiletimeArrayEntries(replayFunction, var, state.getValue(), new ArrayList<>(), ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites, - state.getModifiedIndexes()); + state.getModifiedIndexes(), unsupportedEntries); } else if (state.getTypeArguments().isEmpty()) { throw new InterpreterException(var.getTrace(), "Could not determine the generic specialization for compiletime array " + var.getName()); } else { emitCompiletimeGenericArrayState(replayFunction, var, state, - ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); + ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites, unsupportedEntries); } } + if (!unsupportedEntries.isEmpty()) { + WLogger.warning("Compiletime array '" + var.getName() + "' contains " + + unsupportedEntries.count + " unsupported compiletime entries" + + " (first at index " + unsupportedEntries.firstIndexes + + ", value type " + unsupportedEntries.firstValueType + "); " + + "runtime initialization remains authoritative (" + sourceDiagnostic(var) + ")"); + } } } - private ArrayReplayLocation findArrayReplayTarget(ImVar var) { + private String sourceDiagnostic(ImVar var) { + return var.getTrace().attrSource().printShort(); + } + + private StateReplayLocation findReplayTarget(ImVar var) { List initializers = imProg.getGlobalInits().getOrDefault(var, Collections.emptyList()); if (initializers.isEmpty()) { - return new ArrayReplayLocation(null, Collections.emptySet()); + return new StateReplayLocation(null, Collections.emptySet()); } List candidates = new ArrayList<>(); candidates.add(translator.getGlobalInitFunc()); @@ -724,17 +804,35 @@ private ArrayReplayLocation findArrayReplayTarget(ImVar var) { } if (!matching.isEmpty()) { if (candidate != translator.getGlobalInitFunc()) { - return new ArrayReplayLocation(candidate, matching); + return new StateReplayLocation(candidate, matching); } - return new ArrayReplayLocation(null, Collections.emptySet()); + return new StateReplayLocation(null, Collections.emptySet()); } } - return new ArrayReplayLocation(null, Collections.emptySet()); + return new StateReplayLocation(null, Collections.emptySet()); + } + + private void emitCompiletimeGenericScalarState(ImFunction replayFunction, ImVar var, + ProgramState.ScalarState state) { + 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(), + "initCompiletimeScalarState_" + genericScalarStateInitCounter++, + JassIm.ImTypeVars(typeVars), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), + JassIm.ImStmts(JassIm.ImSet(var.getTrace(), JassIm.ImVarAccess(var), + constantToExpr(var.getTrace(), state.getValue(), var.getType()))), Collections.emptyList()); + imProg.getFunctions().add(replay); + scalarStateSplitTargets.add(replay); + replayFunction.getBody().add(JassIm.ImFunctionCall( + var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); } private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar var, ProgramState.ArrayState state, ImType entryType, - Set runtimeArrayWrites) { + Set runtimeArrayWrites, + UnsupportedArrayEntries unsupportedEntries) { List typeVars = new ArrayList<>(); for (int i = 0; i < state.getTypeArguments().size(); i++) { typeVars.add(JassIm.ImTypeVar("T" + i)); @@ -746,7 +844,7 @@ private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar v imProg.getFunctions().add(replay); arrayStateSplitTargets.add(replay); emitCompiletimeArrayEntries(replay, var, state.getValue(), new ArrayList<>(), entryType, - runtimeArrayWrites, state.getModifiedIndexes()); + runtimeArrayWrites, state.getModifiedIndexes(), unsupportedEntries); if (!replay.getBody().isEmpty()) { replayFunction.getBody().add(JassIm.ImFunctionCall( var.getTrace(), replay, JassIm.ImTypeArguments(state.getTypeArguments()), JassIm.ImExprs(), true, CallType.NORMAL)); @@ -755,13 +853,15 @@ private void emitCompiletimeGenericArrayState(ImFunction replayFunction, ImVar v private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstArray values, List indexes, ImType entryType, Set runtimeArrayWrites, - Set> modifiedIndexes) { + Set> modifiedIndexes, + UnsupportedArrayEntries unsupportedEntries) { for (it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry entry : values.entries()) { 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, modifiedIndexes); + ((ImArrayLikeType) entryType).getEntryType(), runtimeArrayWrites, modifiedIndexes, + unsupportedEntries); } else if (!modifiedIndexes.contains(nextIndexes)) { continue; } else if (isPersistableCompiletimeValue(entry.getValue())) { @@ -780,8 +880,7 @@ private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstAr .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()); + unsupportedEntries.add(nextIndexes, entry.getValue()); } else { throw new InterpreterException(var.getTrace(), message); } @@ -789,8 +888,26 @@ private void emitCompiletimeArrayEntries(ImFunction target, ImVar var, ILconstAr } } - private Set findRuntimeArrayWrites() { - Set result = new HashSet<>(); + private static final class UnsupportedArrayEntries { + private int count; + private List firstIndexes; + private String firstValueType; + + private void add(List indexes, ILconst value) { + count++; + if (firstIndexes == null) { + firstIndexes = new ArrayList<>(indexes); + firstValueType = value.getClass().getSimpleName(); + } + } + + private boolean isEmpty() { + return count == 0; + } + } + + private Set findRuntimeWrites(Set runtimeScalarWrites) { + Set runtimeArrayWrites = new HashSet<>(); Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); Deque pending = new ArrayDeque<>(translator.initFuncMap.values()); pending.add(translator.getMainFunc()); @@ -803,19 +920,21 @@ private Set findRuntimeArrayWrites() { @Override public void visit(ImSet set) { super.visit(set); - if (set.getLeft() instanceof ImVarArrayAccess) { + if (set.getLeft() instanceof ImVarAccess) { + runtimeScalarWrites.add(((ImVarAccess) set.getLeft()).getVar()); + } else if (set.getLeft() instanceof ImVarArrayAccess) { 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)); + runtimeArrayWrites.add(new RuntimeArrayWrite(access.getVar(), indexes)); } } }); pending.addAll(UsedFunctions.calculate(function)); } - return result; + return runtimeArrayWrites; } private static final class RuntimeArrayWrite { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 2d3365173..28ede72fb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -140,7 +140,7 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l } else { throw new InterpreterException(globalState, "Variable " + var.getName() + " is not initialized."); } - globalState.setVal(var, r); + globalState.setValUntracked(var, r); } return r; } 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 9d47a2c6b..ba3a5000a 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 @@ -39,6 +39,9 @@ public class ProgramState extends State implements AutoCloseable { private final Map genericStaticOwner = new HashMap<>(); + private final Set modifiedScalars = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set modifiedGenericScalars = new HashSet<>(); + private final Map> genericScalarTypeArguments = new HashMap<>(); private final Object2ObjectOpenHashMap genericStaticArrays = new Object2ObjectOpenHashMap<>(); private final Set modifiedGenericArrays = new HashSet<>(); private final Map>> modifiedGenericArrayIndexes = new HashMap<>(); @@ -681,10 +684,23 @@ private static String vid(ImVar v) { @Override public void setVal(ImVar v, ILconst val) { + modifiedScalars.add(v); String key = genericStaticKey(v); if (key != null) { WLogger.trace(() -> "[GENSTATIC] set " + key + " = " + val); genericStaticScalarVals.put(key, val); + modifiedGenericScalars.add(key); + genericScalarTypeArguments.computeIfAbsent(key, ignored -> genericStaticTypeArguments(v)); + return; + } + super.setVal(v, val); + } + + public void setValUntracked(ImVar v, ILconst val) { + String key = genericStaticKey(v); + if (key != null) { + WLogger.trace(() -> "[GENSTATIC] initialize " + key + " = " + val); + genericStaticScalarVals.put(key, val); return; } super.setVal(v, val); @@ -855,6 +871,57 @@ public Set> getModifiedIndexes() { } } + public static final class ScalarState { + private final ILconst value; + private final List typeArguments; + private final boolean generic; + + private ScalarState(ILconst value, List typeArguments, boolean generic) { + this.value = value; + this.typeArguments = Collections.unmodifiableList(new ArrayList<>(typeArguments)); + this.generic = generic; + } + + public ILconst getValue() { + return value; + } + + public List getTypeArguments() { + return typeArguments; + } + + public boolean isGeneric() { + return generic; + } + } + + public Set getModifiedScalars() { + return Collections.unmodifiableSet(modifiedScalars); + } + + public Collection getScalarStates(ImVar v) { + String prefix = v.getName() + "|"; + List keys = new ArrayList<>(modifiedGenericScalars); + Collections.sort(keys); + List result = new ArrayList<>(); + for (String key : keys) { + if (key.startsWith(prefix)) { + ILconst value = genericStaticScalarVals.get(key); + if (value != null) { + result.add(new ScalarState(value, + genericScalarTypeArguments.getOrDefault(key, Collections.emptyList()), true)); + } + } + } + if (result.isEmpty() && genericStaticKey(v) == null) { + ILconst value = getVal(v); + if (value != null) { + result.add(new ScalarState(value, Collections.emptyList(), false)); + } + } + return result; + } + public Collection getArrayStates(ImVar v) { String prefix = v.getName() + "|"; List keys = new ArrayList<>(modifiedGenericArrays); 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 fca1ff8a8..3571a06f7 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 @@ -1,7 +1,17 @@ package tests.wurstscript.tests; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + public class CompiletimeTests extends WurstScriptTest { @@ -62,6 +72,140 @@ public void testCompiletimeArray() { " testSuccess()"); } + @Test + public void testUnsupportedCompiletimeArrayWarningIsAggregatedAndReadable() { + Logger logger = (Logger) LoggerFactory.getLogger("default"); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + try { + test().withStdLib().testLua(true).luaOnly(true).runCompiletimeFunctions(true) + .lines("package Test", + "init", + " let _firstPlayer = players[0]"); + } finally { + logger.detachAppender(appender); + appender.stop(); + } + + List playerWarnings = appender.list.stream() + .map(ILoggingEvent::getFormattedMessage) + .filter(message -> message.contains("Player_players")) + .toList(); + assertEquals(playerWarnings.size(), 1, "expected one warning for the entire array"); + String warning = playerWarnings.get(0); + assertTrue(warning.contains("28 unsupported compiletime entries"), warning); + assertTrue(warning.contains("Player, line"), warning); + assertFalse(warning.contains("GlobalVarDef"), warning); + } + + @Test + public void testCompiletimePackageScalarState() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package A", + "public int source = 1", + "@compiletime function fill()", + " source = 42", + "endpackage", + "package B", + "import A", + "native testSuccess()", + "int observed = source", + "init", + " if source == 42 and observed == 42", + " testSuccess()"); + } + + @Test + public void testCompiletimeObjectAndNullScalarState() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "class A", + " int value", + "A source", + "string cleared = \"value\"", + "@compiletime function fill()", + " source = new A", + " source.value = 42", + " cleared = null", + "init", + " if source.value == 42 and cleared == null", + " testSuccess()"); + } + + @Test + public void testCompiletimeScalarReplayOnlyWrittenValues() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package A", + "public int seed = 1", + "init", + " seed = 2", + "endpackage", + "package B", + "import A", + "native testSuccess()", + "int observed = seed", + "int migrated = 0", + "@compiletime function fill()", + " let snapshot = observed", + " migrated = snapshot + 41", + "init", + " if observed == 2 and migrated == 42", + " testSuccess()"); + } + + @Test + public void testCompiletimeScalarRuntimeWriteRemainsAuthoritative() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "int source = 1", + "@compiletime function fill()", + " source = 42", + "init", + " source = 7", + " if source == 7", + " testSuccess()"); + } + + @Test + public void testCompiletimeClassStaticScalarState() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "class Counter", + " static int value = 1", + " static function setValue(int newValue)", + " value = newValue", + " static function getValue() returns int", + " return value", + "int observed = Counter.getValue()", + "@compiletime function fill()", + " Counter.setValue(42)", + "init", + " if Counter.getValue() == 42 and observed == 42", + " testSuccess()"); + } + + @Test + public void testCompiletimeGenericClassStaticScalarState() { + test().testLua(true).luaOnly(false).executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) + .lines("package Test", + "native testSuccess()", + "class Counter", + " static T value", + " static function setValue(T newValue)", + " value = newValue", + " static function getValue() returns T", + " return value", + "@compiletime function fill()", + " Counter.setValue(42)", + "init", + " if Counter.getValue() == 42", + " testSuccess()"); + } + @Test public void testCompiletimeArrayState() { test().executeProg(true).executeProgOnlyAfterTransforms().runCompiletimeFunctions(true) diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index 0f10bfab8..96ec963c3 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 @@ -106,6 +106,39 @@ public void compiletimeArrayReplaySplittingIsDeterministicAcrossPackages() { assertEquals("compiletime replay splitting must not depend on identity-hash iteration", first, second); } + @Test + public void compiletimeScalarReplaySplittingIsDeterministicAcrossPackages() { + RunArgs runArgs = new RunArgs().with( + "-lua", "-runcompiletimefunctions", "-functionSplitLimit", "1"); + String[] source = { + "package A", "public int a", "@compiletime function fillA()", " a = 10", "endpackage", + "package B", "public int b", "@compiletime function fillB()", " b = 20", "endpackage", + "package C", "public int c", "@compiletime function fillC()", " c = 30", "endpackage", + "package D", "public int d", "@compiletime function fillD()", " d = 40", "endpackage", + "package Test", "import A", "import B", "import C", "import D", "native testSuccess()", "init", + " if a + b + c + d == 100", " testSuccess()" + }; + + String first = compileLuaWithRunArgs("compiletimeScalarReplaySplittingIsDeterministicAcrossPackages", runArgs, source); + String second = compileLuaWithRunArgs("compiletimeScalarReplaySplittingIsDeterministicAcrossPackages", runArgs, source); + assertEquals("compiletime scalar replay splitting must be deterministic", first, second); + + java.util.regex.Matcher replayBody = java.util.regex.Pattern + .compile("function initCompiletimeScalarState[^\\n]*\\n(.*?)\\nend", java.util.regex.Pattern.DOTALL) + .matcher(first); + int persistedAssignments = 0; + while (replayBody.find()) { + int assignmentsInFunction = 0; + for (int value : new int[]{10, 20, 30, 40}) { + assignmentsInFunction += countOccurrences(replayBody.group(1), " = " + value); + } + assertTrue("each scalar replay leaf must honor the configured split limit:\n" + replayBody.group(), + assignmentsInFunction <= 1); + persistedAssignments += assignmentsInFunction; + } + assertEquals("all compiletime scalar values must still be emitted", 4, persistedAssignments); + } + @Test public void localPlayerEffectfulBooleanOperandSurvivesOptimization() { String compiled = compileOptimizedLua(