@main in a single Swift file?

I've got a simple Swift script that I'm trying to run:

// swift-driver version: 1.62.15 Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) 
// Target: arm64-apple-macosx13.0
// Testing.swift
@main
struct Testing {
    static func main() {
        print("Hello, world!")
    }
}

However when I run swift Testing.swift from the command line, I get the following error:

Testing.swift:1:1: error: 'main' attribute cannot be used in a module that contains top-level code
@main
^
Testing.swift:1:1: note: top-level code defined in this source file
@main
^

Other posts seem to suggest that this happens when a file is named main.swift, but the file is named Testing.swift and there are no other files in the directory.

Am I missing something obvious here? Does @main work in single file scripts?

compile with -parse-as-library.

2 Likes

Thanks - so the whole flow is:

swiftc -parse-as-library Testing.swift
./Testing

Surprised that @main isn't something that works out of the box with swift - seems like a bug perhaps? Or is there a reason it's not supported?

For context - I'm testing out the latest from vscode swift and regardless of how I run it in the terminal the editor will show an error on the line with @main. Might be confusing for first timers.

1 Like

I've the same problem, but only on Windows, and not in a script but a command-line app. Your recent screenshot is from Visual Studio and your first entry contains Target: arm64-apple-macosx13.0. Do both platforms suffer?

Yes I'm on macOS. Sounds like @main doesn't work unless the special compiler flag is used or it's inside a package.

Removing the @main and instead adding Testing.main() at the top level works fine with swift Testing.swift:

struct Testing {
    static func main() {
        print("Hello, world!")
    }
}

Testing.main()

It's an easy workaround but I was just surprised that @main doesn't work out of the box.

It would be great if instead of emitting this error, the compiler just set the -parse-as-library flag and started over from the top.

3 Likes

The rules around how top-level code is treated are somewhat nuanced:

  • @main is forbidden in files that allow top-level statements, because the top-level statements provide an implicit entry point.

  • If you invoke the compiler without any special flags and pass it a single source file, it is treated as top-level code (meaning @main is forbidden; you just write the top-level statements instead). Passing -parse-as-library to the compiler disables this top-level behavior.

  • If you invoke the compiler without any special flags and pass it multiple source files, all files are treated as library code (top-level statements are forbidden), unless one of the files you pass is named main.swift, in which case the top-level statements in that specific file provide the main function.

IMO, there's not much of an advantage to writing @main explicitly in small single-file scripts; the real power of that attribute is when it's used in a library where some protocol defines the entry point and you just conform to that protocol and get it for free. But clearly folks do want to use it directly even for small scripts, so (in a slight variation to Nate's comment above) I wonder if we'd break compatibility with anything if we just had the frontend use a "deferred main decision" by default and decide whether to allow top-level statements based on whether it detected @main in the file.

7 Likes

Have async top-level statements shipped yet? That's the only reason I use @main, it's the only way to get an async entry point into a simple executable.

3 Likes

I believe that was implemented in this PR, which shipped in Swift 5.7. But I don't have a lot of experience with the feature directly.

Gotcha - thanks for clarifying, makes sense!

For what it's worth I was working off of this stack overflow question.

Since @main is described in it's proposal A Swift language feature for designating a type as the entry point for beginning program execution. IMO it could be confusing for first timers who are doing some simple googling to test out some Swift, see @main is the latest in how to designate an entry point, then get hit with a somewhat cryptic error when they do.

So yeah, detecting @main and deciding whether to allow top-level statements sounds like a great solution!

1 Like

@Jon_Shier

Have async top-level statements shipped yet? That's the only reason I use @main , it's the only way to get an async entry point into a simple executable.

Yes, yes it has.

Now back to the original question, @joshw

Newer compilers have a note suggesting you use -parse-as-library if the @main is intentional. Adding actionable note to @main error by etcwilde · Pull Request #59919 · apple/swift · GitHub

It's still not a great situation though, and I'm regularly getting pulled into conversations and questions on it. So, story time!

So. first, the name of the flag is confusing. You have an executable, not a library. It comes from an implementation detail in the C++ parser. There's an enum that tells the parser how it's parsing the file.

A "Library", as far as the parser is concerned, is any normal Swift code. a "Main" swift file is top-level code. -parse-as-library just forces the parser to set the main source file's SourceFileKind to Library instead of Main.

The current heuristic used looks at the number of files and their names. Any single file passed to the parser is given the "Main" SourceFileKind. If more than one file is passed to the compiler, any file with the name "main.swift" will be given the SourceFileKind of "main". It's important to note here that SwiftPM and Xcode both have different heuristics than the compiler, I'm only referring to the compiler.

Okay, so why are we in this mess in the first place? Parsing top-level code and a normal Swift file is different, and the resulting parse trees are not the same. Unfortunately, in order to know whether we have an @main attribute, we have to parse. By the time we know we have an @main, it's too late because we've already parsed incorrectly. I've considered the trade-off of tearing everything down and re-starting the parse in Library mode. The new Swift-Parser work does things differently. I'm hoping that I'll be able to fish the attribute out of the token stream before any ASTs are formed, and make that judgement early.
With the current parser, there's not really a point between the lexing and parsing where I can check before it's too late. The alternative to manually using -parse-as-library would be to tear everything down and restart the compilation when we get it wrong. It's a tradeoff. Maybe the wrong one? To balance things out a bit, I added the note to newer compilers to recommend adding -parse-as-library if the @main is intentional. That way you have some options and aren't just left in the dark.

Hopefully that helps shed some light on what is going on here.

9 Likes

IMO the preferred trade-off would be to emit a warning (instead of an error) with the -parse-as-library recommendation, and then tear things down and reparse as a library anyway. If @main exists, the intent is clear, so the error is just an unpleasant user experience with the tools.

3 Likes

.. for anyone following in my footsteps, using SPM, the magical incantation is:

targets: [
    .executableTarget(
        name: "PropApp",
        dependencies: ["PropLib"],
        swiftSettings: [
            .unsafeFlags(["-parse-as-library"])
        ]),
]
4 Likes

It's simpler to leave in the @main and just rename main.swift to Testing.swift

Apologies for waking up an old thread but I just got bitten by this. I am surprised the issue has not been addressed.

Take any programming language and you will be able to find some pretty straightforward—and crucially, working—instructions on how to write the simplest of programs, e.g., one that outputs "Hello World!" Following these instructions will reliably produce a working program. This is sill not the case for Swift where there are instructions and following them reliably does not work. Imagine the disappointment of any enthusiasts, kids, etc., who want to try out the simplest "hello world" program and as a result end up confronting cryptic error messages incl. advice that this behavior is a known compiler bug (is it? for how long?) and so on.

From a software engineering perspective this does not sound like a difficult problem: (1) the compiler behavior is defined formally (I think this description of the main attribute
and this proposal qualify), and (2), the compiler is implemented to comply with the prescribed behavior. But the issue appears to persist.

I understand that there are "workarounds" but they are essentially hacks and can be very discouraging to enthusiasts during their first steps learning and using Swift.

Do we know if someone is working on fixing this?

3 Likes

There is the “Getting started” link on the top of the swift.org page. There, you have the Start tutorial link for Command-line tools.

It might indeed be better to open a new topic if e.g. the documentation is not clear (maybe with a reference to the old topic), because generally not all contributors of the old ticket are still interested in getting updates on it.