Skip to content

Conversation

@susmonteiro
Copy link
Contributor

The ClangTypeEscapability and CxxValueSemantics requests are similar and both relied on recursion, which could cause cycles when the type being analyzed depended on itself. This patch refactors these two requests to remember which types have been seen, preventing these cycles from happening.

@susmonteiro susmonteiro added the c++ interop Feature: Interoperability with C++ label Nov 13, 2025
@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test

@susmonteiro susmonteiro force-pushed the susmonteiro/remove-request-cycles branch from a56eeb1 to ae20b9f Compare November 17, 2025 11:38
@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test

@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test windows

Copy link
Contributor

@j-hui j-hui left a comment

Choose a reason for hiding this comment

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

Thanks for this patch Susana! I do have quite a few questions and suggestions for improvement, but I'm happy for you to merge patch mostly as is—this is a big refactor that happened to touch a lot of logic I hadn't closely reviewed before, which is probably orthogonal to what you're trying to do here. Feel free to address my feedback incrementally in multiple follow-up patches, at your discretion.

Aside from what I've already commented on, my main ask is that you check in at least one test case that involves circularity, which would have triggered a compiler error prior to your refactor. I'd like to make sure we have those cases accounted for in our test suite, so that we don't accidentally regress.

Another orthogonal issue here is of diagnostics and how it interacts with cacheing. I'm a little worried that by doing the diagnostics in the request, and mixing and matching it with the short-circuiting, we might encounter weird cases like this, where we should emit a diagnostic, but don't:

Outer<MoveOnlyType, MalformedAnnotations>

where Outer<T, U> is COPYABLE_IF both its parameters are. If we request the copyability of this type, we will consider it move-only due to the MoveOnlyType template parameter without ever encountering the MalformedAnnotations type in our traversal.

If my instincts are right about how the cacheing will work out here, I think we'll also get duplicate diagnostics for MalformedAnnotations for a pair of types like this:

Outer<MalformedAnnotations, int>;
Outer<MalformedAnnotations, float>;

since these two types are cached separately.

I think the right way to deal with this is to do a separate pass for diagnostics, which never short-circuits, though that might lead to more code duplication.

Then again, I believe these may all be short-comings of the existing implementation, and in any case COPYABLE_IF is an opt-in annotation that probably doesn't have much adoption yet, so maybe we're ok on that front.

