diff --git a/base/src/main/java/org/aya/resolve/error/CyclicDependencyError.java b/base/src/main/java/org/aya/resolve/error/CyclicDependencyError.java index 386b52d2a2..7a0800f24b 100644 --- a/base/src/main/java/org/aya/resolve/error/CyclicDependencyError.java +++ b/base/src/main/java/org/aya/resolve/error/CyclicDependencyError.java @@ -8,17 +8,21 @@ import org.aya.syntax.ref.GeneralizedVar; import org.aya.util.error.SourcePos; import org.jetbrains.annotations.NotNull; +import kala.collection.immutable.ImmutableSeq; public record CyclicDependencyError( @NotNull SourcePos sourcePos, - @NotNull GeneralizedVar var + @NotNull GeneralizedVar var, + @NotNull ImmutableSeq cyclePath ) implements Problem { @Override public @NotNull Severity level() { return Severity.ERROR; } @Override public @NotNull Stage stage() { return Stage.RESOLVE; } @Override public @NotNull Doc describe(@NotNull PrettierOptions options) { - return Doc.sep( - Doc.plain("Cyclic dependency detected in variable declaration:"), - Doc.plain(var.name()) + return Doc.vcat( + Doc.plain("Cyclic dependency detected in variable declarations:"), + Doc.plain(String.join(" -> ", cyclePath.view() + .map(GeneralizedVar::name) + .toImmutableSeq())) ); } } diff --git a/base/src/main/java/org/aya/resolve/visitor/VariableDependencyCollector.java b/base/src/main/java/org/aya/resolve/visitor/VariableDependencyCollector.java index 6612372ba1..7c3ff7e35b 100644 --- a/base/src/main/java/org/aya/resolve/visitor/VariableDependencyCollector.java +++ b/base/src/main/java/org/aya/resolve/visitor/VariableDependencyCollector.java @@ -19,13 +19,13 @@ /** * Collects dependency information for generalized variables using DFS on their types. - * + * * 1. A variable's type may reference other generalized variables; we record those as dependencies. * 2. If we revisit a variable already on the DFS stack ("visiting" set), that indicates * a cyclic dependency, and we report an error. * 3. Once a variable is fully processed, it goes into the "visited" set; future registrations * of the same variable skip repeated traversal. - * + * * Pitfalls & Notes: * - A single variable (e.g. “A”) should be registered once, to avoid duplication. * - Attempting to re-scan or re-introduce “A” in another variable’s context can cause @@ -34,10 +34,10 @@ * if it’s not in the allowedGeneralizes map. */ public final class VariableDependencyCollector { - private final Map> dependencies = new HashMap<>(); private final Reporter reporter; private final MutableSet visiting = MutableSet.create(); private final MutableSet visited = MutableSet.create(); + private final MutableList currentPath = MutableList.create(); public VariableDependencyCollector(Reporter reporter) { this.reporter = reporter; @@ -48,24 +48,29 @@ public void registerVariable(GeneralizedVar var) { // If var is already being visited in current DFS path, we found a cycle if (!visiting.add(var)) { - reporter.report(new CyclicDependencyError(var.sourcePos(), var)); + // Find cycle start index + var cycleStart = currentPath.indexOf(var); + var cyclePath = currentPath.view().drop(cycleStart).appended(var); + reporter.report(new CyclicDependencyError(var.sourcePos(), var, cyclePath.toImmutableSeq())); throw new Context.ResolvingInterruptedException(); } + currentPath.append(var); var deps = collectReferences(var); - dependencies.put(var, deps); + var.setDependencies(deps); // Recursively register dependencies for (var dep : deps) { registerVariable(dep); } + currentPath.removeLast(); visiting.remove(var); visited.add(var); } public ImmutableSeq getDependencies(GeneralizedVar var) { - return dependencies.getOrDefault(var, ImmutableSeq.empty()); + return var.getDependencies(); } private ImmutableSeq collectReferences(GeneralizedVar var) { diff --git a/cli-impl/src/test/java/org/aya/test/fixtures/ScopeError.java b/cli-impl/src/test/java/org/aya/test/fixtures/ScopeError.java index 5a52e9b0db..d53b9a0694 100644 --- a/cli-impl/src/test/java/org/aya/test/fixtures/ScopeError.java +++ b/cli-impl/src/test/java/org/aya/test/fixtures/ScopeError.java @@ -103,4 +103,8 @@ def test (A : Type) (a : A) : A => | x : A := a in x """; + @Language("Aya") String testCyclicDependency = """ + variable A : B + variable B : A + """; } diff --git a/cli-impl/src/test/resources/negative/ScopeError.txt b/cli-impl/src/test/resources/negative/ScopeError.txt index 58bec7ba96..d23d5228a1 100644 --- a/cli-impl/src/test/resources/negative/ScopeError.txt +++ b/cli-impl/src/test/resources/negative/ScopeError.txt @@ -318,3 +318,6 @@ That looks right! LocalShadowSuppress: That looks right! +CyclicDependency: +That looks right! + diff --git a/syntax/src/main/java/org/aya/syntax/ref/GeneralizedVar.java b/syntax/src/main/java/org/aya/syntax/ref/GeneralizedVar.java index 9492a22b9d..10dbd0fd0f 100644 --- a/syntax/src/main/java/org/aya/syntax/ref/GeneralizedVar.java +++ b/syntax/src/main/java/org/aya/syntax/ref/GeneralizedVar.java @@ -6,17 +6,27 @@ import org.aya.util.error.SourceNode; import org.aya.util.error.SourcePos; import org.jetbrains.annotations.NotNull; +import kala.collection.immutable.ImmutableSeq; public final class GeneralizedVar implements AnyVar, SourceNode { public final @NotNull String name; public final @NotNull SourcePos sourcePos; public Generalize owner; + private @NotNull ImmutableSeq dependencies = ImmutableSeq.empty(); public GeneralizedVar(@NotNull String name, @NotNull SourcePos sourcePos) { this.name = name; this.sourcePos = sourcePos; } + public void setDependencies(@NotNull ImmutableSeq deps) { + this.dependencies = deps; + } + + public @NotNull ImmutableSeq getDependencies() { + return dependencies; + } + public @NotNull LocalVar toLocal() { return new LocalVar(name, sourcePos, new GenerateKind.Generalized(this)); }