A struct that references itself through opaque-typed computed properties can be built in Swift prior to 6.3. For example:
struct RecursiveView: View {
var depth: Int
@ViewBuilder
var contentView: some View {
if depth > 0 {
childContent // references childContent's opaque type
} else {
Text("leaf")
}
}
var childContent: some View {
ForEachView(content: RecursiveView(depth: depth - 1)) // self-referential
}
var body: some View {
ModifiedContent(content: contentView, modifier: 0)
}
}
However, since 6.3 a self-recursive View patterns with some View-returning types cause the compiler to crash with ABORT() in substOpaqueTypesWithUnderlyingTypes:
Problem
The substitution limit mechanism introduced in 103428fe804 means that when the limit is exhausted, the compiler will ABORT() at SubstitutionMap.cpp / TypeSubstitution.cpp.
Although I’ve seen multiple changes to adjust the limit (see commit hashes below), no finite limit value can fix this. The commit message for 103428fe804 acknowledges that "in general termination is undecidable."
This prevents valid self-recursive view patterns (common in SwiftUI) to crash the compiler rather than producing a diagnostic or gracefully degrading.
Solution
I wonder if we can soften the crash. The compiler should either:
Detect the cycle and return the partially-substituted type (use the old SeenDecl mechanism for trivial cases like self-recursion).
Emit a diagnostic at the source location instead of calling ABORT() (as the existing FIXME in the code suggests, I wonder if any effort is spent on this.
Do nothing at compiler level and explicitly state (in the documentation) that self-recursive opaque return types are unsupported and require an AnyView wrapper. This is already the workaround we are using. But without a proper diagnostic, developers can only discover it only through a compiler crash.
Yeah, the “crash” is intentional and I do hope to catch more of these cases with a diagnostic eventually. This assert is there to prevent infinite recursion, and the intent is that “simple” cycles should be caught earlier with a diagnostic. In the optimizer, we don’t have a source location to emit a diagnostic, but perhaps as a backstop we can use the source location of the function currently being optimized.
That’s my intention, if we can somehow inform the developers what/how they should change their code instead of crashing that’s the best. A crashing compiler brings (grunting) developers to our doorstep and that’s not necessary.
Any guidance in terms of how can I help? Would love to address this.
@Slava_Pestov I have a proposal, let me know what you think:
Instead of checkCircularOpaqueReturnTypeDecl:
can we replace it with CheckOpaqueTypeCyclesThroughNominals where it looks over all types in one SourceFile, and construct a dependency graph by scanning each opaque decl's underlying type for two things:
Other opaque archetypes (existing ones)
Nominal types that have members with opaque return types (the key addition)
With the dependency graph, it would be easy to do a DFS style check to find the circular dependency and error/warning out early.
The existing check can be made smarter in various ways to detect some infinite substitution instances ahead of time, but it's not quite as simple performing a DFS over underlying types or anything like that; because of how opaque archetypes can nest and reference each other, the problem of "fully expanding" an opaque archetype to its underlying types is actually undecidable. One way to show this is to take my example from Termination checking for type substitution and adapt it to use opaque return types instead of associated types. This means that any "ahead of time" analysis to detect cycles here has to be conservative.
The simplest "analysis" that works here is to run the operation up to some number of steps, and then report an error if this fails. This has already been mostly wired up for opaque archetypes, the only missing part is reporting an error with a reasonable source location instead of calling the assertion handler, as you saw.