Comment on lines +5531 to +5557
auto cxxRecordDecl = dyn_cast<clang::CXXRecordDecl>(recordDecl);
if (recordDecl->getDefinition() &&
(!cxxRecordDecl || cxxRecordDecl->isAggregate())) {
if (cxxRecordDecl) {
for (auto base : cxxRecordDecl->bases())
maybePushToStack(base.getType()->getUnqualifiedDesugaredType());
}
for (auto field : recordDecl->fields())
maybePushToStack(field->getType()->getUnqualifiedDesugaredType());
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit confused by why !cxxRecordDecl || cxxRecordDecl->isAggregate() here. Can you please leave a comment to explain why we're not checking non-aggregate C++ records?

nit: it might make things a little clearer to refactor it like this + rebind recordDecl to its definition if distinct:

Suggested change
auto cxxRecordDecl = dyn_cast<clang::CXXRecordDecl>(recordDecl);
if (recordDecl->getDefinition() &&
(!cxxRecordDecl || cxxRecordDecl->isAggregate())) {
if (cxxRecordDecl) {
for (auto base : cxxRecordDecl->bases())
maybePushToStack(base.getType()->getUnqualifiedDesugaredType());
}
for (auto field : recordDecl->fields())
maybePushToStack(field->getType()->getUnqualifiedDesugaredType());
if ((recordDecl = recordDecl->getDefinition())) {
auto checkFields = true;
if (auto cxxRecDecl = dyn_cast<clang::CXXRecordDecl>(recordDecl)) {
checkFields = cxxRecDec->isAggregate();
for (auto base : cxxRecDecl->Bases())
// etc.
}
if (checkFields) {
for (auto field : recordDecl->fields())
// etc.
}

Copy link
Contributor Author

@susmonteiro susmonteiro Nov 19, 2025

Choose a reason for hiding this comment

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

For escapability, we do inference and checking separately. The inference is done in this request, while the checking is part of VisitRecordDecl. We only attempt to infer escapability in simple cases (trying to do so in more complex cases caused a few breakages), and that's why this inference is behind the aggregate check.

Having said that, since annotationOnly is always set to true, we never actually get to infer escapability at the moment. This needs some refactoring, which will be done in a follow-up patch.

}
for (auto field : recordDecl->fields())
maybePushToStack(field->getType()->getUnqualifiedDesugaredType());
continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

How are we handling cases of RecordDecls for which we lack a definition, or for non-aggregate CxxRecordDecls?

Shouldn't those raise hasUnknown?

In any case, I suggest handling that case explicitly (or at least leave a comment if there's nothing to do), rather than just let it "fall off the end" like would happen with the current version.

Copy link
Contributor Author

@susmonteiro susmonteiro Nov 19, 2025

Choose a reason for hiding this comment

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

That's a great point. Once again, because annotationOnly is always true, we actually always set hasUnknown if the record doesn't have an escapability annotation. But I agree we should make it explicit! I'll add a comment for now, but will take this into account in the follow-up patch to get rid of annotationOnly

const auto *type = desc.type;
// A C++ type can be imported to Swift as Copyable or ~Copyable.
// We assume a type can be imported as Copyable unless:
// - There's no copy constructor
Copy link
Contributor

Choose a reason for hiding this comment

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

How are we handling cases for (1) CxxRecordDecl vs plain C RecordDecls; (2) and any kind of RecordDecl that lacks a definition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We currently assume that a RecordDecl that is lacking definition or a RecordDecl that is not a CxxRecordDecl are copyable (unless explicit annotated with SWIFT_NONCOPYABLE). I'll add a comment to the code as well :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On a second thought, I don't think this is something that needs a comment. The comment you highlighted correctly describes these situations (Copyable unless there's a SWIFT_NONCOPYABLE annotation) and the part of the code where we deal with these is quite explicit, imo:

if (!cxxRecordDecl || !recordDecl->isCompleteDefinition()) {
  if (hasNonCopyableAttr(recordDecl))
    return CxxValueSemanticsKind::MoveOnly;
  continue;
}

What do you think?

@susmonteiro
Copy link
Contributor Author

Thanks for the thorough review @j-hui

Aside from what I've already commented on, my main ask is that you check in at least one test case that involves circularity, which would have triggered a compiler error prior to your refactor. I'd like to make sure we have those cases accounted for in our test suite, so that we don't accidentally regress.

I agree that it's important to add these tests. The thing is the patch that created the cycle was reverted and has not been merged yet - I actually have it on draft here #85559 as I'm waiting for this one to be merged. I don't see a case where the current patch creates a circular reference, but the future one already contains some tests that would trigger a cycle before.

Another orthogonal issue here is of diagnostics and how it interacts with cacheing. I'm a little worried that by doing the diagnostics in the request, and mixing and matching it with the short-circuiting, we might encounter weird cases like this, where we should emit a diagnostic, but don't:

Outer<MoveOnlyType, MalformedAnnotations>
where Outer<T, U> is COPYABLE_IF both its parameters are. If we request the copyability of this type, we will consider it move-only due to the MoveOnlyType template parameter without ever encountering the MalformedAnnotations type in our traversal.

I also agree with your point regarding diagnostics. However, since this would involve some changes and some new diagnostics, I opted to add them on a different patch, which I'm already working on. The diagnostics are important, but if you're fine with that, we'll add them later on.

I'll explore your suggestion to do a separate pass for diagnostics. Perhaps we could check them during import, instead of during the copyability request, and these way we would avoid cycles. Anyways, since I think this should be added in a future PR, we can discuss this offline.

Copy link
Contributor

@Xazax-hun Xazax-hun left a comment

Choose a reason for hiding this comment

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

Overall looks good, some nits inline.


bool hasUnknown = false;
auto desugared = desc.type->getUnqualifiedDesugaredType();
if (const auto *recordType = desugared->getAs<clang::RecordType>()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually need this code path? Shouldn't this be handled inside the loop already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inside of the loop, we don't return from the request when we encounter an escapable type, because there might still be a nonescapable type, which takes precedence. However, if the root type has a SWIFT_ESCAPABLE annotation, then it should be imported as escapable, without further analysis needed.

return std::nullopt;
};
std::function maybePushToStack = [&](const clang::Type *type) {
const auto *recordType = type->getAs<clang::RecordType>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: if we are only interested in the decl we can skip this intermediate step and use the getAsRecordDecl API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately using the getAsRecordDecl API triggers an assertion failure while casting the type in Linux. I'll investigate this further, but for now I'll leave this as is

@susmonteiro susmonteiro force-pushed the susmonteiro/remove-request-cycles branch from ae20b9f to f4337c4 Compare November 20, 2025 12:36
@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test

@susmonteiro susmonteiro force-pushed the susmonteiro/remove-request-cycles branch from f4337c4 to 1962da2 Compare November 20, 2025 15:35
@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test

@susmonteiro susmonteiro force-pushed the susmonteiro/remove-request-cycles branch from 1962da2 to 791194b Compare November 20, 2025 18:43
@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test

@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test

@susmonteiro susmonteiro enabled auto-merge December 1, 2025 09:53
@susmonteiro
Copy link
Contributor Author

@swift-ci please smoke test windows

@susmonteiro susmonteiro merged commit 92cbcb6 into swiftlang:main Dec 1, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ interop Feature: Interoperability with C++

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants