What to do about `static func main() throws`?

ever since backtracing landed in 5.9, @main with a throwing main function is quite useless. (to me, anyway.)

to adapt, i’ve started making use of some function decorators such as:

#if canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#else
#error("unsupported platform")
#endif

extension SystemProcess
{
    @MainActor
    public static
    func `do`(_ operation:() async throws -> Void) async
    {
        do
        {
            try await operation()
        }
        catch let error
        {
            print(error)
            exit(1)
        }
    }
}
@main
enum Main
{
    static
    func main() async
    {
        await SystemProcess.do(Self._main)
    }

    private static
    func _main() async throws
    {

that’s not the worst thing in the world, but it seems odd to me that static func main() async throws is still allowed when there are likely no situations in which you would ever want to throw from main. the backtrace will never contain any useful information, it just wastes 30 seconds of CPU time.

💣 Program crashed: Illegal instruction at 0x00007f95c110cc82

Thread 5 "NIO-ELT-0-#0" crashed:

0 0x00007f95c110cc82 _assertionFailure(_:_:file:line:flags:) + 354 in libswiftCore.so
1 0x00007f95c1173c6c swift_errorInMain + 619 in libswiftCore.so
2 0x00007f95c1822fb0 completeTaskAndRelease(swift::AsyncContext*, swift::SwiftError*) in libswift_Concurrency.so

Backtrace took 28.80s

what should we do about static func main() async throws?

2 Likes

It feels to me that — instead of “crashing” — throwing from main should dump the error (optionally?) and then exit(1). That seems like a reasonable default behavior.

It could be interesting to see some kind of standardized SystemError type that when thrown would have different well defined behaviors. Kinda like LocalizableError. But maybe that’s always too application specific to standardize.

That would lead to a situation where these three forms don’t have the same behavior:

func main() throws {
  try someThrowingFn()
}

func main() {
  try! someThrowingFn()
}

// main.swift
try! someThrowingFn()

To not get a backtrace from the first would be frustrating, because the fix would be to turn all your trys into try!s.

If you don’t want main to produce a backtrace on failure, you can wrap everything in a do { } catch { } that dumps the error and exits. Then you’re visibly and explicitly opting out of the backtrace capture.

3 Likes

why would this be preferable to logging the error and exiting gracefully? the backtrace would never contain any useful information, it will just cause your terminal to hang for 30 seconds.

Why wouldn’t the backtrace contain useful information? You can have more than one try in a function. Wouldn’t you like to know which one threw?

it would not trace where the error was thrown from, it would only trace where the error was caught, which is always the compiler-generated swift_errorInMain.

2 Likes

Oof, that’s kind of a bummer.

@al45tair maybe we could consider changing the default behaviour to trace back errors thrown from main if that's possible at all?

3 Likes

for context, there was some discussion about this last year, but i don’t recall it leading anywhere.

Yeah, it didn't end up going anywhere. I had it pretty well working, but didn't have the time or energy to argue why we don't actually want to expose exit directly. Given the discovery that CommandLine.arguments doesn't work consistently on Linux, I may take another stab at proposing a "full-featured" main-entrypoint that passes in the arguments and has a return-code, though that would likely not be for throwing purposes, but more so that I can move away from the ol'

@_cdecl("main")
func main(_ argc: Int32, _ argv: UnsafePointer<UnsafePointer<Int8>>) -> Int {
    // ...
    return 0
}

That said, Doug's suggestion of having the ability to return directly from top-level code is growing on me with every script I write, especially in guard blocks.

guard let thingamabob = getAThingamabob() else {
  print("No thingamabob for you")
  return 1
}

would make me happier than the current fatalError that you get to do today. I don't really need a stacktrace for main there.

I can't guarantee I have time or energy to push this through at the moment either though. ¯\_(ツ)_/¯

1 Like

Shouldn’t this also return Int32, not Int? The calling conventions are compatible (at least on x86-64), but this just goes to show how fragile it is.

From what I remember, C# allows its Main method to return void or int, and allows it to take either no arguments or a string[] argument. (C# doesn’t mark whether a method throws in its type system, so I have no advice in that regard.)

The spec says main has return type int, but different operating systems on the same hardware can choose between LP, ILP and LLP. So I think the real answer is “whatever works”.

Maybe CInt? ¯\_(ツ)_/¯ Anyway, I think letting the compiler do the right thing consistently is probably a good thing to do. Given that exit skips deinit's and defer blocks, and is generally not something we want to encourage in libraries and frameworks, or really anywhere that isn't the main function, I still think that just returning from main is the right thing. But my focus is needed elsewhere at the moment unfortunately.

2 Likes

I'm not quite sure what you mean by this. Swift error handling isn't done by stack unwinding, so we don't actually know where the error was originally raised.

I'm honestly not certain what the correct behaviour here is; clearly if you throw an error from main, there's nothing to catch it and today that means you've crashed. There's no way in an error to specify an exit code, so it can't really be any kind of "clean" exit right now. On the other hand, using exit() from the C library to terminate the program is awkward in other ways (as mentioned in the aforementioned discussion from last year, which I'm reading as I write this).

It seems to me that this is more of a language design issue rather than a runtime issue per se, and that there probably should be a more Swift-y way to exit with an error code.

1 Like

If you use try!, the backtrace is taken from the line that failed. Whereas if you use do { try }, every single failure’s backtrace starts from the same spot: the synthesized catch that wraps main.

2 Likes

I'm not sure how that affects things. The point is that we'll get a backtrace from the place the program crashes, which is presently the point at which Swift spots that there's an error that it wasn't expecting. I thought Max might be thinking that we could somehow find out where the Error originated, but I don't believe that's possible, since we've actually returned from the function(s) that threw it at that point.

Would something like this work?

extension CommandLine {
  protocol Error: Swift.Error {
    var exitCode: CInt
  }
}

If an error that confoms to CommandLine.Error is thrown at top level, it exits the program with its exitCode.

That is indeed the problem. If I use try!, the backtracer points at the actual try! line that threw:

crasher/main.swift:8: Fatal error: 'try!' expression unexpectedly raised an error: crasher.Err()

💣 Program crashed: System trap at 0x00000001920151e8

Thread 0 crashed:

0 0x00000001920151e8 _assertionFailure(_:_:file:line:flags:) + 268 in libswiftCore.dylib
1 0x0000000192081888 swift_unexpectedError + 516 in libswiftCore.dylib
2 static Main.main() + 144 in crasher at /Users/kyle/crasher/Sources/main.swift:8:8

