Compare commits

...
1 Commits
Author SHA1 Message Date
julian 3bd19da742 prototype implementation of scc unification
Build and Test with Maven / Build-and-test-with-Maven (push) Failing after 43m51s
2026-09-01 10:44:37 +02:00
11 changed files with 905 additions and 7 deletions
@@ -153,6 +153,7 @@ public abstract class GenerateGenerics implements IGenerics {
}
Target.logger.info("Simplified constraints: " + simplifiedConstraints);
}
public record GenericsState(Map<TPH, RefTypeOrTPHOrWildcardOrGeneric> concreteTypes, Map<TypePlaceholder, TypePlaceholder> equality) {}
@@ -0,0 +1,311 @@
package de.dhbwstuttgart.typeinference.dependency;
import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.AbstractASTWalker;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Constructor;
import de.dhbwstuttgart.syntaxtree.Method;
import de.dhbwstuttgart.syntaxtree.statement.BinaryExpr;
import de.dhbwstuttgart.syntaxtree.statement.BoolExpression;
import de.dhbwstuttgart.syntaxtree.statement.CastExpr;
import de.dhbwstuttgart.syntaxtree.statement.DoStmt;
import de.dhbwstuttgart.syntaxtree.statement.Expression;
import de.dhbwstuttgart.syntaxtree.statement.ExpressionReceiver;
import de.dhbwstuttgart.syntaxtree.statement.ForEachStmt;
import de.dhbwstuttgart.syntaxtree.statement.ForStmt;
import de.dhbwstuttgart.syntaxtree.statement.IfStmt;
import de.dhbwstuttgart.syntaxtree.statement.InstanceOf;
import de.dhbwstuttgart.syntaxtree.statement.MethodCall;
import de.dhbwstuttgart.syntaxtree.statement.NewClass;
import de.dhbwstuttgart.syntaxtree.statement.Receiver;
import de.dhbwstuttgart.syntaxtree.statement.Statement;
import de.dhbwstuttgart.syntaxtree.statement.StaticClassName;
import de.dhbwstuttgart.syntaxtree.statement.Super;
import de.dhbwstuttgart.syntaxtree.statement.SuperCall;
import de.dhbwstuttgart.syntaxtree.statement.This;
import de.dhbwstuttgart.syntaxtree.statement.ThisCall;
import de.dhbwstuttgart.syntaxtree.statement.Throw;
import de.dhbwstuttgart.syntaxtree.statement.WhileStmt;
import de.dhbwstuttgart.syntaxtree.type.RefType;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.syntaxtree.type.TypePlaceholder;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* --- Comment AI generated ---
* Builds the method/constructor call graph for the classes currently being compiled together --
* the input to splitting whole-program type inference into per-strongly-connected-component
* batches, so that only mutually recursive methods have to be inferred jointly.
*
* Scope, deliberately:
* <ul>
* <li>Nodes are methods and constructors with a body, declared directly in one of
* {@code definedClasses}. Field initializers are NOT modeled as graph nodes here: a field is
* never generalized (it has exactly one type, never a per-use-site instantiated scheme like a
* method does), so it never needs SCC/mutual-recursion treatment -- only a simple topological
* position relative to whatever it calls, which a separate, much simpler pass can handle.</li>
* <li>Inherited method copies ({@code Method.isInherited}, produced by
* {@code JavaTXCompiler.addMethods}) are skipped. This builder is meant to run right after
* parsing and before {@code addMethods} has run, so in practice those copies should not exist
* yet; the check is kept as a defensive no-op in case that ordering assumption changes.</li>
* <li>Call targets are resolved using ordinary Java scoping where the receiver's type is known
* syntactically without needing inference: member lookup through the current class's
* hierarchy for unqualified/{@code this}-qualified calls (a member always shadows a
* same-named import per JLS 15.12.1), direct resolution for {@code TypeName.foo()} calls,
* {@code new Foo()}, and receivers with an explicit declared type. Only when the receiver's
* own type is itself not yet known (an omitted/inferred TPH -- e.g. the result of a chained
* call) does resolution fall back to a conservative name+arity match across all classes being
* compiled. That fallback can only ever add edges that don't exist at runtime
* (over-approximation, which just merges SCCs unnecessarily); it can never omit a real edge,
* which is the property that actually matters for soundness.</li>
* </ul>
*/
public class CallGraphBuilder {
private final JavaTXCompiler compiler;
private final Set<ClassOrInterface> definedClasses;
private final IdentityHashMap<Method, DependencyNode> nodes = new IdentityHashMap<>();
public CallGraphBuilder(JavaTXCompiler compiler, Set<ClassOrInterface> definedClasses) {
this.compiler = compiler;
this.definedClasses = definedClasses;
}
public DependencyGraph build() {
for (ClassOrInterface cl : definedClasses) {
for (Method m : cl.getMethods()) {
if (!m.isInherited && m.block != null) nodeFor(cl, m);
}
for (Constructor c : cl.getConstructors()) {
if (c.block != null) nodeFor(cl, c);
}
}
Map<DependencyNode, Set<DependencyNode>> edges = new LinkedHashMap<>();
for (Map.Entry<Method, DependencyNode> entry : nodes.entrySet()) {
DependencyNode from = entry.getValue();
EdgeCollector collector = new EdgeCollector(from.getOwner());
entry.getKey().block.accept(collector);
edges.put(from, collector.targets);
}
return new DependencyGraph(new LinkedHashSet<>(nodes.values()), edges);
}
private DependencyNode nodeFor(ClassOrInterface owner, Method m) {
return nodes.computeIfAbsent(m, mm -> new DependencyNode(owner, mm));
}
private ClassOrInterface resolve(JavaClassName name) {
return compiler.getClass(name);
}
private ClassOrInterface typeToClass(RefTypeOrTPHOrWildcardOrGeneric type) {
return type instanceof RefType rt ? resolve(rt.getName()) : null;
}
private ClassOrInterface superClassOf(ClassOrInterface cl) {
return cl.getSuperClass() == null ? null : resolve(cl.getSuperClass().getName());
}
private List<ClassOrInterface> hierarchyOf(ClassOrInterface start) {
List<ClassOrInterface> result = new ArrayList<>();
Set<JavaClassName> visited = new HashSet<>();
Deque<ClassOrInterface> queue = new ArrayDeque<>();
queue.add(start);
while (!queue.isEmpty()) {
ClassOrInterface cl = queue.poll();
if (cl == null || !visited.add(cl.getClassName())) continue;
result.add(cl);
if (cl.getSuperClass() != null) queue.add(resolve(cl.getSuperClass().getName()));
for (RefType parent : cl.getSuperInterfaces()) {
queue.add(resolve(parent.getName()));
}
}
return result;
}
private boolean matches(Method m, String name, int arity) {
return m.getName().equals(name) && m.getParameterList().getFormalparalist().size() == arity;
}
private boolean hierarchyDeclaresName(ClassOrInterface start, String name) {
for (ClassOrInterface cl : hierarchyOf(start)) {
for (Method m : cl.getMethods()) {
if (!m.isInherited && m.getName().equals(name)) return true;
}
}
return false;
}
private Set<DependencyNode> resolveInHierarchy(ClassOrInterface start, String name, int arity) {
Set<DependencyNode> found = new LinkedHashSet<>();
for (ClassOrInterface cl : hierarchyOf(start)) {
if (!definedClasses.contains(cl)) continue;
for (Method m : cl.getMethods()) {
if (!m.isInherited && m.block != null && matches(m, name, arity)) found.add(nodeFor(cl, m));
}
}
return found;
}
private Set<DependencyNode> resolveConservatively(String name, int arity) {
Set<DependencyNode> found = new LinkedHashSet<>();
for (ClassOrInterface cl : definedClasses) {
for (Method m : cl.getMethods()) {
if (!m.isInherited && m.block != null && matches(m, name, arity)) found.add(nodeFor(cl, m));
}
}
return found;
}
private ClassOrInterface receiverClass(Receiver receiver, ClassOrInterface owner) {
if (receiver instanceof StaticClassName scn) {
return typeToClass(scn.getType());
}
if (receiver instanceof ExpressionReceiver er) {
Expression inner = er.expr;
if (inner instanceof This) return owner;
if (inner instanceof Super) return superClassOf(owner);
if (inner instanceof NewClass nc) return typeToClass(nc.getType());
// Any other expression (local var, field, parameter, ...): only resolvable when its
// own declared type is an explicit RefType rather than an omitted/inferred TPH.
if (!(inner.getType() instanceof TypePlaceholder)) return typeToClass(inner.getType());
}
return null;
}
private class EdgeCollector extends AbstractASTWalker {
private final ClassOrInterface owner;
final Set<DependencyNode> targets = new LinkedHashSet<>();
EdgeCollector(ClassOrInterface owner) {
this.owner = owner;
}
@Override
public void visit(MethodCall methodCall) {
super.visit(methodCall);
int arity = methodCall.getArgumentList().getArguments().size();
targets.addAll(resolveCall(methodCall.receiver, methodCall.name, arity));
}
@Override
public void visit(BinaryExpr binary) {
binary.lexpr.accept(this);
binary.rexpr.accept(this);
}
@Override
public void visit(BoolExpression logical) {
logical.lexpr.accept(this);
logical.rexpr.accept(this);
}
@Override
public void visit(CastExpr castExpr) {
castExpr.expr.accept(this);
}
@Override
public void visit(InstanceOf instanceOf) {
instanceOf.getExpression().accept(this);
}
@Override
public void visit(Throw aThrow) {
aThrow.expr.accept(this);
}
@Override
public void visit(IfStmt ifStmt) {
ifStmt.expr.accept(this);
super.visit(ifStmt);
}
@Override
public void visit(WhileStmt whileStmt) {
whileStmt.expr.accept(this);
super.visit(whileStmt);
}
@Override
public void visit(DoStmt doStmt) {
doStmt.expr.accept(this);
super.visit(doStmt);
}
@Override
public void visit(ForStmt forStmt) {
for (Statement s : forStmt.initializer) s.accept(this);
if (forStmt.condition != null) forStmt.condition.accept(this);
for (Expression e : forStmt.loopExpr) e.accept(this);
super.visit(forStmt);
}
@Override
public void visit(ForEachStmt forEachStmt) {
forEachStmt.expression.accept(this);
super.visit(forEachStmt);
}
@Override
public void visit(SuperCall superCall) {
superCall.getArgumentList().accept(this);
ClassOrInterface parent = superClassOf(owner);
if (parent != null && definedClasses.contains(parent)) {
int arity = superCall.getArgumentList().getArguments().size();
for (Constructor c : parent.getConstructors()) {
if (c.getParameterList().getFormalparalist().size() == arity) targets.add(nodeFor(parent, c));
}
}
}
@Override
public void visit(ThisCall thisCall) {
thisCall.getArgumentList().accept(this);
int arity = thisCall.getArgumentList().getArguments().size();
for (Constructor c : owner.getConstructors()) {
if (c.getParameterList().getFormalparalist().size() == arity) targets.add(nodeFor(owner, c));
}
}
@Override
public void visit(NewClass newClass) {
super.visit(newClass);
ClassOrInterface target = typeToClass(newClass.getType());
if (target != null && definedClasses.contains(target)) {
int arity = newClass.getArgumentList().getArguments().size();
for (Constructor c : target.getConstructors()) {
if (c.getParameterList().getFormalparalist().size() == arity) targets.add(nodeFor(target, c));
}
}
}
private Set<DependencyNode> resolveCall(Receiver receiver, String name, int arity) {
// Unqualified / this-qualified calls follow JLS 15.12.1: a member always shadows a
// same-named import, so search the current class's hierarchy first -- but unlike an
// explicitly typed receiver (below), finding nothing there is not a resolved "no
// edges", it means falling through to import resolution, approximated conservatively.
if (receiver instanceof ExpressionReceiver er && er.expr instanceof This) {
if (hierarchyDeclaresName(owner, name)) return resolveInHierarchy(owner, name, arity);
return resolveConservatively(name, arity);
}
ClassOrInterface known = receiverClass(receiver, owner);
if (known != null) return resolveInHierarchy(known, name, arity);
return resolveConservatively(name, arity);
}
}
}
@@ -0,0 +1,35 @@
package de.dhbwstuttgart.typeinference.dependency;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* A directed graph over DependencyNodes: an edge from A to B means A's body contains a
* call that may resolve to B
*/
public final class DependencyGraph {
private final Set<DependencyNode> nodes;
private final Map<DependencyNode, Set<DependencyNode>> edges;
public DependencyGraph(Set<DependencyNode> nodes, Map<DependencyNode, Set<DependencyNode>> edges) {
this.nodes = Collections.unmodifiableSet(new LinkedHashSet<>(nodes));
Map<DependencyNode, Set<DependencyNode>> copy = new LinkedHashMap<>();
for (DependencyNode n : this.nodes) {
copy.put(n, Collections.unmodifiableSet(new LinkedHashSet<>(edges.getOrDefault(n, Set.of()))));
}
this.edges = Collections.unmodifiableMap(copy);
}
public Set<DependencyNode> nodes() {
return nodes;
}
/** The nodes {@code node}'s body may call into. */
public Set<DependencyNode> callees(DependencyNode node) {
return edges.getOrDefault(node, Set.of());
}
}
@@ -0,0 +1,31 @@
package de.dhbwstuttgart.typeinference.dependency;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Method;
import java.util.Objects;
public final class DependencyNode {
private final ClassOrInterface owner;
private final Method method;
DependencyNode(ClassOrInterface owner, Method method) {
this.owner = Objects.requireNonNull(owner);
this.method = Objects.requireNonNull(method);
}
public ClassOrInterface getOwner() {
return owner;
}
public Method getMethod() {
return method;
}
@Override
public String toString() {
return owner.getClassName().getClassName() + "." + method.getName() + "/"
+ method.getParameterList().getFormalparalist().size();
}
}
@@ -0,0 +1,125 @@
package de.dhbwstuttgart.typeinference.dependency;
import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Method;
import de.dhbwstuttgart.syntaxtree.SourceFile;
import de.dhbwstuttgart.syntaxtree.factory.UnifyTypeFactory;
import de.dhbwstuttgart.syntaxtree.visual.ASTTypePrinter;
import de.dhbwstuttgart.typeinference.result.ResultPair;
import de.dhbwstuttgart.typeinference.result.ResultSet;
import de.dhbwstuttgart.typeinference.typeAlgo.TYPE;
import de.dhbwstuttgart.typeinference.unify.PlaceholderRegistry;
import de.dhbwstuttgart.typeinference.unify.TypeUnify;
import de.dhbwstuttgart.typeinference.unify.UnifyContext;
import de.dhbwstuttgart.typeinference.unify.UnifyResultListenerImpl;
import de.dhbwstuttgart.typeinference.unify.UnifyResultModel;
import de.dhbwstuttgart.typeinference.unify.UnifyTaskModel;
import de.dhbwstuttgart.typeinference.unify.model.FiniteClosure;
import de.dhbwstuttgart.typeinference.unify.model.PlaceholderType;
import de.dhbwstuttgart.typeinference.unify.model.UnifyPair;
import de.dhbwstuttgart.util.Logger;
import java.io.File;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public class GeneralizingBytecodeDemo {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("usage: GeneralizingBytecodeDemo <file1.jav> [file2.jav ...]");
return;
}
List<File> files = Arrays.stream(args).map(File::new).toList();
JavaTXCompiler compiler = new JavaTXCompiler(files);
compiler.parseAll();
Set<ClassOrInterface> definedClasses = new HashSet<>();
Set<ClassOrInterface> allClasses = new HashSet<>();
for (File f : files) {
SourceFile sf = compiler.sourceFiles.get(f);
definedClasses.addAll(sf.KlassenVektor);
allClasses.addAll(compiler.getAvailableClasses(f));
allClasses.addAll(sf.availableClasses);
}
allClasses.removeAll(definedClasses);
allClasses.addAll(definedClasses);
DependencyGraph graph = new CallGraphBuilder(compiler, definedClasses).build();
List<Set<DependencyNode>> sccs = StronglyConnectedComponents.compute(graph);
TYPE ty = new TYPE(definedClasses, allClasses);
Logger logger = new Logger("GeneralizingBytecodeDemo");
PlaceholderRegistry placeholderRegistry = new PlaceholderRegistry();
FiniteClosure finiteClosure = UnifyTypeFactory.generateFC(
allClasses.stream().toList(), logger, compiler.getClassLoader(), compiler, placeholderRegistry);
UnifyTaskModel usedTasks = new UnifyTaskModel();
Function<UnifyPair, UnifyPair> distributeInnerVars = x -> {
var lhs = x.getLhsType();
var rhs = x.getRhsType();
if (lhs instanceof PlaceholderType lp && rhs instanceof PlaceholderType rp && (lp.isInnerType() || rp.isInnerType())) {
lp.setInnerType(true);
rp.setInnerType(true);
}
return x;
};
Set<ResultPair> combined = new LinkedHashSet<>();
System.out.println("Solving " + sccs.size() + " SCCs independently, materializing each into the AST before the next SCC runs:");
for (int i = 0; i < sccs.size(); i++) {
Set<DependencyNode> component = sccs.get(i);
List<Map.Entry<ClassOrInterface, Method>> group = component.stream()
.<Map.Entry<ClassOrInterface, Method>>map(n -> new AbstractMap.SimpleEntry<>(n.getOwner(), n.getMethod()))
.toList();
var cons = ty.getConstraintsFor(group);
var unifyCons = UnifyTypeFactory.convert(compiler, cons, placeholderRegistry).map(distributeInnerVars);
UnifyResultModel urm = new UnifyResultModel(cons, finiteClosure);
UnifyResultListenerImpl listener = new UnifyResultListenerImpl();
urm.addUnifyResultListener(listener);
UnifyContext context = new UnifyContext(logger, true, urm, usedTasks, placeholderRegistry);
TypeUnify.unifyParallel(unifyCons.getUndConstraints(), unifyCons.getOderConstraints(), finiteClosure, context);
System.out.println("[" + i + "] " + component);
List<ResultSet> results = listener.getResults();
if (results.isEmpty()) {
System.out.println(" (no results)");
continue;
}
ResultSet sccResult = results.getFirst();
System.out.println(" isolated result: " + sccResult.getSortedResults());
combined.addAll(sccResult.results);
for (Map.Entry<ClassOrInterface, Method> entry : group) {
GeneralizingSccDemo.materialize(entry.getKey(), entry.getValue(), sccResult);
}
}
ResultSet whole = new ResultSet(combined);
System.out.println();
System.out.println("Source after materialization:");
for (File f : files) {
System.out.println(ASTTypePrinter.print(compiler.sourceFiles.get(f)));
}
System.out.println("Generating bytecode from the union of all SCCs' isolated ResultSets ...");
for (File f : files) {
SourceFile sf = compiler.sourceFiles.get(f);
var classes = compiler.generateBytecode(sf, List.of(whole));
compiler.writeClassFile(classes, f);
System.out.println("wrote " + classes.keySet());
}
}
}
@@ -0,0 +1,252 @@
package de.dhbwstuttgart.typeinference.dependency;
import de.dhbwstuttgart.core.JavaTXCompiler;
import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.ClassOrInterface;
import de.dhbwstuttgart.syntaxtree.Constructor;
import de.dhbwstuttgart.syntaxtree.GenericDeclarationList;
import de.dhbwstuttgart.syntaxtree.GenericTypeVar;
import de.dhbwstuttgart.syntaxtree.Method;
import de.dhbwstuttgart.syntaxtree.ParameterList;
import de.dhbwstuttgart.syntaxtree.Pattern;
import de.dhbwstuttgart.syntaxtree.SourceFile;
import de.dhbwstuttgart.syntaxtree.factory.UnifyTypeFactory;
import de.dhbwstuttgart.syntaxtree.type.ExtendsWildcardType;
import de.dhbwstuttgart.syntaxtree.type.GenericRefType;
import de.dhbwstuttgart.syntaxtree.type.RefType;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.syntaxtree.type.SuperWildcardType;
import de.dhbwstuttgart.syntaxtree.type.TypePlaceholder;
import de.dhbwstuttgart.syntaxtree.visual.ASTTypePrinter;
import de.dhbwstuttgart.typeinference.result.ResultPair;
import de.dhbwstuttgart.typeinference.result.ResultSet;
import de.dhbwstuttgart.typeinference.typeAlgo.TYPE;
import de.dhbwstuttgart.typeinference.unify.PlaceholderRegistry;
import de.dhbwstuttgart.typeinference.unify.TypeUnify;
import de.dhbwstuttgart.typeinference.unify.UnifyContext;
import de.dhbwstuttgart.typeinference.unify.UnifyResultListenerImpl;
import de.dhbwstuttgart.typeinference.unify.UnifyResultModel;
import de.dhbwstuttgart.typeinference.unify.UnifyTaskModel;
import de.dhbwstuttgart.typeinference.unify.model.FiniteClosure;
import de.dhbwstuttgart.typeinference.unify.model.PlaceholderType;
import de.dhbwstuttgart.typeinference.unify.model.UnifyPair;
import de.dhbwstuttgart.util.Logger;
import java.io.File;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public class GeneralizingSccDemo {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("usage: GeneralizingSccDemo <file1.jav> [file2.jav ...]");
return;
}
List<File> files = Arrays.stream(args).map(File::new).toList();
JavaTXCompiler compiler = new JavaTXCompiler(files);
compiler.parseAll();
Set<ClassOrInterface> definedClasses = new HashSet<>();
Set<ClassOrInterface> allClasses = new HashSet<>();
for (File f : files) {
SourceFile sf = compiler.sourceFiles.get(f);
definedClasses.addAll(sf.KlassenVektor);
allClasses.addAll(compiler.getAvailableClasses(f));
allClasses.addAll(sf.availableClasses);
}
allClasses.removeAll(definedClasses);
allClasses.addAll(definedClasses);
System.out.println("Source with TPH names (unresolved type slots only):");
for (File f : files) {
System.out.println(ASTTypePrinter.print(compiler.sourceFiles.get(f)));
}
DependencyGraph graph = new CallGraphBuilder(compiler, definedClasses).build();
List<Set<DependencyNode>> sccs = StronglyConnectedComponents.compute(graph);
TYPE ty = new TYPE(definedClasses, allClasses);
Logger logger = new Logger("GeneralizingSccDemo");
PlaceholderRegistry placeholderRegistry = new PlaceholderRegistry();
FiniteClosure finiteClosure = UnifyTypeFactory.generateFC(
allClasses.stream().toList(), logger, compiler.getClassLoader(), compiler, placeholderRegistry);
UnifyTaskModel usedTasks = new UnifyTaskModel();
Function<UnifyPair, UnifyPair> distributeInnerVars = x -> {
var lhs = x.getLhsType();
var rhs = x.getRhsType();
if (lhs instanceof PlaceholderType lp && rhs instanceof PlaceholderType rp && (lp.isInnerType() || rp.isInnerType())) {
lp.setInnerType(true);
rp.setInnerType(true);
}
return x;
};
System.out.println("Solving " + sccs.size() + " SCCs independently, materializing each into the AST before the next SCC runs:");
for (int i = 0; i < sccs.size(); i++) {
Set<DependencyNode> component = sccs.get(i);
List<Map.Entry<ClassOrInterface, Method>> group = component.stream()
.<Map.Entry<ClassOrInterface, Method>>map(n -> new AbstractMap.SimpleEntry<>(n.getOwner(), n.getMethod()))
.toList();
var cons = ty.getConstraintsFor(group);
var unifyCons = UnifyTypeFactory.convert(compiler, cons, placeholderRegistry).map(distributeInnerVars);
UnifyResultModel urm = new UnifyResultModel(cons, finiteClosure);
UnifyResultListenerImpl listener = new UnifyResultListenerImpl();
urm.addUnifyResultListener(listener);
UnifyContext context = new UnifyContext(logger, true, urm, usedTasks, placeholderRegistry);
TypeUnify.unifyParallel(unifyCons.getUndConstraints(), unifyCons.getOderConstraints(), finiteClosure, context);
System.out.println("[" + i + "] " + component + (component.size() > 1 ? " <- mutually recursive, solved jointly" : ""));
System.out.println(" raw constraints (pre-unify, AST-level types): " + cons);
System.out.println(" unify constraints (UnifyPair, and-constraints only): " + unifyCons.getUndConstraints());
if (!unifyCons.getOderConstraints().isEmpty()) {
System.out.println(" unify constraints (or-constraints): " + unifyCons.getOderConstraints());
}
List<ResultSet> results = listener.getResults();
if (results.isEmpty()) {
System.out.println(" (no results)");
continue;
}
ResultSet sccResult = results.getFirst();
System.out.println(" isolated result: " + sccResult.getSortedResults());
for (Map.Entry<ClassOrInterface, Method> entry : group) {
Method materialized = materialize(entry.getKey(), entry.getValue(), sccResult);
System.out.println(" materialized " + entry.getKey().getClassName().getClassName() + "." + materialized.getName()
+ ": generics=" + describeGenerics(materialized) + " params=" + describeParams(materialized)
+ " returns=" + describe(materialized.getReturnType()));
}
}
System.out.println();
System.out.println("Source after materialization (still prints TPH names for anything left, e.g. method bodies -- only signatures were rewritten):");
for (File f : files) {
System.out.println(ASTTypePrinter.print(compiler.sourceFiles.get(f)));
}
}
static Method materialize(ClassOrInterface owner, Method method, ResultSet sccResult) {
Set<TypePlaceholder> used = new LinkedHashSet<>();
for (Pattern p : method.getParameterList().getFormalparalist()) collectTPHs(p.getType(), used);
collectTPHs(method.getReturnType(), used);
if (used.isEmpty()) return method;
Set<TypePlaceholder> free = new LinkedHashSet<>();
for (TypePlaceholder tph : used) {
if (sccResult.resolveType(tph).resolvedType instanceof TypePlaceholder) free.add(tph);
}
UnionFind uf = new UnionFind();
for (ResultPair rp : sccResult.results) {
if (rp.getLeft() instanceof TypePlaceholder l && rp.getRight() instanceof TypePlaceholder r
&& free.contains(l) && free.contains(r)) {
uf.union(l, r);
}
}
Map<TypePlaceholder, List<TypePlaceholder>> groups = new LinkedHashMap<>();
for (TypePlaceholder tph : free) groups.computeIfAbsent(uf.find(tph), k -> new ArrayList<>()).add(tph);
Map<TypePlaceholder, RefTypeOrTPHOrWildcardOrGeneric> substitution = new HashMap<>();
List<GenericTypeVar> newGenerics = new ArrayList<>();
RefType objectBound = new RefType(new JavaClassName("java.lang.Object"), new NullToken());
for (List<TypePlaceholder> members : groups.values()) {
String genName = members.get(0).getName();
newGenerics.add(new GenericTypeVar(genName, List.of(objectBound), new NullToken(), new NullToken()));
for (TypePlaceholder member : members) substitution.put(member, new GenericRefType(genName, new NullToken()));
}
for (TypePlaceholder tph : used) {
if (!free.contains(tph)) substitution.put(tph, sccResult.resolveType(tph).resolvedType);
}
if (substitution.isEmpty()) return method;
List<Pattern> newParams = new ArrayList<>();
for (Pattern p : method.getParameterList().getFormalparalist()) {
newParams.add(p.withType(substitute(p.getType(), substitution)));
}
ParameterList newParamList = new ParameterList(newParams, method.getParameterList().getOffset());
RefTypeOrTPHOrWildcardOrGeneric newReturnType = substitute(method.getReturnType(), substitution);
GenericDeclarationList newGenericDecl = new GenericDeclarationList(newGenerics, new NullToken());
Method rewritten = method instanceof Constructor
? new Constructor(method.modifier, method.name, newReturnType, newParamList, method.block, newGenericDecl, method.getOffset())
: new Method(method.modifier, method.name, newReturnType, newParamList, method.block, newGenericDecl, method.getOffset());
replaceMethod(owner, method, rewritten);
return rewritten;
}
private static void collectTPHs(RefTypeOrTPHOrWildcardOrGeneric type, Set<TypePlaceholder> out) {
if (type instanceof TypePlaceholder tph) out.add(tph);
else if (type instanceof RefType rt) for (var p : rt.getParaList()) collectTPHs(p, out);
else if (type instanceof ExtendsWildcardType w) collectTPHs(w.getInnerType(), out);
else if (type instanceof SuperWildcardType w) collectTPHs(w.getInnerType(), out);
}
private static RefTypeOrTPHOrWildcardOrGeneric substitute(RefTypeOrTPHOrWildcardOrGeneric type, Map<TypePlaceholder, RefTypeOrTPHOrWildcardOrGeneric> sub) {
if (type instanceof TypePlaceholder tph) return sub.getOrDefault(tph, tph);
if (type instanceof RefType rt) return new RefType(rt.getName(), rt.getParaList().stream().map(p -> substitute(p, sub)).toList(), rt.getOffset());
if (type instanceof ExtendsWildcardType w) return new ExtendsWildcardType(substitute(w.getInnerType(), sub), w.getOffset());
if (type instanceof SuperWildcardType w) return new SuperWildcardType(substitute(w.getInnerType(), sub), w.getOffset());
return type;
}
private static void replaceMethod(ClassOrInterface owner, Method original, Method replacement) {
if (original instanceof Constructor) {
var list = owner.getConstructors();
for (int i = 0; i < list.size(); i++) if (list.get(i) == original) { list.set(i, (Constructor) replacement); return; }
} else {
var list = owner.getMethods();
for (int i = 0; i < list.size(); i++) if (list.get(i) == original) { list.set(i, replacement); return; }
}
throw new IllegalStateException("method not found in owner's own list: " + original);
}
private static String describeGenerics(Method m) {
List<String> names = new ArrayList<>();
m.getGenerics().forEach(gtv -> names.add(gtv.getName()));
return names.toString();
}
private static String describeParams(Method m) {
List<String> params = new ArrayList<>();
m.getParameterList().getFormalparalist().forEach(p -> params.add(describe(p.getType())));
return params.toString();
}
private static String describe(RefTypeOrTPHOrWildcardOrGeneric type) {
if (type instanceof GenericRefType g) return g.getParsedName().toString();
if (type instanceof TypePlaceholder tph) return "TPH " + tph.getName();
return type.toString();
}
private static final class UnionFind {
private final Map<TypePlaceholder, TypePlaceholder> parent = new HashMap<>();
TypePlaceholder find(TypePlaceholder x) {
parent.putIfAbsent(x, x);
TypePlaceholder p = parent.get(x);
if (p != x) { p = find(p); parent.put(x, p); }
return p;
}
void union(TypePlaceholder a, TypePlaceholder b) {
TypePlaceholder ra = find(a), rb = find(b);
if (ra != rb) parent.put(ra, rb);
}
}
}
@@ -0,0 +1,40 @@
package de.dhbwstuttgart.typeinference.dependency;
import de.dhbwstuttgart.syntaxtree.SourceFile;
import de.dhbwstuttgart.syntaxtree.type.TypePlaceholder;
import de.dhbwstuttgart.syntaxtree.visual.OutputGenerator;
import de.dhbwstuttgart.typeinference.result.ResultSet;
/**
* Prints a SourceFile the same way ASTTypePrinter/OutputGenerator do, except every TypePlaceholder
* the given ResultSet resolves is printed as its inferred type instead of "TPH <name>". Falls back
* to "TPH <name>" for any TPH the ResultSet doesn't cover (e.g. one that belongs to an SCC whose
* result wasn't merged in -- see SccUnifyDemo).
*/
public class ResolvedSourcePrinter extends OutputGenerator {
private final ResultSet resultSet;
private ResolvedSourcePrinter(StringBuilder out, ResultSet resultSet) {
super(out);
this.resultSet = resultSet;
}
public static String print(SourceFile sourceFile, ResultSet resultSet) {
StringBuilder out = new StringBuilder();
new ResolvedSourcePrinter(out, resultSet).visit(sourceFile);
return out.toString();
}
@Override
public void visit(TypePlaceholder typePlaceholder) {
var resolved = resultSet.resolveType(typePlaceholder).resolvedType;
if (resolved instanceof TypePlaceholder) {
out.append("TPH ").append(typePlaceholder.getName());
} else {
// resolved type may itself contain nested TPHs (e.g. a generic parameter) --
// recurse through the same visitor so those get resolved too, not just the outer type.
resolved.accept(this);
}
}
}
@@ -0,0 +1,82 @@
package de.dhbwstuttgart.typeinference.dependency;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* --- Comment AI generated ---
* Computes the strongly connected components of a {@link DependencyGraph} via Tarjan's
* algorithm, returning them in the order the SCC-scoped inference driver needs to process them:
* for every edge u -> v (u calls v) with u and v in different components, the component
* containing v appears BEFORE the component containing u -- i.e. a method's callees are always
* fully processed (and, in the inference driver, generalized) before the method itself.
*
* This is not a separate reversal step: it falls directly out of Tarjan's algorithm, since a
* component is only popped off the stack (and appended to the result) once the DFS has finished
* exploring all of its outgoing edges, which means every component it points to has already been
* popped.
*
* Implementation is recursive, so its stack depth is bounded by the depth of the call graph
* among the classes being compiled together, not by the size of the program. That is fine for
* realistic programs; a pathologically deep call chain could still overflow the JVM stack, same
* tradeoff most textbook Tarjan implementations make.
*/
public final class StronglyConnectedComponents {
private final DependencyGraph graph;
private final Map<DependencyNode, Integer> index = new HashMap<>();
private final Map<DependencyNode, Integer> lowlink = new HashMap<>();
private final Set<DependencyNode> onStack = new HashSet<>();
private final Deque<DependencyNode> stack = new ArrayDeque<>();
private final List<Set<DependencyNode>> result = new ArrayList<>();
private int counter = 0;
private StronglyConnectedComponents(DependencyGraph graph) {
this.graph = graph;
}
public static List<Set<DependencyNode>> compute(DependencyGraph graph) {
StronglyConnectedComponents scc = new StronglyConnectedComponents(graph);
for (DependencyNode node : graph.nodes()) {
if (!scc.index.containsKey(node)) {
scc.strongConnect(node);
}
}
return scc.result;
}
private void strongConnect(DependencyNode v) {
index.put(v, counter);
lowlink.put(v, counter);
counter++;
stack.push(v);
onStack.add(v);
for (DependencyNode w : graph.callees(v)) {
if (!index.containsKey(w)) {
strongConnect(w);
lowlink.put(v, Math.min(lowlink.get(v), lowlink.get(w)));
} else if (onStack.contains(w)) {
lowlink.put(v, Math.min(lowlink.get(v), index.get(w)));
}
}
if (lowlink.get(v).equals(index.get(v))) {
Set<DependencyNode> component = new LinkedHashSet<>();
DependencyNode w;
do {
w = stack.pop();
onStack.remove(w);
component.add(w);
} while (w != v);
result.add(component);
}
}
}
@@ -45,6 +45,26 @@ public class TYPE {
return ret;
}
/**
* Generates constraints for an explicit subset of methods/constructors (e.g. one strongly
* connected component of the call graph) instead of every method of every defined class.
* Uses the same TypeInferenceInformation (the whole program's classes, same as
* {@link #getConstraints()}) -- this does not change what a method's constraints can refer
* to, only which methods' constraints get generated and later unified together.
*/
public ConstraintSet<Pair> getConstraintsFor(Collection<Map.Entry<ClassOrInterface, Method>> group) {
ConstraintSet<Pair> ret = new ConstraintSet<>();
Set<ClassOrInterface> allClasses = TypeUnifyTaskHelper.getPresizedHashSet(allAvailableClasses.size());
allClasses.addAll(allAvailableClasses);
TypeInferenceInformation info = new TypeInferenceInformation(allClasses);
for (Map.Entry<ClassOrInterface, Method> entry : group) {
ClassOrInterface cl = entry.getKey();
Method m = entry.getValue();
ret.addAll(m instanceof Constructor c ? getConstraintsConstructor(c, info, cl) : getConstraintsMethod(m, info, cl));
}
return ret;
}
private ConstraintSet getConstraintsClass(ClassOrInterface cl, TypeInferenceInformation info) {
ConstraintSet ret = new ConstraintSet();
ConstraintSet methConstrains;
@@ -392,7 +392,8 @@ public class TYPEStmt implements StatementVisitor {
@Override
public void visit(BoolExpression expr) {
expr.rexpr.accept(this);
expr.lexpr.accept(this);
constraintsSet.addUndConstraint(new Pair(bool, expr.getType(), PairOperator.EQUALSDOT, loc(expr.getOffset())));
constraintsSet.addUndConstraint(new Pair(bool, expr.lexpr.getType(), PairOperator.EQUALSDOT, loc(expr.getOffset())));
constraintsSet.addUndConstraint(new Pair(bool, expr.rexpr.getType(), PairOperator.EQUALSDOT, loc(expr.getOffset())));
@@ -32,12 +32,12 @@ public class Logger {
private static Writer defaultWriter;
private static void initLogger() {
if (defaultWriter != null) return;
if (ConsoleInterface.writeLogFiles) {
try {
Files.createDirectories(logFolder.toPath());
} catch (Exception e) {
throw new RuntimeException("Could not create directory for log files: " + logFolder, e);
}
if (!ConsoleInterface.writeLogFiles) return;
try {
Files.createDirectories(logFolder.toPath());
} catch (Exception e) {
throw new RuntimeException("Could not create directory for log files: " + logFolder, e);
}
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");