Skip to content
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

TypeCheckType: Fix some bugs in the any syntax checker #72659

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions include/swift/AST/TypeRepr.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ class alignas(1 << TypeReprAlignInBits) TypeRepr
/// opaque return type reprs.
bool hasOpaque();

/// Returns a Boolean value indicating whether this written type is
/// parenthesized, that is, matches the following grammar: `'(' type ')'`.
bool isParenType() const;

/// Retrieve the type repr without any parentheses around it.
///
/// The use of this function must be restricted to contexts where
Expand Down
16 changes: 10 additions & 6 deletions lib/AST/TypeRepr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,18 @@ bool TypeRepr::hasOpaque() {
findIf([](TypeRepr *ty) { return isa<OpaqueReturnTypeRepr>(ty); });
}

bool TypeRepr::isParenType() const {
auto *tuple = dyn_cast<TupleTypeRepr>(this);
return tuple && tuple->isParenType();
}

TypeRepr *TypeRepr::getWithoutParens() const {
auto *repr = const_cast<TypeRepr *>(this);
while (auto *tupleRepr = dyn_cast<TupleTypeRepr>(repr)) {
if (!tupleRepr->isParenType())
break;
repr = tupleRepr->getElementType(0);
auto *result = this;
while (result->isParenType()) {
result = cast<TupleTypeRepr>(result)->getElementType(0);
}
return repr;

return const_cast<TypeRepr *>(result);
}

bool TypeRepr::isSimpleUnqualifiedIdentifier(Identifier identifier) const {
Expand Down
142 changes: 96 additions & 46 deletions lib/Sema/TypeCheckType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5938,31 +5938,6 @@ class ExistentialTypeSyntaxChecker : public ASTWalker {
if (T->isInvalid())
return Action::SkipNode();

// Arbitrary protocol constraints are OK on opaque types.
if (isa<OpaqueReturnTypeRepr>(T))
return Action::SkipNode();

// Arbitrary protocol constraints are okay for 'any' types.
if (isa<ExistentialTypeRepr>(T))
return Action::SkipNode();

// Suppressed conformance needs to be within any/some.
if (auto inverse = dyn_cast<InverseTypeRepr>(T)) {
// Find an enclosing protocol composition, if there is one, so we
// can insert 'any' before that.
SourceLoc anyLoc = inverse->getTildeLoc();
if (!reprStack.empty()) {
if (isa<CompositionTypeRepr>(reprStack.back())) {
anyLoc = reprStack.back()->getStartLoc();
}
}

Ctx.Diags.diagnose(inverse->getTildeLoc(), diag::inverse_requires_any)
.highlight(inverse->getConstraint()->getSourceRange())
.fixItInsert(anyLoc, "any ");
return Action::SkipNode();
}

reprStack.push_back(T);

auto *declRefTR = dyn_cast<DeclRefTypeRepr>(T);
Expand Down Expand Up @@ -6015,9 +5990,12 @@ class ExistentialTypeSyntaxChecker : public ASTWalker {
}

private:
bool existentialNeedsParens(TypeRepr *parent) const {
/// Returns a Boolean value indicating whether the insertion of `any` before
/// a type representation with the given parent requires paretheses.
static bool anySyntaxNeedsParens(TypeRepr *parent) {
switch (parent->getKind()) {
case TypeReprKind::Optional:
case TypeReprKind::ImplicitlyUnwrappedOptional:
case TypeReprKind::Protocol:
return true;
case TypeReprKind::Metatype:
Expand All @@ -6032,7 +6010,6 @@ class ExistentialTypeSyntaxChecker : public ASTWalker {
case TypeReprKind::UnqualifiedIdent:
case TypeReprKind::QualifiedIdent:
case TypeReprKind::Dictionary:
case TypeReprKind::ImplicitlyUnwrappedOptional:
case TypeReprKind::Inverse:
case TypeReprKind::Tuple:
case TypeReprKind::Fixed:
Expand All @@ -6054,40 +6031,94 @@ class ExistentialTypeSyntaxChecker : public ASTWalker {
}

void emitInsertAnyFixit(InFlightDiagnostic &diag, DeclRefTypeRepr *T) const {
TypeRepr *replaceRepr = T;
TypeRepr *replacementT = T;

// Insert parens in expression context for '(any P).self'
bool needsParens = (exprCount != 0);

// Compute the replacement node (the node to which to apply `any`).
if (reprStack.size() > 1) {
auto parentIt = reprStack.end() - 2;
needsParens = existentialNeedsParens(*parentIt);

// Expand to include parenthesis before checking if the parent needs
// to be replaced.
while (parentIt != reprStack.begin() &&
(*parentIt)->getWithoutParens() != *parentIt)
parentIt -= 1;

// For existential metatypes, 'any' is applied to the metatype.
if ((*parentIt)->getKind() == TypeReprKind::Metatype) {
replaceRepr = *parentIt;
if (parentIt != reprStack.begin())
needsParens = existentialNeedsParens(*(parentIt - 1));
auto it = reprStack.end() - 1;
auto replacementIt = it;

// Backtrack the stack and expand the replacement range to any parent
// inverses, compositions or `.Type` metatypes, skipping only parentheses.
//
// E.g. `(X & ~P).Type` → `any (X & ~P).Type`.
// ↑
// We're here
do {
--it;
if ((*it)->isParenType()) {
continue;
}

if (isa<InverseTypeRepr>(*it) || isa<CompositionTypeRepr>(*it) ||
isa<MetatypeTypeRepr>(*it)) {
replacementIt = it;
continue;
}

break;
} while (it != reprStack.begin());

// Whether parentheses are necessary is determined by the immediate parent
// of the replacement node.
if (replacementIt != reprStack.begin()) {
needsParens = anySyntaxNeedsParens(*(replacementIt - 1));
}

replacementT = *replacementIt;
}

llvm::SmallString<64> fix;
{
llvm::raw_svector_ostream OS(fix);
if (needsParens)
OS << "(";
ExistentialTypeRepr existential(SourceLoc(), replaceRepr);
ExistentialTypeRepr existential(SourceLoc(), replacementT);
existential.print(OS);
if (needsParens)
OS << ")";
}

diag.fixItReplace(replaceRepr->getSourceRange(), fix);
diag.fixItReplace(replacementT->getSourceRange(), fix);
}

/// Returns a Boolean value indicating whether the type representation being
/// visited, assuming it is a constraint type demanding `any` or `some`, is
/// missing either keyword.
bool isAnyOrSomeMissing() const {
assert(isa<DeclRefTypeRepr>(reprStack.back()));

if (reprStack.size() < 2) {
return true;
}

auto it = reprStack.end() - 1;
while (true) {
--it;
if (it == reprStack.begin()) {
break;
}

// Look through parens, inverses, metatypes, and compositions.
if ((*it)->isParenType() || isa<InverseTypeRepr>(*it) ||
isa<CompositionTypeRepr>(*it) || isa<MetatypeTypeRepr>(*it)) {
continue;
}

// Look through '?' and '!' too; `any P?` et al. is diagnosed in the
// type resolver.
if (isa<OptionalTypeRepr>(*it) ||
isa<ImplicitlyUnwrappedOptionalTypeRepr>(*it)) {
continue;
}

break;
}

return !(isa<OpaqueReturnTypeRepr>(*it) || isa<ExistentialTypeRepr>(*it));
}

void checkDeclRefTypeRepr(DeclRefTypeRepr *T) const {
Expand All @@ -6097,13 +6128,32 @@ class ExistentialTypeSyntaxChecker : public ASTWalker {
return;
}

// Backtrack the stack, looking just through parentheses and metatypes. If
// we find an inverse (which always requires `any`), diagnose it specially.
Copy link
Member

Choose a reason for hiding this comment

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

You might be able to just pass down the outermost location instead of "backtracking" here, but it's fine the way it is

if (reprStack.size() > 1) {
auto it = reprStack.end() - 2;
while (it != reprStack.begin() &&
((*it)->isParenType() || isa<MetatypeTypeRepr>(*it))) {
--it;
continue;
}

if (auto *inverse = dyn_cast<InverseTypeRepr>(*it);
inverse && isAnyOrSomeMissing()) {
auto diag = Ctx.Diags.diagnose(inverse->getTildeLoc(),
diag::inverse_requires_any);
emitInsertAnyFixit(diag, T);
return;
}
}

auto *decl = T->getBoundDecl();
if (!decl) {
return;
}

if (auto *proto = dyn_cast<ProtocolDecl>(decl)) {
if (proto->existentialRequiresAny()) {
if (proto->existentialRequiresAny() && isAnyOrSomeMissing()) {
auto diag =
Ctx.Diags.diagnose(T->getNameLoc(), diag::existential_requires_any,
proto->getDeclaredInterfaceType(),
Expand Down Expand Up @@ -6133,7 +6183,7 @@ class ExistentialTypeSyntaxChecker : public ASTWalker {
if (auto *PCT = type->getAs<ProtocolCompositionType>())
diagnose |= !PCT->getInverses().empty();

if (diagnose) {
if (diagnose && isAnyOrSomeMissing()) {
auto diag = Ctx.Diags.diagnose(
T->getNameLoc(), diag::existential_requires_any,
alias->getDeclaredInterfaceType(),
Expand Down
7 changes: 0 additions & 7 deletions test/Parse/inverses.swift
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have incorporated the tests I deleted here into test/type/explicit_existential.swift and test/type/explicit_existential_swift6.swift

Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ func what(one: ~Copyable..., // expected-error {{noncopyable type '~Copyable' ca

struct A { struct B { struct C {} } }

typealias Z0 = (~Copyable).Type // expected-error{{constraint that suppresses conformance requires 'any'}}{{17-17=any }}
typealias Z1 = ~Copyable.Type // expected-error{{constraint that suppresses conformance requires 'any'}}{{16-16=any }}
typealias Z2 = ~A.B.C // expected-error {{type 'A.B.C' cannot be suppressed}}
typealias Z3 = ~A? // expected-error {{type 'A?' cannot be suppressed}}
typealias Z4 = ~Rope<Int> // expected-error {{type 'Rope<Int>' cannot be suppressed}}
Expand All @@ -120,13 +118,8 @@ func typeInExpression() {
_ = X<(borrowing any ~Copyable) -> Void>()

_ = ~Copyable.self // expected-error{{unary operator '~' cannot be applied to an operand of type '(any Copyable).Type'}}
_ = (~Copyable).self // expected-error{{constraint that suppresses conformance requires 'any'}}{{8-8=any }}
_ = (any ~Copyable).self
}

func param1(_ t: borrowing ~Copyable) {} // expected-error{{constraint that suppresses conformance requires 'any'}}{{28-28=any }}
func param2(_ t: ~Copyable.Type) {} // expected-error{{constraint that suppresses conformance requires 'any'}}{{18-18=any }}
func param3(_ t: borrowing any ~Copyable) {}
func param4(_ t: any ~Copyable.Type) {}

func param3(_ t: borrowing ExtraNoncopyProto & ~Copyable) {} // expected-error{{constraint that suppresses conformance requires 'any'}}{{28-28=any }}
6 changes: 3 additions & 3 deletions test/decl/protocol/protocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ struct DoesNotConform : Up {
// Circular protocols

protocol CircleMiddle : CircleStart { func circle_middle() }
// expected-note@-1 2 {{protocol 'CircleMiddle' declared here}}
protocol CircleStart : CircleEnd { func circle_start() } // expected-error 2 {{protocol 'CircleStart' refines itself}}
protocol CircleEnd : CircleMiddle { func circle_end()} // expected-note 2 {{protocol 'CircleEnd' declared here}}
// expected-note@-1 3 {{protocol 'CircleMiddle' declared here}}
protocol CircleStart : CircleEnd { func circle_start() } // expected-error 3 {{protocol 'CircleStart' refines itself}}
protocol CircleEnd : CircleMiddle { func circle_end()} // expected-note 3 {{protocol 'CircleEnd' declared here}}
Comment on lines -106 to +108
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is unfortunate although definitely not a ExistentialTypeSyntaxChecker problem.


protocol CircleEntry : CircleTrivial { }
protocol CircleTrivial : CircleTrivial { } // expected-error {{protocol 'CircleTrivial' refines itself}}
Expand Down