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

[Sema]Skip Sendable conformance check when sending are added to parameters or return types of an actor-isolated function #78601

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

stzn
Copy link
Contributor

@stzn stzn commented Jan 13, 2025

Resolves #76710

Previously, we encountered errors and notes when using a non-Sendable type with sending for parameters or return types of an actor-isolated function, as shown below:

class NonSendable { var value = 0 } // note: class 'NonSendable' does not conform to the 'Sendable' protocol

protocol P {
    func bar(a: sending NonSendable) async -> sending NonSendable
}

@MainActor
class PImpl: P {
    // error: non-sendable type 'NonSendable' cannot be returned from main actor-isolated implementation to caller of protocol requirement 'bar(a:)'
    // error: non-sendable parameter type 'NonSendable' cannot be sent from caller of protocol requirement 'bar(a:)' into main actor-isolated implementation
    func bar(a: sending NonSendable) async -> sending NonSendable { a }
}

Based on the issue described, this functionality should work without any errors. To resolve the issue, we need to bypass the Sendable conformance check for non-Sendable types when sending keywords is added to function's parameters or return types.

--

Even if the conformance check is skipped, invalid scenarios will still be validated at the SIL stage to ensure correctness.

For example:

When running -emit-sil -swift-version 6, a potential data race error is still detected.

func test() async {
    var nonSendable = NonSendable()
    let p = PImpl()

    // error: sending 'nonSendable' risks causing data races
    // note: 'nonSendable' used after being passed as a 'sending' parameter; Later uses could race 
    _ = await p.bar(a: nonSendable)

    // note: access can happen concurrently 
    nonSendable.value = 1 
}

@stzn stzn changed the title [Sema]Skip Sendable conformance check when the sending keyword is added to a function parameter or return type [Sema]Skip Sendable conformance check when the sending keyword is added to an actor-isolated function parameter or return type Jan 13, 2025
@stzn stzn force-pushed the fix-sending-typecheck branch from 4c3c8fe to f96c267 Compare January 13, 2025 07:30
@stzn stzn changed the title [Sema]Skip Sendable conformance check when the sending keyword is added to an actor-isolated function parameter or return type [Sema]Skip Sendable conformance check when sending keywords are added to an actor-isolated function parameter or return type Jan 13, 2025
@stzn stzn changed the title [Sema]Skip Sendable conformance check when sending keywords are added to an actor-isolated function parameter or return type [Sema]Skip Sendable conformance check when sending keywords are added to parameters or return types of an actor-isolated function Jan 13, 2025
@stzn stzn changed the title [Sema]Skip Sendable conformance check when sending keywords are added to parameters or return types of an actor-isolated function [Sema]Skip Sendable conformance check when sending are added to parameters or return types of an actor-isolated function Jan 13, 2025
@stzn stzn force-pushed the fix-sending-typecheck branch from f96c267 to 5a6d928 Compare January 13, 2025 07:40
@stzn stzn marked this pull request as ready for review January 13, 2025 09:23
class NonSendableKlass1 {}

protocol P1 {
func bar(_ a: sending NonSendableKlass1) async -> sending NonSendableKlass1

Choose a reason for hiding this comment

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

nit

Suggested change
func bar(_ a: sending NonSendableKlass1) async -> sending NonSendableKlass1
func bar(_ a: sending NonSendableKlass1) async -> sending NonSendableKlass1

@@ -1166,6 +1166,9 @@ bool swift::diagnoseNonSendableTypesInReference(
if (funcCheckOptions.contains(FunctionCheckKind::Params)) {
// only check params if funcCheckKind specifies so
for (auto param : *function->getParameters()) {
if (param->isSending())
return true;
Copy link

@azarovalex azarovalex Jan 13, 2025

Choose a reason for hiding this comment

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

Here you are skipping sendability validation on all other parameters if at least one of them is marked as sending, which seems incorrect, we should check sendability of all arguments.
Also this function is supposed to return true if there are any errors being produced, which is not what happens in our case.
It seems like instead of bailing out early from this function we should keep checking other arguments but skip diagnoseNonSendableTypes check below if the argument is marked as sending.

Could you also consider adding a test for a case where first argument is marked with sending but a second argument isn't?

Copy link
Contributor Author

@stzn stzn Jan 13, 2025

Choose a reason for hiding this comment

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

Thank you for the review.

we should check sendability of all arguments.

I think that's right. Let me check it again.

Also, I overlooked type->hasError() in diagnoseSingleNonSendableType. I think that we need to check type->hasError() as well (not sure point).

static bool diagnoseSingleNonSendableType(
    Type type, SendableCheckContext fromContext,
    Type inDerivedConformance, SourceLoc loc,
    llvm::function_ref<bool(Type, DiagnosticBehavior)> diagnose) {
  if (type->hasError())
    return false;

Could you also consider adding a test for a case where first argument is marked with sending but a second argument isn't?

Sure, I'll add it.

Thank you!

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 fixed it in f633505. Please check it when you have time. Thank you!

@stzn stzn requested a review from ktoso as a code owner January 15, 2025 19:37
@omochi
Copy link
Contributor

omochi commented Jan 16, 2025

@swift-ci please smoke test

@@ -1181,6 +1185,10 @@ bool swift::diagnoseNonSendableTypesInReference(
if (funcCheckOptions.contains(FunctionCheckKind::Results)) {
// only check results if funcCheckKind specifies so
Type resultType = func->getResultInterfaceType().subst(subs);

if (func->hasSendingResult() && !resultType->hasError())
Copy link

@azarovalex azarovalex Jan 16, 2025

Choose a reason for hiding this comment

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

We probably shouldn't bail out early if the result is markin with sending, there are a lot of other checks further down in this function that should still pass.

Also the return value can't be true here, since based on the comment here true means that an error was emitted, in our case though there is no error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you.

Also the return value can't be true here,
It was my misunderstanding🤦🏻‍♂️ Let me fix it.

We probably shouldn't bail out early if the result is markin with sending, there are a lot of other checks further down in this function that should still pass.

How about checking diag.ID like the below?

if (diag.ID == diag::non_sendable_result_in_witness.ID &&
            func->hasSendingResult() && !resultType->hasError())
    return false;

non_sendable_result_in_witness.ID is “non-sendable type %0 cannot be returned from %2 implementation to caller of protocol requirement %1”.

Copy link
Contributor Author

@stzn stzn Jan 23, 2025

Choose a reason for hiding this comment

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

@azarovalex

Hi, what do you think about #78601 (comment) ?

I changed it in this commit

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.

subscript declarations can have sending on parameters/result:

struct S {
    subscript(_: sending Int) -> Bool { true }
}

I think it should be possible to unify some of the logic in this function because i.e. getParameters() and getIndices() both produce ParameterList * and for results the logic is pretty much the same except to exactly how the type has been retrieved.

@stzn
Copy link
Contributor Author

stzn commented Jan 25, 2025

@xedin

Thank you very much for the advice. I combined the function and subscript checks into one.

Sorry, It's my misunderstanding. the below subscirpt is not async.

I have a question. I tried reproducing the issue with the subscript, but I encountered a different error as shown below.!

class NonSendable {}
protocol P {
subscript(_: sending NonSendable) -> Bool { get }
}
@mainactor
class C: P {
// Main actor-isolated subscript 'subscript(_:)' cannot be used to satisfy nonisolated protocol requirement
subscript(_: sending NonSendable) -> Bool { true } // If add nonisolated, the error is solved
}

I tried more patterns, but all of them successfully built.
Is there a way to reproduce the issue with subscripts? I’d like to add tests for it.

@stzn stzn force-pushed the fix-sending-typecheck branch 2 times, most recently from d350737 to ed63c60 Compare January 27, 2025 00:46
// Check the result type of a function.
if (auto func = dyn_cast<FuncDecl>(function)) {
// Check the result type of a function or subscript.
if (auto func = dyn_cast<FuncDecl>(decl)) {
if (funcCheckOptions.contains(FunctionCheckKind::Results)) {
// only check results if funcCheckKind specifies so
Type resultType = func->getResultInterfaceType().subst(subs);
Copy link
Contributor Author

@stzn stzn Jan 27, 2025

Choose a reason for hiding this comment

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

That is probably incorrect. It looks that we need to use getElementInterfaceType in the case of subscript. I'll correct it (and the above if condition as well).

@stzn
Copy link
Contributor Author

stzn commented Jan 28, 2025

I’m currently looking into several potential issues related to subscripts that use the
sending keyword.

1

An assertion failure occurs during SIL generation (see https://github.com/swiftlang/swift/blob/main/lib/AST/ASTPrinter.cpp#L3816). It appears that
ParameterTypeFlags is not set the sending flag. The issue occurs when printing the subscript getter in SILWitnessTable.

sil_witness_table hidden S: P module main {
  method #P.subscript!getter: <Self where Self : P> (Self) -> (Assertion failed: (flags.isSending() && "Only valid when sending is enabled"), function printParameterFlags, file ASTPrinter.cpp, line 3759.

Steps to reproduce:

Run the following: swift -frontend -emit-sil -swift-version 6

protocol P {
    subscript(_: sending NonSendable) -> Bool { get }
}

struct S: P {
    subscript(_: sending NonSendable) -> Bool { true }
}

It seems that setSending need be called when subscriptParam includes the sending keyword in the case of a subscript getter.

https://github.com/swiftlang/swift/blob/main/lib/AST/Decl.cpp#L10701

if (subscriptParam->isSending())
        param->setSending();

Could this be a separate issue that would be better addressed in a different PR?

I fixed it in another PR.
#79042

2

The SubscriptDecl does not have a HasSending field, so retrieving sending information relies on the
SendingTypeExpr. I am uncertain whether this is the correct approach.

It appears ok.

3

The sending keyword is not printed because the ASTPrinter does not handle it in visitSubscriptDecl.

For example, given the following code:

struct S5 {
    subscript(_: Bool) -> sending NonSendable { NonSendable() }
}

The output is:

struct S5 {
  subscript(_: Bool) -> NonSendable { get }
  init()
}

In this case, the sending keyword is omitted. I am unsure if this behavior is intentional.

Is this related to #78612?

I fixed it in another PR.
#79560

@stzn stzn force-pushed the fix-sending-typecheck branch 2 times, most recently from 59c071a to bd1cd03 Compare January 28, 2025 19:51
@stzn
Copy link
Contributor Author

stzn commented Jan 28, 2025

I made a mistake with the branch. I corrected it.

@stzn stzn force-pushed the fix-sending-typecheck branch 2 times, most recently from 81c2f43 to 414fbe4 Compare January 28, 2025 20:06
…eters or return types of an actor-isolated function
@stzn stzn force-pushed the fix-sending-typecheck branch from 414fbe4 to acfabdd Compare January 28, 2025 20:07
@stzn
Copy link
Contributor Author

stzn commented Mar 28, 2025

I think I can find all of my answers in the comment.

So, I’d like to proceed with this PR again.

What do I need to modify more to fix the original issue?

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.

Despite the sending return, I cannot satisfy protocol with a sending return
4 participants