Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package de.peeeq.wurstscript.translation.imtranslation;

import java.util.*;
import java.util.function.BooleanSupplier;
import java.util.function.Function;

/** Pass-local existential dependency facts. Cycles require a real intrinsic demand, not their own cached truth. */
final class SpecializationDemandGraph<T> {
record Facts<T>(boolean intrinsic, List<T> dependencies, BooleanSupplier valid) {
}

private final Function<T, Facts<T>> facts;
private final Map<T, Node> nodes = new IdentityHashMap<>();
private int validationRound;

private final class Node {
final T key;
final List<Node> dependencies = new ArrayList<>();
final List<Node> users = new ArrayList<>();
boolean built;
boolean intrinsic;
boolean demanded;
BooleanSupplier valid;
int validatedAt;

Node(T key) { this.key = key; }
}

SpecializationDemandGraph(Function<T, Facts<T>> facts) {
this.facts = facts;
}

boolean query(T key) {
Node root = node(key);
if (!root.built) materialize(root);
while (true) {
List<Node> changed = null;
int round = ++validationRound;
ArrayDeque<Node> work = new ArrayDeque<>();
work.add(root);
while (!work.isEmpty()) {
Node current = work.removeFirst();
if (current.validatedAt == round) continue;
current.validatedAt = round;
if (!current.valid.getAsBoolean()) {
if (changed == null) changed = new ArrayList<>();
changed.add(current);
}
work.addAll(current.dependencies);
}
if (changed == null) return root.demanded;
for (Node node : changed) rebuild(node);
// Rebuilding can introduce dependencies that were not part of the previous validation.
}
}

/** Rebuild this node's outgoing edges; its unchanged dependencies keep their already computed facts. */
private void rebuild(Node root) {
List<Node> previous = new ArrayList<>(root.dependencies);
boolean intrinsicBefore = root.intrinsic;
for (Node dependency : previous) dependency.users.remove(root);
root.dependencies.clear();
root.built = false;
materialize(root);
if (intrinsicBefore == root.intrinsic && previous.equals(root.dependencies)) return;

// A removed edge can leave a cycle incorrectly supporting its old true answer. Clear the complete
// reverse region, then derive its least fixed point from intrinsic and unaffected outside demands.
Set<Node> affected = new LinkedHashSet<>();
ArrayDeque<Node> work = new ArrayDeque<>();
work.add(root);
while (!work.isEmpty()) {
Node current = work.removeFirst();
if (affected.add(current)) work.addAll(current.users);
}
for (Node current : affected) current.demanded = false;
ArrayDeque<Node> demanded = new ArrayDeque<>();
for (Node current : affected) seed(current, demanded);
propagate(demanded);
}

private Node node(T key) {
Node result = nodes.get(key);
if (result == null) {
result = new Node(key);
nodes.put(key, result);
}
return result;
}

private void materialize(Node root) {
List<Node> added = new ArrayList<>();
ArrayDeque<Node> work = new ArrayDeque<>();
work.add(root);
while (!work.isEmpty()) {
Node current = work.removeFirst();
if (current.built) continue;
Facts<T> currentFacts = facts.apply(current.key);
current.valid = currentFacts.valid();
current.built = true;
current.intrinsic = currentFacts.intrinsic();
added.add(current);
if (current.intrinsic) continue;
Set<Node> seen = currentFacts.dependencies().size() > 1
? Collections.newSetFromMap(new IdentityHashMap<>()) : null;
for (T dependencyKey : currentFacts.dependencies()) {
Node dependency = node(dependencyKey);
if (seen != null && !seen.add(dependency)) continue;
current.dependencies.add(dependency);
dependency.users.add(current);
if (!dependency.built) work.add(dependency);
}
}
ArrayDeque<Node> demanded = new ArrayDeque<>();
for (Node current : added) seed(current, demanded);
propagate(demanded);
}

private void seed(Node node, ArrayDeque<Node> work) {
if (node.intrinsic) {
mark(node, work);
return;
}
for (Node dependency : node.dependencies) {
if (dependency.demanded) {
mark(node, work);
return;
}
}
}

private void mark(Node node, ArrayDeque<Node> work) {
if (!node.demanded) {
node.demanded = true;
work.add(node);
}
}

private void propagate(ArrayDeque<Node> work) {
while (!work.isEmpty()) {
for (Node user : work.removeFirst().users) mark(user, work);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package de.peeeq.wurstscript.translation.imtranslation;

import de.peeeq.wurstscript.ast.Ast;
import de.peeeq.wurstscript.jassIm.ImFunction;
import de.peeeq.wurstscript.jassIm.ImClass;
import de.peeeq.wurstscript.jassIm.JassIm;
import org.testng.annotations.Test;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static org.testng.Assert.*;

public class SpecializationDemandGraphTest {
private static final class Key {
boolean intrinsic;
List<Key> dependencies = List.of();
int version;
void setIntrinsic(boolean value) { intrinsic = value; version++; }
void setDependencies(Key... values) { dependencies = List.of(values); version++; }
SpecializationDemandGraph.Facts<Key> facts() {
int seen = version;
return new SpecializationDemandGraph.Facts<>(intrinsic, dependencies, () -> version == seen);
}
}

@Test
public void cycleNeedsAnExternalSeedAndLosesDemandWhenThatEdgeGoes() {
Key a = new Key(), b = new Key(), seed = new Key(), caller = new Key();
a.setDependencies(b);
b.setDependencies(a, seed);
caller.setDependencies(a);
var graph = new SpecializationDemandGraph<Key>(Key::facts);
assertFalse(graph.query(caller));
seed.setIntrinsic(true);

assertTrue(graph.query(caller));
assertTrue(graph.query(b));
b.setDependencies(a);

assertFalse(graph.query(a));
assertFalse(graph.query(b));
assertFalse(graph.query(caller));
assertTrue(graph.query(seed));
b.setDependencies(a, seed);

assertTrue(graph.query(caller));
}

@Test
public void diamondKeepsIndependentDemandAndRebuildsOnlyTheChangedNode() {
Key root = new Key(), left = new Key(), right = new Key(), seed = new Key();
seed.setIntrinsic(true);
root.setDependencies(left, right);
left.setDependencies(seed);
right.setDependencies(seed);
AtomicInteger builds = new AtomicInteger();
var graph = new SpecializationDemandGraph<Key>(key -> { builds.incrementAndGet(); return key.facts(); });
assertTrue(graph.query(root));
assertEquals(builds.get(), 4);
for (int i = 0; i < 100; i++) assertTrue(graph.query(root));
assertEquals(builds.get(), 4);
left.setDependencies();

assertTrue(graph.query(root));
assertEquals(builds.get(), 5);
right.setDependencies();

assertFalse(graph.query(root));
assertEquals(builds.get(), 6);
}

@Test
public void deepChainIsIterativeAndQueriesDoNotRebuildFacts() {
List<Key> chain = new ArrayList<>();
for (int i = 0; i < 20_000; i++) chain.add(new Key());
chain.getLast().setIntrinsic(true);
for (int i = 1; i < chain.size(); i++) chain.get(i - 1).setDependencies(chain.get(i));
AtomicInteger builds = new AtomicInteger();
var graph = new SpecializationDemandGraph<Key>(key -> { builds.incrementAndGet(); return key.facts(); });
for (Key key : chain) assertTrue(graph.query(key));
assertEquals(builds.get(), chain.size());
chain.getLast().setIntrinsic(false);
assertFalse(graph.query(chain.getFirst()));
assertEquals(builds.get(), chain.size() + 1);
}

@Test
public void aReferencedFunctionMutationIsDetectedWithoutChangingTheCaller() {
ImFunction caller = function("caller"), callee = function("callee");
var graph = new SpecializationDemandGraph<ImFunction>(function -> {
int version = function.modificationCount();
return new SpecializationDemandGraph.Facts<>(!function.getBody().isEmpty(),
function == caller ? List.of(callee) : List.of(), () -> function.modificationCount() == version);
});
assertFalse(graph.query(caller));
int callerVersion = caller.modificationCount();
callee.getBody().add(JassIm.ImReturn(Ast.NoExpr(), JassIm.ImIntVal(1)));
assertEquals(caller.modificationCount(), callerVersion);
assertTrue(graph.query(caller));
callee.getBody().removeAll();
assertFalse(graph.query(caller));
}

private static ImFunction function(String name) {
return JassIm.ImFunction(Ast.NoExpr(), name, JassIm.ImTypeVars(), JassIm.ImVars(),
JassIm.ImSimpleType("integer"), JassIm.ImVars(), JassIm.ImStmts(), List.of());
}

@Test
public void generatedClassListsHaveStableIdentitySnapshots() {
ImClass clazz = clazz("C"), base = clazz("Base");
assertTrue(EliminateGenerics.classMembersUnchanged(clazz, List.of(), List.of(), List.of()));
clazz.getSuperClasses().add(JassIm.ImClassType(base, JassIm.ImTypeArguments()));
assertFalse(EliminateGenerics.classMembersUnchanged(clazz, List.of(), List.of(), List.of()));
assertTrue(EliminateGenerics.classMembersUnchanged(clazz, List.of(), List.of(), List.of(base)));
ImFunction implementation = function("implementation");
var method = JassIm.ImMethod(Ast.NoExpr(), JassIm.ImClassType(clazz, JassIm.ImTypeArguments()),
"method", implementation, new ArrayList<>(), new ArrayList<>(), "", false);
clazz.getMethods().add(method);
assertFalse(EliminateGenerics.classMembersUnchanged(clazz, List.of(), List.of(), List.of(base)));
assertTrue(EliminateGenerics.classMembersUnchanged(clazz, List.of(method), List.of(implementation), List.of(base)));
method.setImplementation(function("replacement"));
assertFalse(EliminateGenerics.classMembersUnchanged(clazz, List.of(method), List.of(implementation), List.of(base)));
}

private static ImClass clazz(String name) {
return JassIm.ImClass(Ast.NoExpr(), name, JassIm.ImTypeVars(), JassIm.ImVars(),
JassIm.ImMethods(), JassIm.ImFunctions(), new ArrayList<>());
}

@Test
public void aNewDependencyIsValidatedEvenIfItWasOutsideTheOldClosure() {
Key root = new Key(), dependency = new Key();
dependency.setIntrinsic(true);
var graph = new SpecializationDemandGraph<Key>(Key::facts);
assertFalse(graph.query(root));
assertTrue(graph.query(dependency));
dependency.setIntrinsic(false);
root.setDependencies(dependency);
assertFalse(graph.query(root));
}

@Test
public void changingCyclicGraphsAgreeWithIndependentSearches() {
Random random = new Random(17);
List<Key> keys = new ArrayList<>();
for (int i = 0; i < 64; i++) keys.add(new Key());
for (Key key : keys) {
key.setIntrinsic(random.nextInt(8) == 0);
key.setDependencies(keys.get(random.nextInt(keys.size())), keys.get(random.nextInt(keys.size())));
}
AtomicInteger builds = new AtomicInteger();
var graph = new SpecializationDemandGraph<Key>(key -> { builds.incrementAndGet(); return key.facts(); });
for (int step = 0; step <= 200; step++) {
if (step > 0) {
Key changed = keys.get(random.nextInt(keys.size()));
if (random.nextBoolean()) changed.setIntrinsic(!changed.intrinsic);
else changed.setDependencies(keys.get(random.nextInt(keys.size())), keys.get(random.nextInt(keys.size())));

}
for (Key root : keys) {
Set<Key> visited = Collections.newSetFromMap(new IdentityHashMap<>());
ArrayDeque<Key> work = new ArrayDeque<>();
work.add(root);
boolean expected = false;
while (!work.isEmpty()) {
Key current = work.removeFirst();
if (!visited.add(current)) continue;
if (current.intrinsic) { expected = true; break; }
work.addAll(current.dependencies);
}
assertEquals(graph.query(root), expected, "after dependency update " + step);
}
}
assertEquals(builds.get(), keys.size() + 200);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@
*/
public class TypeClassTests extends WurstScriptTest {

@Test
public void recursiveSpecializationDemandFromBothEntrypoints() {
test().testLua(true).luaOnly(false).executeProg().lines(
"package test",
"native testSuccess()",
"interface ToIndex<T:>",
" function toIndex(T x) returns int",
"implements ToIndex<int>",
" function toIndex(int x) returns int",
" return x * 2",
"function first<Q: ToIndex>(Q x, int depth) returns int",
" if depth == 0",
" return Q.toIndex(x)",
" return second(x, depth - 1)",
"function second<Q: ToIndex>(Q x, int depth) returns int",
" return first(x, depth)",
"function erasedFirst<Q:>(Q x, int depth) returns int",
" if depth == 0",
" return 42",
" return erasedSecond(x, depth - 1)",
"function erasedSecond<Q:>(Q x, int depth) returns int",
" return erasedFirst(x, depth)",
"init",
" if second(21, 2) == 42 and first(21, 2) == 42",
" if erasedSecond(\"a\", 2) == 42 and erasedFirst(7, 2) == 42",
" testSuccess()"
);
}

@Test
public void parseInstanceDecl() {
testAssertOkLines(false,
Expand Down
Loading
Loading