Skip to content

Commit c590db5

Browse files
committed
Analyze package constants in emitted init order
1 parent cdb3dd2 commit c590db5

4 files changed

Lines changed: 342 additions & 64 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java

Lines changed: 269 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,31 @@
33
import com.google.common.collect.Sets;
44
import de.peeeq.wurstscript.attributes.CompileError;
55
import de.peeeq.wurstscript.ast.GlobalVarDef;
6-
import de.peeeq.wurstscript.ast.InitBlock;
7-
import de.peeeq.wurstscript.ast.CompilationUnit;
8-
import de.peeeq.wurstscript.ast.WImport;
9-
import de.peeeq.wurstscript.ast.WEntity;
10-
import de.peeeq.wurstscript.ast.WPackage;
116
import de.peeeq.wurstscript.jassIm.*;
127
import de.peeeq.wurstscript.translation.imtranslation.ImHelper;
138
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
149
import de.peeeq.wurstscript.utils.Utils;
1510
import de.peeeq.wurstscript.validation.NamePreservation;
1611
import org.jetbrains.annotations.Nullable;
1712

13+
import java.util.ArrayDeque;
1814
import java.util.ArrayList;
15+
import java.util.BitSet;
16+
import java.util.Collection;
1917
import java.util.Collections;
2018
import java.util.IdentityHashMap;
2119
import java.util.List;
20+
import java.util.Map;
2221
import java.util.Optional;
2322
import java.util.Set;
2423

