Skip to content

[Constraint System] Fix covariant erasure for constrained existentials #64788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

angela-laar
Copy link
Contributor

@angela-laar angela-laar commented Mar 30, 2023

Constrained existentials should be type erased to an upper bound that is dependent on other type parameters.

let t: (any Sequence<Int>) = [1, 2]

for value in t {
    let v = value + 1 // the existential is opened so that `value` is generic parameter Int instead of Any
}

Fixes rdar://101343646

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar force-pushed the constrained-existential-yields-uppercase-Any branch from cdd526e to 8e8f872 Compare March 30, 2023 22:25
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

Use of an associated type in the member is not considered invariant
if that associated type has been made concrete by the existential type.
@angela-laar angela-laar marked this pull request as draft March 31, 2023 17:43
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar force-pushed the constrained-existential-yields-uppercase-Any branch from 8e8f872 to 91bd400 Compare April 1, 2023 01:05
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test macOS

@angela-laar angela-laar force-pushed the constrained-existential-yields-uppercase-Any branch from 91bd400 to bdd6bfd Compare April 1, 2023 05:17
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar marked this pull request as ready for review April 1, 2023 05:23
@@ -2103,7 +2103,7 @@ typeEraseExistentialSelfReferences(
if (t->is<GenericTypeParamType>()) {
erasedTy = baseTy;
} else {
erasedTy = existentialSig->getNonDependentUpperBounds(t);
erasedTy = existentialSig->getDependentUpperBounds(t);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if:

protocol P<A> {
  associatedtype A
}
protocol Q {
  associatedtype B
  associatedtype C: P<B>

  func foo() -> C
}

func test(q: any Q) {
  let _ = q.foo() // Currently erased to 'any P', but would be 'any P<B>'?
}

The erased type should not be dependent on the opened existential. Instead, I think we should keep using getNonDependentUpperBounds, but refactor it to type-erase the result of calling getDependentUpperBounds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain a bit more what you mean by type-erase the result of getDependentUpperBounds?

Copy link
Contributor Author

@angela-laar angela-laar Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, with and without this change, that example will still produce this error: Inferred result type 'any P' requires explicit coercion due to loss of generic requirements. Looks like the example will still be erased to 'any P' so we might be ok here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good example, I can add a test for it 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the example will still be erased to 'any P'

Interesting, not sure if that’s expected or not. Either way we shouldn’t be assuming that getDependentUpperBounds will always produce a non-dependent result; it will break stuff one way or the other:

class C1 {}
class C2<T>: C1 {}

protocol P {
  associatedtype A
  associatedtype B: C2<A>

  func foo() -> B
}

func test(p: any P) {
  let _ = p.foo()
}

Can you explain a bit more what you mean by type-erase the result of getDependentUpperBounds?

What I mean is to reimplement getNonDependentUpperBounds in terms of getDependentUpperBounds.

This is a good example, I can add a test for it 🙂

Nvm, turns out this particular case was covered in #42559 🙃

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing I noticed is that typeEraseExistentialSelfReferences() is basically a copy-and-paste of typeEraseOpenedArchetypesWithRoot() from Type.cpp. Perhaps if we update both copies or merge them together it will fix this problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more principled approach would be to look into why the function is being passed any IteratorProtocol rather than any IteratorProtocol<Int> as the base type.

@AnthonyLatsis If I understand your comment correctly by "the function" you mean typeEraseExistentialSelfReferences and considering next() reference.

The problem is with makeIterator reference, that's why next() doesn't get any IteratorProtocol<Int> for the base. I simplified the example down to:

func increment(n : any Sequence<Float>) {
 var iter = n.makeIterator()
}

(the solver transforms for-in loop into var $gen = <expr>.makeIterator(); while let v = $gen.next() { ... } so $gen is n in this example).

typeEraseExistentialSelfReferences gets any Sequence<Float> as a base type and refTy is () -> τ_0_0.Iterator, generic signature is <τ_0_0 where τ_0_0 : Sequence, τ_0_0.Element == Float>.

Since the transform runs on refTy it would try to call getConcreteType on τ_0_0.Iterator and that does not produce a concrete type so it falls through to erasure case and produces any IteratorProtocol.

I think we should keep using getNonDependentUpperBounds, but refactor it to type-erase the result of calling getDependentUpperBounds.

Let me see if I understand your point here - getNonDependentUpperBounds should be allowed to use use type information provided by primary associated types, but only them, to maintain the contract that visible type retains only type information as available before the reference otherwise we might end up in the situation that leaks type information from opened type to the outside. In practice it means that getNonDependentUpperBounds needs to keep erasing until it reaches bound that does not have any unbound dependent members/generic parameters (based on the existential signature), right?

For your second example this means that getNonDependentUpperBounds would produce C1 (because C2 references undetermined dependent member A) but if P did have a primary associated type A and it was bound in expression i.e. any P<Int> getNonDependentUpperBounds would produce C2<Int> instead, does that make sense?

Copy link
Collaborator

@AnthonyLatsis AnthonyLatsis Apr 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it took so long, somehow I only noticed your reply this evening.

Let me see if I understand your point here - getNonDependentUpperBounds should be allowed to use use type information provided by primary associated types, but only them, to maintain the contract

getNonDependentUpperBounds is allowed to use all the type info provided by the receiver (the generic signature derived from the existential base type). All generic constraints contribute to the result, e.g.

class C<T> {}
protocol P { 
  associatedtype A 
  associatedtype B: C<A>
}
protocol Q: P where A == Int {
  func getB() -> B
}

do {
  let q: any Q
  let _ = q.getB() // OK, 'C<Int>'
}

getNonDependentUpperBounds produces a constraint type that is supposed to be the non-dependent supremum of the dependent upper bounds, which retain all the type info. In practice and by contract, it’s always a non-dependent type, but not always as specific as it could have been. For example, the method wasn’t updated for constrained existentials, so it doesn’t know how to build parameterized protocol types. This means it never produces a type containing a parameterized protocol type; just bare protocols.

In practice it means that getNonDependentUpperBounds needs to keep erasing until it reaches bound that does not have any unbound dependent members/generic parameters (based on the existential signature), right?

For your second example this means that getNonDependentUpperBounds would produce C1 (because C2 references undetermined dependent member A) but if P did have a primary associated type A and it was bound in expression i.e. any P<Int> getNonDependentUpperBounds would produce C2<Int> instead, does that make sense?

Yes.


typeEraseExistentialSelfReferences gets any Sequence<Float> as a base type and refTy is () -> τ_0_0.Iterator, generic signature is <τ_0_0 where τ_0_0 : Sequence, τ_0_0.Element == Float>.

Since the transform runs on refTy it would try to call getConcreteType on τ_0_0.Iterator and that does not produce a concrete type so it falls through to erasure case and produces any IteratorProtocol.

I see, that makes a lot of sense. So after all this suggests we have to teach getNonDependentUpperBounds to build parameterized protocol types, and probably do so on demand via a flag because — I presume — upgrading upper bounds for more than just the built-in for-in transform would compromise the requires explicit coercion diagnostic.

The robust approach is to base getNonDependentUpperBounds on getDependentUpperBounds (bonus: the latter already builds parameterized protocol types).

Copy link
Contributor Author

@angela-laar angela-laar Apr 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok! will do, converting to draft while I make those changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider the fact that your example compiles today to be a bug.

I will track fixing this diagnostic issue in a second PR since after a couple of attempts, it seems significant changes to the Coercion Checker are needed

Opened a new PR for the original issue: #65785

Copy link
Contributor

@xedin xedin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add more tests before merging as we've discussed.

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test macOS

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

2 similar comments
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar force-pushed the constrained-existential-yields-uppercase-Any branch from c3ba584 to 77014e5 Compare April 13, 2023 00:24
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar force-pushed the constrained-existential-yields-uppercase-Any branch from 6bc59a6 to 9f809fa Compare April 13, 2023 01:28
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar marked this pull request as draft April 20, 2023 22:55
And update expectederror for member access test
When opening an existential return or parameter type that has a
primaray associated type, the primmary asssociated type should be
preserved if the type can be reduced to a concrete type. Otherwise,
we should erase the existential to a type with no dependent members
or generic parameters and diagnose explicit coercion is needed.
@angela-laar angela-laar force-pushed the constrained-existential-yields-uppercase-Any branch from 455ba6e to 5799b0a Compare May 4, 2023 00:25
@angela-laar angela-laar marked this pull request as ready for review May 4, 2023 00:25
@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar
Copy link
Contributor Author

@swift-ci please smoke test

@angela-laar angela-laar marked this pull request as draft May 4, 2023 23:12
@angela-laar
Copy link
Contributor Author

I consider the fact that your example compiles today to be a bug.

I will track fixing this diagnostic issue in a second PR since after a couple of attempts, it seems significant changes to the Coercion Checker are needed

Opened a new PR for the original issue: #65785

@angela-laar angela-laar closed this May 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants