Skip to content

[Sema]Coerce to rvalue when withoutActuallyEscaping receives an lvalue argument #79238

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

stzn
Copy link
Contributor

@stzn stzn commented Feb 8, 2025

Resolves #79078

When withoutActuallyEscaping receives an lvalue argument, the compiler crashed since FunctionType is expected as the type of NonescapingClosureValue, but in the case of lvalue, it has the type LValueType. Then, closureFnTy is null and aborted.

 auto closureFnTy =
          E->getNonescapingClosureValue()->getType()->getAs<FunctionType>();

https://github.com/swiftlang/swift/blob/main/lib/AST/ASTVerifier.cpp#L2353

It could be an lvalue when the argument is inout, consuming, sending.

If I understand correctly, the body parameter of withoutActuallyEscaping expects an rvalue. I also referred to the below comment.

// Proceed with a "WithoutActuallyEscaping" operation. The body closure
// receives a copy of the argument closure that is temporarily made
// @escaping.

https://github.com/swiftlang/swift/blob/main/lib/Sema/TypeOfReference.cpp#L2245

Given this, it seems we need to coerce the lvalue to an rvalue.

--

Regarding this, we always set withAsync(true) in the case of withoutActuallyEscaping (https://github.com/swiftlang/swift/blob/main/lib/Sema/TypeOfReference.cpp#L2278). As a result, the async version gets selected by the ConstraintSystem even when it's not async.

---Solution---
Fixed score: [sync-in-asynchronous(s), weight: 17, impact: 1]
Type variables:
  $T0 as Void @ locator@0x127053000 [[email protected]:4:5]
  $T1 as (@escaping () async -> Void, (@escaping () async -> Void) async -> Void) async -> Void @ locator@0x127053068 [[email protected]:4:15]
  $T2 as () async -> Void @ locator@0x1270530b0 [[email protected]:4:15 → function argument]
  $T3 as () async -> Void @ locator@0x1270530b0 [[email protected]:4:15 → function argument]

Overload choices:
  locator@0x127053068 [[email protected]:4:15] with Swift.(file).withoutActuallyEscaping(_:do:) as withoutActuallyEscaping: ($T2, ($T3) async throws($T5) -> $T4) async throws($T5) -> $T4

https://github.com/swiftlang/swift/blob/main/lib/Sema/CSSimplify.cpp#L1919

However, I believe this approach is incorrect when the declaration is not in an async context. Therefore, it is necessary to check whether the context is asynchronous or not.

@stzn stzn force-pushed the fix-withoutActuallyEscaping-crash branch from f6dc044 to 3523a94 Compare February 8, 2025 08:11
@@ -8217,7 +8217,7 @@ Expr *ExprRewriter::finishApply(ApplyExpr *apply, Type openedType,
// Resolve into a MakeTemporarilyEscapableExpr.
auto *args = apply->getArgs();
assert(args->size() == 2 && "should have two arguments");
auto *nonescaping = args->getExpr(0);
auto *nonescaping = cs.coerceToRValue(args->getExpr(0));
Copy link
Contributor

@xedin xedin Feb 8, 2025

Choose a reason for hiding this comment

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

I think we should do exactly what TypeOf does here, use coerceCallArguments before attempting to use args, that would make sure that AST is updated correctly and call gets LoadExpr as well as nonescaping.

Copy link
Contributor Author

@stzn stzn Feb 8, 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 advice.

When I attempted to replicate the behavior of TypeOf, another crash occurred

SIL verification failed: cannot call an async function from a non async function:

https://github.com/swiftlang/swift/blob/main/lib/SIL/Verifier/SILVerifier.cpp#L2057

With the following code:

func test(
    f: inout () -> Void
) {
    withoutActuallyEscaping(f) { $0() }
}

It appears that withoutActuallyEscaping(_:do:) is regarded as an async function.

(call_expr type="<null>" isolation_crossing="none"
  (declref_expr type="(@escaping () -> Void, (@escaping () -> Void) async -> Void) async -> Void" location=79078.swift:37:5 range=[79078.swift:37:5 - line:37:5] decl="Swift.(file).withoutActuallyEscaping(_:do:)" function_ref=single apply)

What I did

        auto *args = apply->getArgs();

        auto appliedWrappers = solution.getAppliedPropertyWrappers(
            calleeLocator.getAnchor());
        auto fnType = cs.getType(fn)->getAs<FunctionType>();
        args = coerceCallArguments(
            args, fnType, declRef, apply,
            locator.withPathElement(ConstraintLocator::ApplyArgument),
            appliedWrappers);
        if (!args)
          return nullptr;

        auto *nonescaping = cs.coerceToRValue(args->getExpr(0));

I will investigate the issue. 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 believe I’ve identified the cause of the SIL verification failed issue.

It seems that the withAsync(true) is always being set:
https://github.com/swiftlang/swift/blob/main/lib/Sema/TypeOfReference.cpp#L2278

However, I don’t think this is necessary when the code is not in an async context. To address this, I updated the implementation by passing the DeclContext to the getTypeOfReferenceWithSpecialTypeCheckingSemantics function. This allowed me to set an isAsync flag based on the current context.

You can check the changes here:
ebca3f8

@stzn stzn force-pushed the fix-withoutActuallyEscaping-crash branch from 3523a94 to 4ff3542 Compare February 9, 2025 01:51
@stzn stzn force-pushed the fix-withoutActuallyEscaping-crash branch from 4ff3542 to 7365d09 Compare February 9, 2025 01:52
@stzn stzn force-pushed the fix-withoutActuallyEscaping-crash branch from aa75aad to 4fd9a44 Compare February 9, 2025 02:04
@stzn stzn marked this pull request as ready for review February 9, 2025 21:42
@omochi
Copy link
Contributor

omochi commented Feb 11, 2025

@swift-ci please smoke test

@stzn
Copy link
Contributor Author

stzn commented Feb 11, 2025

I apologize for not noticing this change earlier. I believe this change was expected since it's not in an async context, but please correct me if I'm wrong.

auto bodyClosure = FunctionType::get(arg, result,
FunctionType::ExtInfoBuilder()
.withNoEscape(true)
.withAsync(true)
.withAsync(isAsync)
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this was originally added in #35664

@@ -2261,10 +2261,11 @@ static DeclReferenceType getTypeOfReferenceWithSpecialTypeCheckingSemantics(
CS.getConstraintLocator(locator, ConstraintLocator::ThrownErrorType),
0);
FunctionType::Param arg(escapeClosure);
bool isAsync = CS.isAsynchronousContext(useDC);
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a problem with this approach because closures are allowed to assume async keyword but ClosureEffectsRequest only checks presence of await so context cannot always tell you at this point exactly whether a closure is async or not when it has withoutActuallyEscaping in its body.

I this the main issue here is that we need to set the bit on an interface type at the point where we don't actually know whether a type that is passed in as the first argument is async or not.

I think we need a special constraint that would allow to form bodyClosure type based on escapeClosureType and the same thing for refType when types are sufficiently resolved...

Copy link
Contributor

Choose a reason for hiding this comment

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

Meanwhile maybe there is a way to workaround in SILGen the fact that there is always a async as a temporary fix?

Copy link
Contributor Author

@stzn stzn Feb 22, 2025

Choose a reason for hiding this comment

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

Apologies for my late reply.

I've been exploring potential workarounds in SILGen for the async requirement, but it's challenging to me to find a viable solution for now.

I attempted several approaches, including implementing the following change:
stzn@e07814b

I mainly tried to make types async (e.g. from () -> () to () async -> ()).

While this modification resolved the SIL verification failure for the inout sync pattern, it unfortunately triggered a different assertion failure when running the following code:

func checkAssumeMainActor() {
    MainActor.assumeIsolated {}
}

The resulting error message was:

Assertion failed: ((!F || opTI->isABICompatibleWith(resTI, *F).isCompatible()) && "Can not convert in between ABI incompatible function types"), function create, file SILInstructions.cpp, line 2819.

Despite attempting to make various components async, I haven't been able to resolve this assertion error. Furthermore, I'm uncertain whether my current approach is the correct path forward.

Would you be able to provide some guidance on this matter?

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.

@xedin
I'd like to hear your thoughts on the workaround direction. Thank you.

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.

withoutActuallyEscaping with sending @escaping causes crash
3 participants