2524
public class GlobalsInliner implements OptimizerPass {
26-
private final Set<WPackage> initLaterPackages = Collections.newSetFromMap(new IdentityHashMap<>());
27-
private boolean initLaterPackagesCollected;
28-
25+
@Override
2926
public int optimize(ImTranslator trans) {
3027
int obsoleteCount = 0;
3128
ImProg prog = trans.getImProg();
3229
prog.clearAttributes(); // TODO only clear read/write attributes
30+
LiteralConstantAnalysis literalConstants = analyzeLiteralConstants(trans, prog);
3331

3432
Set<ImVar> obsoleteVars = Sets.newLinkedHashSet();
3533
for (final ImVar v : prog.getGlobals()) {
@@ -63,16 +61,21 @@ public int optimize(ImTranslator trans) {
6361
continue;
6462
}
6563

66-
boolean literalConstant = isLiteralConstantGlobal(v.getTrace(), prog);
64+
boolean literalConstant = literalConstants.safeConstants.contains(v);
6765
if (v.attrWrites().size() == 1 || literalConstant) {
6866
ImExpr right = null;
6967
ImVarWrite obs = null;
70-
for (ImVarWrite write : v.attrWrites()) {
71-
ImFunction func = write.getNearestFunc();
72-
if (isInInitGlobals(func) || (literalConstant && isLiteral(write.getRight()))) {
73-
right = write.getRight();
74-
obs = write;
75-
break;
68+
if (literalConstant) {
69+
obs = literalConstants.replacementWrites.get(v);
70+
right = obs.getRight();
71+
} else {
72+
for (ImVarWrite write : v.attrWrites()) {
73+
ImFunction func = write.getNearestFunc();
74+
if (isInInitGlobals(func)) {
75+
right = write.getRight();
76+
obs = write;
77+
break;
78+
}
7679
}
7780
}
7881
if (obs == null) {
@@ -176,69 +179,277 @@ private static boolean isInInitGlobals(ImFunction func) {
176179
}
177180

178181
/**
179-
* Package globals are initialized by package init functions, rather than initGlobals.
180-
* A source-level constant is immutable, but an earlier initializer in the same
181-
* package may still observe its default value before the constant is assigned.
182-
* Configurable constants stay runtime globals until configuration resolution owns them.
182+
* A package constant is assigned at runtime in a package initializer. Replacing all reads is
183+
* valid only when no startup path can observe the default value before that emitted assignment.
184+
* Analyze the actual IM startup order once, including transitive calls and function references,
185+
* rather than trying to reconstruct translation and dependency order from source positions.
183186
*/
184-
private boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element trace, ImProg prog) {
185-
if (!(trace instanceof GlobalVarDef)) {
186-
return false;
187+
private static LiteralConstantAnalysis analyzeLiteralConstants(ImTranslator trans, ImProg prog) {
188+
List<ImFunction> initializationOrder = trans.getInitializationOrder();
189+
IdentityHashMap<ImStmt, Integer> statementRanks = new IdentityHashMap<>();
190+
IdentityHashMap<ImVarWrite, Long> writeRanks = new IdentityHashMap<>();
191+
for (int functionRank = 0; functionRank < initializationOrder.size(); functionRank++) {
192+
ImFunction initializer = initializationOrder.get(functionRank);
193+
for (int statementRank = 0; statementRank < initializer.getBody().size(); statementRank++) {
194+
statementRanks.put(initializer.getBody().get(statementRank), statementRank);
195+
}
196+
int[] writeRank = {0};
197+
int currentFunctionRank = functionRank;
198+
initializer.getBody().accept(new ImStmt.DefaultVisitor() {
199+
@Override
200+
public void visit(ImSet write) {
201+
long rank = ((long) currentFunctionRank << 32) | (writeRank[0]++ & 0xffffffffL);
202+
writeRanks.put(write, rank);
203+
super.visit(write);
204+
}
205+
});
187206
}
188-
GlobalVarDef global = (GlobalVarDef) trace;
189-
if (!global.attrIsConstant() || global.hasAnnotation("@configurable")) {
190-
return false;
207+
208+
List<ImVar> candidates = new ArrayList<>();
209+
IdentityHashMap<ImVar, ImVarWrite> replacementWrites = new IdentityHashMap<>();
210+
for (ImVar var : prog.getGlobals()) {
211+
if (!isSourceConstant(var)) {
212+
continue;
213+
}
214+
ImVarWrite replacementWrite = null;
215+
ImExpr replacement = null;
216+
long replacementRank = Long.MAX_VALUE;
217+
boolean eligible = !var.attrWrites().isEmpty();
218+
for (ImVarWrite write : var.attrWrites()) {
219+
ImFunction initializer = write.getNearestFunc();
220+
ImStmt statement = initializer == null ? null
221+
: topLevelStatement((de.peeeq.wurstscript.jassIm.Element) write, initializer);
222+
Integer statementRank = statementRanks.get(statement);
223+
Long writeRank = writeRanks.get(write);
224+
if (statement == null || statementRank == null
225+
|| writeRank == null || !isLiteral(write.getRight())) {
226+
eligible = false;
227+
break;
228+
}
229+
if (replacement == null) {
230+
replacement = write.getRight();
231+
} else if (!replacement.structuralEquals(write.getRight())) {
232+
eligible = false;
233+
break;
234+
}
235+
if (writeRank < replacementRank) {
236+
replacementRank = writeRank;
237+
replacementWrite = write;
238+
}
239+
}
240+
if (eligible) {
241+
candidates.add(var);
242+
replacementWrites.put(var, replacementWrite);
243+
}
191244
}
192-
WPackage packageOfGlobal = packageOf(global);
193-
if (packageOfGlobal == null) {
194-
return true;
245+
if (candidates.isEmpty()) {
246+
return new LiteralConstantAnalysis(identitySet(), replacementWrites);
195247
}
196-
if (isInitializedLater(packageOfGlobal)) {
197-
return false;
248+
249+
Set<ImFunction> functions = identitySet();
250+
functions.addAll(ImHelper.calculateFunctionsOfProg(prog));
251+
functions.addAll(initializationOrder);
252+
IdentityHashMap<ImFunction, BitSet> readsByFunction = new IdentityHashMap<>();
253+
IdentityHashMap<ImStmt, BitSet> readsByStatement = new IdentityHashMap<>();
254+
IdentityHashMap<ImStmt, BitSet> writesByStatement = new IdentityHashMap<>();
255+
BitSet unsafe = new BitSet(candidates.size());
256+
257+
for (int i = 0; i < candidates.size(); i++) {
258+
ImVar candidate = candidates.get(i);
259+
for (ImVarRead read : candidate.attrReads()) {
260+
ImFunction function = read.getNearestFunc();
261+
if (function == null) {
262+
unsafe.set(i);
263+
continue;
264+
}
265+
readsByFunction.computeIfAbsent(function, ignored -> new BitSet()).set(i);
266+
ImStmt statement = topLevelStatement((de.peeeq.wurstscript.jassIm.Element) read, function);
267+
if (statement != null) {
268+
readsByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i);
269+
}
270+
}
271+
for (ImVarWrite write : candidate.attrWrites()) {
272+
ImFunction function = write.getNearestFunc();
273+
ImStmt statement = function == null ? null
274+
: topLevelStatement((de.peeeq.wurstscript.jassIm.Element) write, function);
275+
if (statement != null) {
276+
writesByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i);
277+
}
278+
}
279+
}
280+
281+
IdentityHashMap<ImFunction, Set<ImFunction>> callers = new IdentityHashMap<>();
282+
ArrayDeque<ImFunction> undiscovered = new ArrayDeque<>(functions);
283+
while (!undiscovered.isEmpty()) {
284+
ImFunction caller = undiscovered.removeFirst();
285+
readsByFunction.computeIfAbsent(caller, ignored -> new BitSet());
286+
for (ImFunction callee : caller.calcUsedFunctions()) {
287+
if (callee == null) {
288+
continue;
289+
}
290+
callers.computeIfAbsent(callee, ignored -> identitySet()).add(caller);
291+
if (functions.add(callee)) {
292+
undiscovered.addLast(callee);
293+
}
294+
}
295+
}
296+
297+
ArrayDeque<ImFunction> changedFunctions = new ArrayDeque<>();
298+
Set<ImFunction> queued = identitySet();
299+
for (Map.Entry<ImFunction, BitSet> entry : readsByFunction.entrySet()) {
300+
if (!entry.getValue().isEmpty()) {
301+
changedFunctions.addLast(entry.getKey());
302+
queued.add(entry.getKey());
303+
}
198304
}
199-
for (WEntity entity : packageOfGlobal.getElements()) {
200-
if (entity instanceof InitBlock
201-
&& entity.attrSource().getLeftPos() < global.attrSource().getLeftPos()) {
202-
return false;
305+
while (!changedFunctions.isEmpty()) {
306+
ImFunction callee = changedFunctions.removeFirst();
307+
queued.remove(callee);
308+
BitSet calleeReads = readsByFunction.get(callee);
309+
for (ImFunction caller : callers.getOrDefault(callee, Collections.emptySet())) {
310+
BitSet callerReads = readsByFunction.computeIfAbsent(caller, ignored -> new BitSet());
311+
int before = callerReads.cardinality();
312+
callerReads.or(calleeReads);
313+
if (callerReads.cardinality() != before && queued.add(caller)) {
314+
changedFunctions.addLast(caller);
315+
}
203316
}
204317
}
205-
for (ImVar other : prog.getGlobals()) {
206-
if (other.getTrace() instanceof GlobalVarDef
207-
&& packageOf((GlobalVarDef) other.getTrace()) == packageOfGlobal
208-
&& other.getTrace().attrSource().getLeftPos() < global.attrSource().getLeftPos()
209-
&& !other.attrWrites().isEmpty()) {
210-
return false;
318+
319+
BitSet pending = new BitSet(candidates.size());
320+
pending.set(0, candidates.size());
321+
ImFunction config = trans.getConfFunc();
322+
if (config != null) {
323+
scanStartupStatements(config.getBody(), pending, unsafe, readsByStatement, writesByStatement,
324+
readsByFunction);
325+
}
326+
if (!initializationOrder.isEmpty()) {
327+
scanStartupStatements(initializationOrder.get(0).getBody(), pending, unsafe, readsByStatement,
328+
writesByStatement, readsByFunction);
329+
scanMainPrefix(trans, initializationOrder, pending, unsafe, readsByStatement, writesByStatement,
330+
readsByFunction);
331+
}
332+
for (int i = 1; i < initializationOrder.size(); i++) {
333+
ImFunction initializer = initializationOrder.get(i);
334+
scanStartupStatements(initializer.getBody(), pending, unsafe, readsByStatement, writesByStatement,
335+
readsByFunction);
336+
}
337+
unsafe.or(pending);
338+
339+
Set<ImVar> safeConstants = identitySet();
340+
for (int i = 0; i < candidates.size(); i++) {
341+
if (!unsafe.get(i)) {
342+
safeConstants.add(candidates.get(i));
211343
}
212344
}
213-
return true;
345+
return new LiteralConstantAnalysis(safeConstants, replacementWrites);
214346
}
215347

216-
@Nullable
217-
private static WPackage packageOf(GlobalVarDef global) {
218-
de.peeeq.wurstscript.ast.Element element = global;
219-
while (element != null && !(element instanceof WPackage)) {
220-
element = element.getParent();
348+
private static void scanMainPrefix(ImTranslator trans, List<ImFunction> initializationOrder,
349+
BitSet pending, BitSet unsafe,
350+
Map<ImStmt, BitSet> readsByStatement,
351+
Map<ImStmt, BitSet> writesByStatement,
352+
Map<ImFunction, BitSet> readsByFunction) {
353+
Set<ImFunction> packageInitializers = identitySet();
354+
packageInitializers.addAll(initializationOrder.subList(1, initializationOrder.size()));
355+
for (ImStmt statement : trans.getMainFunc().getBody()) {
356+
Set<ImFunction> usedFunctions = directlyUsedFunctions(statement);
357+
if (!Collections.disjoint(usedFunctions, packageInitializers)) {
358+
return;
359+
}
360+
scanStartupStatements(Collections.singleton(statement), pending, unsafe, readsByStatement,
361+
writesByStatement, readsByFunction);
221362
}
222-
return (WPackage) element;
223363
}
224364

225-
private boolean isInitializedLater(WPackage target) {
226-
if (!initLaterPackagesCollected) {
227-
collectInitLaterPackages(target);
228-
initLaterPackagesCollected = true;
365+
private static void scanStartupStatements(Collection<ImStmt> statements, BitSet pending, BitSet unsafe,
366+
Map<ImStmt, BitSet> readsByStatement,
367+
Map<ImStmt, BitSet> writesByStatement,
368+
Map<ImFunction, BitSet> readsByFunction) {
369+
for (ImStmt statement : statements) {
370+
BitSet reads = readsByStatement.containsKey(statement)
371+
? (BitSet) readsByStatement.get(statement).clone() : new BitSet();
372+
for (ImFunction used : directlyUsedFunctions(statement)) {
373+
BitSet functionReads = readsByFunction.get(used);
374+
if (functionReads != null) {
375+
reads.or(functionReads);
376+
}
377+
}
378+
reads.and(pending);
379+
unsafe.or(reads);
380+
BitSet writes = writesByStatement.get(statement);
381+
if (writes != null) {
382+
pending.andNot(writes);
383+
}
229384
}
230-
return initLaterPackages.contains(target);
231385
}
232386

233-
private void collectInitLaterPackages(WPackage target) {
234-
for (CompilationUnit unit : target.getModel()) {
235-
for (WPackage candidate : unit.getPackages()) {
236-
for (WImport imported : candidate.getImports()) {
237-
if (imported.getIsInitLater() && imported.attrImportedPackage() instanceof WPackage importedPackage) {
238-
initLaterPackages.add(importedPackage);
387+
private static Set<ImFunction> directlyUsedFunctions(ImStmt statement) {
388+
Set<ImFunction> result = identitySet();
389+
statement.accept(new ImStmt.DefaultVisitor() {
390+
@Override
391+
public void visit(ImFunctionCall call) {
392+
super.visit(call);
393+
result.add(call.getFunc());
394+
}
395+
396+
@Override
397+
public void visit(ImFuncRef ref) {
398+
super.visit(ref);
399+
result.add(ref.getFunc());
400+
}
401+
402+
@Override
403+
public void visit(ImMethodCall call) {
404+
super.visit(call);
405+
if (call.getMethod().getImplementation() != null) {
406+
result.add(call.getMethod().getImplementation());
407+
}
408+
for (ImMethod subMethod : call.getMethod().getSubMethods()) {
409+
if (subMethod.getImplementation() != null) {
410+
result.add(subMethod.getImplementation());
239411
}
240412
}
241413
}
414+
});
415+
return result;
416+
}
417+
418+
@Nullable
419+
@SuppressWarnings("ReferenceEquality")
420+
private static ImStmt topLevelStatement(de.peeeq.wurstscript.jassIm.Element element, ImFunction function) {
421+
de.peeeq.wurstscript.jassIm.Element current = element;
422+
while (current != null && current.getParent() != function.getBody()) {
423+
current = current.getParent();
424+
}
425+
return current instanceof ImStmt ? (ImStmt) current : null;
426+
}
427+
428+
private static boolean isSourceConstant(ImVar var) {
429+
if (!(var.getTrace() instanceof GlobalVarDef)) {
430+
return false;
431+
}
432+
if (var.getName().equals("MagicFunctions_compiletime")
433+
|| var.getName().equals("MagicFunctions_isLua")) {
434+
// These values depend on compiler execution context/backend and are lowered by their
435+
// dedicated paths. They are not ordinary source literals for package-constant folding.
436+
return false;
437+
}
438+
GlobalVarDef global = (GlobalVarDef) var.getTrace();
439+
return global.attrIsConstant() && !global.hasAnnotation("@configurable");
440+
}
441+
442+
private static <T> Set<T> identitySet() {
443+
return Collections.newSetFromMap(new IdentityHashMap<>());
444+
}
445+
446+
private static final class LiteralConstantAnalysis {
447+
private final Set<ImVar> safeConstants;
448+
private final Map<ImVar, ImVarWrite> replacementWrites;
449+
450+
private LiteralConstantAnalysis(Set<ImVar> safeConstants, Map<ImVar, ImVarWrite> replacementWrites) {
451+
this.safeConstants = safeConstants;
452+
this.replacementWrites = replacementWrites;
242453
}
243454
}
244455

0 commit comments

Comments
 (0)