Skip to content

Unused arguments to async fn are dropped too early #54716

Closed

Description

@Nemo157

Unused arguments to async fn are not moved into the resulting generator so are dropped before the future runs, here's some example psuedo-code demonstrating this (full running playground example here):

async fn foo(log1: DropLog, _log2: DropLog) {
    println!("Got log1: {:?}", log1);
}

fn bar(log1: DropLog, _log2: DropLog) {
    println!("Got log1: {:?}", log1);
}

fn main() {
    foo(DropLog(1), DropLog(2)).poll(...);
    println!();
    bar(DropLog(3), DropLog(4));
}

which gives the output:

Dropped DropLog(2)
Got log1: DropLog(1)
Dropped DropLog(1)

Got log1: DropLog(3)
Dropped DropLog(4)
Dropped DropLog(3)

I found this because of a related issue with futures-await(0.1) posted to urlo, it's at the very least surprising behaviour that needs documenting if not a bug.

Activity

  1. added
    A-destructorsArea: Destructors (`Drop`, …)
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    C-bugCategory: This is a bug.
    on Jan 27, 2019
  2. nikomatsakis commented on Mar 5, 2019

    @nikomatsakis
    Contributor

    Marking this as blocking stabilization of async-await. Obviously, changing the semantics here would alter the semantics of stable code. Moreover, there's a good case to be made that the current semantics are incorrect. @cramertj and I (along with a few others) discussed this in a recent meeting. Our conclusion was that we effectively wanted a desugaring like the following:

    async fn foo(<pattern> @ x: Type) {
    }
    
    // becomes
    fn foo(<pattern> @ x: Type) {
      async move {
        let <pattern> = x;
      } // <-- as you "exit" the async block
    }

    where x is a kind of "fresh name" not visible to the user.

    Some of the key test cases we want to consider would be:

    • async fn foo(_: Foo) -- the Foo value should be dropped after the future is "waited"
    • async fn foo(_x: Foo) -- same

    This is quite analogous then to the behavior for fn foo(_: Foo) and friends -- basically after the "return" executes, the parameters are dropped (whereas current behavior drops before the body of the function even begins (i.e., before the future is polled)).

  3. nikomatsakis commented on Mar 5, 2019

    @nikomatsakis
    Contributor

    Tagging as E-needs-mentor as we need to write up some notes on how to alter the desugaring here. One bit I can see being complex is that the HIR presently has no way to "name" a parameter apart from entering a pattern.

  4. self-assigned this
    on Mar 5, 2019
  5. removed their assignment
    on Apr 2, 2019
  6. cramertj commented on Apr 2, 2019

    @cramertj
    Member

    (nominating for discussion in T-lang to resolve the desired behavior)

  7. cramertj commented on Apr 2, 2019

    @cramertj
    Member

    Summary of the current issues by @davidtwco can be found on Zulip.

  8. pnkfelix commented on Apr 4, 2019

    @pnkfelix
    Contributor

    blocks #50547

  9. 12 remaining items

  10. CAD97 commented on Apr 19, 2019

    @CAD97
    Contributor

    FWIW, I expect parameters to behave as a "magic" let «pat» = get_fn_param_from_calling_convention!(). This would mean that a parameter that is explicitly ignored (_: «type») would drop "immediately", as happens for let _ = «expr», and a "silenced unused" parameter (_«ident»: «type») would drop "late", as happens for let _«ident» = «expr».

    Maybe the difference in behavior here is surprising, but we have it, and authors know to keep this in mind when holding an e.g. scope_guard. We have a warning for unused bindings that don't start with an underscore today; if the type has drop glue we can theoretically suggest using either depending on when the user wants the drop glue to run.

    I definitely do not expect the addition of the use of a usable binding (that is, one that isn't _) to change drop order. I would consider it a misfeature if that happened. The _ pattern exists as a "trash can" to drop things you don't want anymore into. If I don't use that pattern, I'm ok with it sticking around (and will have an unused lint if it's not prefixed with an underscore).

  11. najamelan commented on Apr 19, 2019

    @najamelan
    Contributor

    (_: «type») would drop "immediately", as happens for let _ = «expr», and a "silenced unused" parameter (_«ident»: «type») would drop "late", as happens for let _«ident» = «expr».

    This seems to solve all the performance questions for the price of users having to know that the two syntaxes don't behave the same. I must say I like it. There won't be any free cake whichever way we take this.

  12. cramertj commented on Apr 19, 2019

    @cramertj
    Member

    We discussed this at the last lang team meeting and decided to change the drop order of async fn to match the existing drop order used by non-async functions. One significant motivation was the converting of things like fn foo(_lock: Mutex<()>) { ... } and other similar "just for the Drop effects" arguments to async fn, and that users might be surprised and confused by the difference in behavior.

    However, in the case that we're not able to come up with a satisfactory implementation prior to resolving the other blockers for async/await, stabilization, we will temporarily introduce a feature gate against unused non-Copy arguments to async fn to allow stabilization to move forward.

  13. najamelan commented on Apr 19, 2019

    @najamelan
    Contributor

    @cramertj Sorry if this is a noob question, but is the drop order for non-async functions: _ => drop early, _ident => drop late? And will that be consistent behaviour everywhere, with the user being able to opt into perf gain by using just _?

  14. cramertj commented on Apr 19, 2019

    @cramertj
    Member

    And will that be consistent behaviour everywhere, with the user being able to opt into perf gain by using just _?

    No, the performance gain being discussed here is not achievable by using _ in argument position. _ bindings in argument position drop at the end of the function, not at the start (or, in this case, prior to the future even having started).

  15. CAD97 commented on Apr 19, 2019

    @CAD97
    Contributor

    Would that be a viable future change (for sync and async) to put through Crater? (As iirc parameter drop order isn't actually specified and it would make it more consistent with let.)

    It doesn't need to be, but it seems desirable on the surface.

  16. cramertj commented on Apr 19, 2019

    @cramertj
    Member

    @CAD97 That would be a breaking change to a pretty significant amount of existing code, a significant chunk of which is probably unsafe (e.g. functions using _: Mutex<()> as a guard, as I mentioned above).

  17. comex commented on Apr 20, 2019

    @comex
    Contributor

    OTOH, if the behavior were changed in an edition, it would be a relatively straightforward automatic migration...

  18. self-assigned this
    on Apr 21, 2019
  19. davidtwco commented on Apr 21, 2019

    @davidtwco
    Member

    Assigning myself again since I have an open PR for this.

  20. added a commit that references this issue on Apr 23, 2019
    fbc5e88
  21. added 2 commits that reference this issue on Apr 23, 2019
    62d1574
    59988da
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-async-awaitArea: Async & AwaitA-destructorsArea: Destructors (`Drop`, …)AsyncAwait-PolishAsync-await issues that are part of the "polish" areaC-bugCategory: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions