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

AsyncParsableCommand exits before running run() async #538

Open
2 tasks done
julianiaccopucci opened this issue Jan 6, 2023 · 6 comments
Open
2 tasks done

AsyncParsableCommand exits before running run() async #538

julianiaccopucci opened this issue Jan 6, 2023 · 6 comments
Labels
bug Something isn't working

Comments

@julianiaccopucci
Copy link

julianiaccopucci commented Jan 6, 2023

When using an AsyncParsableCommand with @main, the command-line-tool exits without running the run() async function and prints the help log.
Note: run() without async runs the function.

ArgumentParser version: 1.2.0
Swift version: swift-driver version: 1.45.2 Apple Swift version 5.6 (swiftlang-5.6.0.323.62 clang-1316.0.20.8)
MacOS 12.6

Checklist

  • If possible, I've reproduced the issue using the main branch of this package
  • I've searched for existing GitHub issues

Steps to Reproduce

Option A: Reproduce by running the Example in this repository

Option B:
Screenshot 2023-01-06 at 10 36 30

Expected behavior

The run() async function should run

Actual behavior

The run() async function isn't run and the program exits with the help log.

@julianiaccopucci
Copy link
Author

julianiaccopucci commented Jan 6, 2023

Just to show what works and what doesn't. Running it from Xcode or command line:

This works:
Screenshot 2023-01-06 at 11 24 12

These three don't work: Note that number three is not async, but the @available is specified at the function level.
1)------------------------------
Screenshot 2023-01-06 at 11 28 50
2)------------------------------
Screenshot 2023-01-06 at 11 29 11
3)------------------------------
Screenshot 2023-01-06 at 11 28 19

@julianiaccopucci
Copy link
Author

I'm using Swift 5.6, so I need to create a standalone type as your asynchronous @main entry point.

@julianiaccopucci
Copy link
Author

Reopening it as the documentation of AnycMainProtocol indicates to use @main directly as I was using it which causes the issue above.

@julianiaccopucci
Copy link
Author

julianiaccopucci commented Jan 6, 2023

I don't experience the issue by setting my package minimum target to MacOS 12.6. But it's not clear what the min version is for this package.
Screenshot 2023-01-06 at 14 48 57
I think this is still an issue and should be addressed by setting a min version for the project or by fixing it for older versions.

@mac-cain13
Copy link

Today I also ran into this issue, my command line tool didn't specify a platform at all in its Package.swift. This causes the following code to compile perfectly fine, but once it runs it just prints the usage instruction:

@main
struct MyCommand: AsyncParsableCommand
  mutating func run() async throws {
    print("Hello World" )
  }
}

Debugging shows that the main function of the ParsableCommand is ran instead of the AsyncParsableCommand, that doesn't try to call the async variant.

Declaring platforms: [.macOS(.v10.15)] (or more recent) does make it work.

It would be great if the library can be adjusted so it gives a clear compile error. Or at least the documentation of AsyncParsableCommand could be updated so this is easier to figure out for people running into the issue.

@blochberger
Copy link

I ran into this or at least a very similar issue as well. I did not use the main functions provided by ArgumentParser but invoked run() directly.

After looking a bit deeper, I noticed that the default implementation of ParsableCommand.run() is executed, instead of AsyncParsableCommand.run().

Reproducible with the following snippet in a main.swift file or playground:

import ArgumentParser

struct Foo: AsyncParsableCommand {
    mutating func run() async throws {
        print("Foo")
    }
}

// Like AsyncParsableCommand.main()
do {
    var cmd: ParsableCommand = try Foo.parseAsRoot()
    if var asyncCmd = cmd as? AsyncParsableCommand {
        try await asyncCmd.run()
    } else {
        try cmd.run()
    }
} catch {
    Foo.exit(withError: error)
}

The Foo.run() function is never executed. If you set a breakpoint at the default implementation in ParsableCommand.run() that will be hit instead.

Note that the compiler warns that the await expression is useless, since no async operations occur within.

The underlaying problem seems to be that Foo has two run() functions, see simplified:

struct Foo {
    func run()
    func run() async
}

This overload behaviour was introduced in SE-0296, see specifically the section Overloading and overload resolution, and also discussed in the Swift forum. The overloads are called based on their context. If you are in another async function, you get the async overload, if you are in a synchronous context, you get the non-async overload.

Seems like the main context in main.swift allows both, which can quickly be confirmed by adding a non-ambiguous async function:

extension AsyncParsableCommand {
    mutating func runAsync() async throws {
        try await self.run()
    }
}

do {
    var cmd: ParsableCommand = try Foo.parseAsRoot()
    if var asyncCmd = cmd as? AsyncParsableCommand {
        //try await asyncCmd.run()
        try await asyncCmd.runAsync()
    } else {
        try cmd.run()
    }
} catch {
    Foo.exit(withError: error)
}

Warpping everything into an async function works as well, which is basically what AsyncParsableCommand.main() does. For ParsableCommand there is no conflict and the synchronous function will be run.

There is a similar effect for the main function. Looking at the @available annotations in this project's code and the comments in this thread, it seems like macOS 10.15 fixed the @main wrapper, so that it properly calls the async main function. However, if you are using main.swift and invoke main directly, the synchronous main function is preferred, which also triggers failAsyncPlatform() (with a less helpful error message). So you would need to wrap it into an asynchronous function explicitly:

func main() async {
    await Foo.main()
}
await main()

Or disambiguate the overload:

extension AsyncParsableCommand {
    static func mainAsync() async throws {
        try await main()
    }
}
await Foo.mainAsync()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants