RFC Mentoring Opportunity: Permit `?` in `main()`

I find this attitude extremely frustrating, because basically all of my work involves either writing programs with meaningful exit statuses, or writing programs that run programs that should have meaningful exit statuses, but sometimes don't, because people like you think it's a forgotten relic of the past and no longer important.

Because of this, it's very important to me that casually-written Rust programs get it right, and that means the language should make the most ergonomic thing be the right thing. ?-in-main is an opportunity to do exactly that, but only if it doesn't panic.

As far as portability, the minimal case - 0 is success, nonzero is failure - is portable to all operating systems that even have exit statuses, and as far as I know the operating systems that don't have exit statuses are embedded things where you aren't supposed to return from main at all.

There're always two exits for main(): returning and unwinding. Returning from main() does not necessarily mean that the program won't panic at a later stage.

Under what circumstances can the program panic after main returns, without this being a straight-up bug in the runtime that needs to be fixed? (Note: drop-handlers for things that go out of scope as main returns do not count as "after main". They are part of the program. And they're drop-handlers, so they need to not fail, period.)

3 Likes

As far as portability, the minimal case - 0 is success, nonzero is failure - is portable to all operating systems that even have exit statuses, and as far as I know the operating systems that don't have exit statuses are embedded things where you aren't supposed to return from main at all.

This is actually not the case. openVMS, though supported by neither rustc or llvm, uses 1 as success. I'd eat my hat if there aren't other operating systems that do this. Its probably better to have consts for success and failure like libc does, then add platform modules of consts if there are conventions beyond success and fail.

1 Like

I didn't say it's not important. What I did say is that the actual mechanism is rather OS-dependent. I'm no expert at all, but i do think that the exit status, if there's one, shall be platform dependent at least in actual numeric value, allowing people to express what they actually mean, sometimes beyond boolean values of 'SUCCEEDED' and 'FAILED'.

Besides that, actually it is the state of art that a rust program returns 0 when returns and non-zero when panics (Now Rust decides what the actual return value is though). All those unwrap()s are making Rust programs go this way. If designing a scheme for "exit status" i think people should take the panics into count instead of leaving them alone just like a segfault.

About the drop-handlers i'd rather think panic safety an important part of it. I don't think people can fully dismiss panics-during-drop, etc, as long as people use it to deal with IO, networking, threading, ..., all kinds of resources. Thinking that they will never fail is far too optimistic.

Would special treatment for Result be inherently bad though? The benefits might outweigh the costs.

Alternatively, something like @crlf0710's suggestion of a From-like trait could hopefully work in a backwards-compatible manner.

1 Like

Really? Does anyone do work in main?

I thought everyone’s main looked like:

fn main() {
    Driver::new(); // <-- real work happens here
}

@nikomatsakis This is turning out to be more complicated/controversial than initially anticipated and I am starting to not like it.

If the objective is to simplify all those examples in the internet, here is a radically different alternative:

A Rust Scripting Mode (look ma’, no main)

We could add a new rule for binary crates (if the main function is not provided, then…) that turns on the “Rust Scripting Mode”, which is stable Rust with some extra features enabled. Some features that we could enable:

  • rust_script_main: if main is not found, the whole file is interpolated into a rust_script_main function and “all rust script mode features are enabled”.
  • operator ?: if using operator ? in the outer-most scope of rust_script_main fails:
    • std::os::process_exit(FAILURE) is called (for anything more complicated, there will always be main).
  • last expression: if the last expression returns an std::os::ExitCode, std::os::process_exit is called with that code, otherwise it is called with SUCCESS (failure and success should be defined for every supported platform anyways).
  • use/extern-crate reordering: inside rust_script_main you can add use/extern crate statements wherever you want, macros must be imported before first usage.
  • // /usr/bin/rustc: A comment in the first line to allow the script to be “executable” by rustc (it would just compile it, and then execute the binary).

One could probably add some other things, but the objective could be to make writing throw-away scripts as pleasant as possible. Those who want control (like more complex error handling in main, can always write their own main function).

An automatic transformation to a proper Rust program with main function could be provided by e.g. rustfmt in case some scripts become “too popular”.

EDIT: By this I do not mean that we should not pursue something like permitting ? in main(), but rather, that if our main objective (haha) is to simplify those examples, there might be other solutions that we might like to explore.

4 Likes

(What does it say that @nikomatsakis floated this idea as a mentoring opportunity, and we’re all just debating the idea itself? I suppose nobody is enthusiastic enough to champion it?)

1 Like

Perhaps. I am mildly torn. I think though that improving the code on a front-page is indeed an objective, but not the only one (perhaps I need to adjust my “opening pitch”). I think of equal or greater importance is improving the code we find in examples in rustdoc and elsewhere. Basically, any time that you have code that someone might cut-and-paste, it should be able to use ? to process errors, rather than writing .unwrap(). These are of course slightly different cases, in that for rustdoc we can possibly change the signature that we generate, or permit users to annotate their examples in some way to describe what kind of wrapper function should be generated.

I personally find the idea of a “scripting mode” less compelling than allowing main() to have more return types, but maybe I need to sit on it.

I’d like a scripting mode more, if it included type inference. So basically, throw ? around all you want, the generated rust_script_main will have the correct signature.

The benefit of this is that we can have examples that one can easily run in a browser that are one liners, but will also run if you copy and paste them into a text file. This is without all the unnecessary writing out the exact type of Result<> your function returns.

Maybe in scripting mode there could be type inference for all functions (although this can be added later) for a prototyping phase.

1 Like

So I went ahead and wrote up an RFC draft based on my understanding of this thread + my opinions about what is necessary. Rendered draft here. It also partially addresses “Allowing main to be divergent in embedded environments”.

Two things may deserve to get split out and filed as just regular issues: reexporting libc::{EXIT_SUCCESS, EXIT_FAILURE} from std::process, and adding std::env::progname() which extracts a short name from env::args().next() for use in error messages.

/attn @nikomatsakis

10 Likes

Nice! Will take a look.

Looks great to me. My only nitpicks are:

  1. std::process feels like a weird place to put the trait when we still want it to be useful on systems where processes aren’t a thing

  2. “catch doesn’t seem to be happening anytime soon.” <-- as of 10 days ago, suddenly it is happening: https://github.com/rust-lang/rust/pull/39921 (not that it’s relevant to the thing you’re actually trying to say there)

Hmm, where would you suggest instead? I just put it there because that's where exit is.

OK, I took some time to read it in more depth – sorry for the delay. I’m also pretty happy with it. =) I think it’s ready to be opened on the RFCs repo, personally.