     6│ @main struct Main {
     7│         public static func main() {
     8│                 try! { throw Err() }()                                                                                                    
      │        ▲
     9│                 try! { throw Err() }()
    10│         }

Whereas if I use func main() throws, the backtracer points ambiguously at the struct main declaration:

Swift/ErrorType.swift:200: Fatal error: Error raised at top level: crasher.Err()

💣 Program crashed: System trap at 0x00000001920151e8

Thread 0 crashed:

0 0x00000001920151e8 _assertionFailure(_:_:file:line:flags:) + 268 in libswiftCore.dylib
1 0x00000001920b2a24 swift_errorInMain + 492 in libswiftCore.dylib
2 crasher_main + 80 in crasher at /Users/kyle/crasher/Sources/main.swift:6:14

     4│ struct Err: Error { }
     5│ 
     6│ @main struct Main {                                                                                                                       
      │              ▲
     7│         public static func main() throws {
     8│                 throw Err()

Can the second case be made equivalent to the first?

Each try! introduces a trap in the caller for each throwing call in the subexpression. You still don't get the backtrace from where the error originated, only where it was caught and a fatal error triggered. Without special casing the code generation for a @main function, we can't make propagating an error out of it do the same thing, since the error was passed out of main into the pre-main function. In general, if you want backtrace information in an error from where it originated, then it'd have to be captured when the error was constructed in its initializer.

1 Like

I get why the backtrace can’t start where the error was thrown, but it’s pretty unfortunate that it can’t start at the line that caused control flow to exit the program. I would be in favor of special-casing @main, perhaps by collaborating with the pre-main function to preserve the IP of the line that branched out of main.