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..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 @@ -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()); @@ -120,6 +123,9 @@ public void run() { long tWriteback = System.nanoTime(); runDelayedActions(); emitCompiletimeObjectAllocs(); + if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { + insertCompiletimeArrayStateInitCalls(); + } long tDelayed = System.nanoTime(); partitionCompiletimeStateInitFunction(); @@ -192,11 +198,16 @@ private static long ms(long nanos) { } private void partitionCompiletimeStateInitFunction() { - if (compiletimeStateInitFunction == null) { - return; + if (compiletimeStateInitFunction != null) { + FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); + } + List splitTargets = new ArrayList<>(arrayStateSplitTargets); + splitTargets.sort(Comparator.comparing(ImFunction::getName)); + for (ImFunction arrayStateFunction : splitTargets) { + if (!arrayStateFunction.getBody().isEmpty()) { + FunctionSplitter.splitFunc(translator, arrayStateFunction); + } } - - FunctionSplitter.splitFunc(translator, compiletimeStateInitFunction); } private boolean isUnitTestMode() { @@ -396,6 +407,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) { @@ -404,11 +419,18 @@ private ImExpr constantToExpr(Element trace, ILconst value) { return JassIm.ImRealVal("" + ((ILconstReal) value).getVal()); } else if (value instanceof ILconstString) { return JassIm.ImStringVal(((ILconstString) value).getVal()); + } else if (value instanceof ILconstNull) { + 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( @@ -493,7 +515,33 @@ 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 List packageArrayStateReplays = new ArrayList<>(); + private final List arrayStateSplitTargets = new ArrayList<>(); + private int genericArrayStateInitCounter; private ImFunction getCompiletimeStateInitFunction() { ImFunction res = this.compiletimeStateInitFunction; @@ -535,6 +583,302 @@ private void addCompiletimeStateInit(ImStmt stmt) { getCompiletimeStateInitFunction().getBody().add(stmt); } + private ImFunction getCompiletimeArrayStateInitFunction(ArrayReplayLocation location) { + if (location.target == null && compiletimeArrayStateInitFunction != null) { + return compiletimeArrayStateInitFunction; + } + Element trace = imProg.getTrace(); + 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); + if (location.target == null) { + compiletimeArrayStateInitFunction = result; + } else { + packageArrayStateReplays.add(new PackageArrayStateReplay( + location.target, location.initializers, result)); + } + return result; + } + + private void insertCompiletimeArrayStateInitCalls() { + if (packageArrayStateReplays.isEmpty() && compiletimeArrayStateInitFunction == null) { + return; + } + ImFunction globalInitFunction = translator.getGlobalInitFunc(); + 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(packageReplay.target, packageReplay.initializers); + if (insertionIndex >= 0) { + packageReplay.target.getBody().add( + insertionIndex + 1, newCompiletimeArrayStateInitCall(packageReplay.replay)); + } + } + + ImFunction mainReplay = compiletimeArrayStateInitFunction; + 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(mainReplay)); + return; + } + } + } + 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)); + return; + } + } + mainBody.add(0, newCompiletimeArrayStateInitCall(mainReplay)); + } + } + + private int findLastArrayInitializer(ImFunction function, Set modifiedArrayInitializers) { + 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))) { + insertionIndex = i; + } + } + return insertionIndex; + } + + 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(); + 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; + } + ArrayReplayLocation replayLocation = findArrayReplayTarget(var); + ImFunction replayFunction = getCompiletimeArrayStateInitFunction(replayLocation); + for (ProgramState.ArrayState state : globalState.getArrayStates(var)) { + if (!state.isGeneric()) { + emitCompiletimeArrayEntries(replayFunction, var, state.getValue(), + 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()); + } else { + emitCompiletimeGenericArrayState(replayFunction, var, state, + ((ImArrayLikeType) var.getType()).getEntryType(), runtimeArrayWrites); + } + } + } + } + + private ArrayReplayLocation findArrayReplayTarget(ImVar var) { + List initializers = imProg.getGlobalInits().getOrDefault(var, Collections.emptyList()); + if (initializers.isEmpty()) { + return new ArrayReplayLocation(null, Collections.emptySet()); + } + 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()) { + return new ArrayReplayLocation(candidate, matching); + } + return new ArrayReplayLocation(null, Collections.emptySet()); + } + } + return new ArrayReplayLocation(null, Collections.emptySet()); + } + + 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)); + } + 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); + arrayStateSplitTargets.add(replay); + 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, + Set> modifiedIndexes) { + 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); + } 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, indexExpressions), + constantToExpr(var.getTrace(), entry.getValue(), entryType))); + } else { + String message = "Unsupported compiletime array entry at index " + entry.getIntKey() + + " (" + entry.getValue() + ")"; + 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()); + } else { + throw new InterpreterException(var.getTrace(), message); + } + } + } + } + + private Set findRuntimeArrayWrites() { + Set result = new HashSet<>(); + 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) { + 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)); + } + } + }); + pending.addAll(UsedFunctions.calculate(function)); + } + 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) { + return true; + } + if (value instanceof ILconstTuple) { + for (ILconst element : ((ILconstTuple) value).values()) { + if (!isPersistableCompiletimeValue(element)) return false; + } + return true; + } + if (value instanceof IlConstHandle) { + return ((IlConstHandle) value).getObj() instanceof LinkedListMultimap; + } + return false; + } + /** * 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..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 @@ -40,6 +40,9 @@ 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 Map>> modifiedGenericArrayIndexes = new HashMap<>(); + private final Map> genericArrayTypeArguments = new HashMap<>(); private final IdentityHashMap> genericStaticVals = new IdentityHashMap<>(); private final Object2ObjectOpenHashMap genericStaticScalarVals = new Object2ObjectOpenHashMap<>(); @@ -762,6 +765,117 @@ 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); + modifiedGenericArrayIndexes.computeIfAbsent(key, ignored -> new HashSet<>()) + .add(Collections.unmodifiableList(new ArrayList<>(indexes))); + 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 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(), 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() { + return value; + } + + public List getTypeArguments() { + return typeArguments; + } + + public boolean isGeneric() { + return generic; + } + + public Set> getModifiedIndexes() { + return modifiedIndexes; + } + } + + public Collection getArrayStates(ImVar v) { + String prefix = v.getName() + "|"; + 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(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(), false, getModifiedArrayIndexes(v))); + } + return result; + } + public Collection getAllObjects() { List values = new ArrayList<>(); 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..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,8 +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; /** * Lazily allocates internal maps ONLY when needed. @@ -19,6 +24,7 @@ public abstract class State { // in State: private @Nullable Object2ObjectOpenHashMap values; private @Nullable Object2ObjectOpenHashMap arrayValues; + private final Map>> modifiedArrayIndexes = new IdentityHashMap<>(); private Object2ObjectOpenHashMap ensureValues() { @@ -77,6 +83,8 @@ static ILconstArray createArrayConstantFromType(ImType vType) { } public void setArrayVal(ImVar v, List indexes, ILconst val) { + modifiedArrayIndexes.computeIfAbsent(v, ignored -> new HashSet<>()) + .add(Collections.unmodifiableList(new ArrayList<>(indexes))); ILconstArray ar = getArray(v); for (int i = 0; i < indexes.size() - 1; i++) { ar = (ILconstArray) ar.get(indexes.get(i)); @@ -84,6 +92,14 @@ public void setArrayVal(ImVar v, List indexes, ILconst val) { ar.set(indexes.get(indexes.size() - 1), val); } + public Set getModifiedArrays() { + return modifiedArrayIndexes.keySet(); + } + + public Set> getModifiedArrayIndexes(ImVar v) { + return modifiedArrayIndexes.getOrDefault(v, Collections.emptySet()); + } + public @Nullable ILconst getArrayVal(ImVar v, List indexes) { ILconstArray ar = getArray(v); for (int i = 0; i < indexes.size() - 1; i++) { 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/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index 02af4d00a..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 @@ -62,12 +62,222 @@ public void testCompiletimeArray() { " testSuccess()"); } + @Test + public void testCompiletimeArrayState() { + test().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 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 = [1]", + "@compiletime function fill()", + " source[0] = 42", + "init", + " if source[0] == 42", + " 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 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 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 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 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 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) + test().executeProg(true).executeProgOnlyAfterTransforms() .runCompiletimeFunctions(true) - .executeProgOnlyAfterTransforms() .lines("type agent extends handle", "type hashtable extends agent", "package Test", @@ -177,6 +387,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) 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(