I’m not sure about the implementation questions about lang_start, I haven’t looked closely at this area of the code. Perhaps @alexcrichton or others might have thoughts here.

Well, I guess one comment:

  • The RFC mentions #[test] functions in two spots, but I’m not honestly quite sure what changes it is proposing there. Is it proposing that #[test] functions can also return things that support “termination”? If so, the integration with the test runner should probably be described, or at least written as an unresolved question.

  • On a similar note, I’d like to figure out how to support rustdoc tests that use ?, but that seems likely to be a separate thing (and maybe we already do? I’m not familiar with the limits here).

Thanks @zackw!

The RFC looks pretty great to me, thanks @zackw! You’re about to make quite a few people happy :slight_smile:

Some thoughts I’d have:

  • If we’re requiring fn main() -> Result<...> to be what you literally write down, do we feel that this adequately solves the “what you write in docs is what you write in normal code” problem? Given today’s rustdoc expansion of tests, we’d have to then do:

    /// /// # fn main() -> Result<(), std::io::Error> { /// let f = File::open("foo")?; /// # } ///

  • For the implementation issues section I think that we’ll basically just want to implement this in the same way that proc macros and the like are implemented today. AFAIK the only stable method to define an entry point is a fn main() so we don’t have to worry that much about all the other entry points, which means we can do:

First update the lang_start lang item with a new signature, such as fn<T: Termination>(main: fn() -> T, argc: isize, argv: *const *const u8) -> isize. To leverage this, the compiler will generate code long before trans/typechecking/etc of the form:

#[start]
fn __rust_injected_start(argc: isize, argv: *const *const u8) -> isize {
    std::rt::lang_start(main, argc, argv)  
}

Then we’ll just naturally pick up #[start] and use that as an entry point in the executable being generated. Using this method we should be able to get nice span information about mis-implemented main functions and such.

1 Like

Great work!

One trap I saw in my first “I can use result to say success or failure from main now” intuition:

fn main() -> Result<(),()> {
    Err(())
} // returns EXIT_SUCCESS

In general, the way Result picks its status_code feels troublesome. Like for bool, main() -> bool feels like it’s success/fail, but main() -> io::Result<bool> wants it to be S_OK/S_FALSE. But having three traits (Termination, TerminationOk, TerminationErr) doesn’t feel great either.

I think the note about ?-generalization is insightful. That RFC takes a stance on which side of Option is success, for example. The questions from RFC might be the thing that decides between the reductionist and essentialist philosophies for that one :thumbsup:

Good catch @scottmcm. Upon closer reading of the Termination impls, I agree that something doesn’t feel quite right in the setup around Result. I think I would have expected perhaps some impls like this:

impl<E: Display> Termination for Result<(), E> {
    fn write_diagnostic(&self, progname: &str, stream: &mut Write) {
        match *self {
            Ok(()) { }
            Err(ref err) { write!(stream, "{}: {}", progname, err); }
        }
    }
    fn exit_status(&self) -> i32 {
        match *self {
            Ok(()) { EXIT_SUCCESS }
            Err(_) { EXIT_FAILURE }
        }
    }
}

impl<O: Display, E: Display> Termination for Result<O, E> {
    // as above, but prints something also for Ok()
}

That seems adequate to me. If you want e.g. a distinct return code, you can create your own variant of Result that does precisely what you want and impl Termination for it. Once RFC 1859 – in whatever form – is implemented, you should also be able to make it so that ? applied to a Result will convert into your funky result type.

1 Like

Well, I disagree with that. The point in that thread is that when you apply the ? operator, the impl takes a stance that None represents an error (or at least an "abrupt exit", but I think in this context it's safe to call that an error). But if you don't use ?, then there is nothing that says Option<Error> is a problem. The Termination trait applies to any value and isn't specific to programs where the user is using ?.

(The only thing is, @zackw, I think the same logic applies to bool. Personally I think we should also remove the impl Termination for bool, which is insufficiently declarative, in preference of people using Result. We could support Result<(), ()> for a "totally silent return" if we wanted that.)

2 Likes