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


#88

(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?)


#89

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.


#90

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.


#91

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


#92

Nice! Will take a look.


#93

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)


#94

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


#95

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.


#96

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).


#97

Thanks @zackw!


#98

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.


#99

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:


#100

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.


#101

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.)


#102

@nikomatsakis Can we just remove the main function? The entry point of Swift code execution is simply the main.swift file…

I’m not sure how implicitly wrapping the main.rs file in a main function would work with Rust’s module system, but it’s something to consider.


#103

While looking through the RFC that @zackw posted, I kind of feel like it’s side-stepping a huge issue, which is what the regular “non-failure” return value needs to be. Look at this example from the RFC:

fn main() -> Result<(), io::Error> {
    let mut stdin = io::stdin();
    let mut raw_stdout = io::stdout();
    let mut stdout = raw_stdout.lock();
    for line in stdin.lock().lines() {
        stdout.write(line?.trim().as_bytes())?;
        stdout.write(b"\n")?;
    }
    stdout.flush()
}

This looks great, but it only looks great because the last operation happens to be the stdout.flush(). Suppose that we wanted to do something else after:

fn main() -> Result<(), io::Error> {
    let mut stdin = io::stdin();
    let mut raw_stdout = io::stdout();
    let mut stdout = raw_stdout.lock();
    for line in stdin.lock().lines() {
        stdout.write(line?.trim().as_bytes())?;
        stdout.write(b"\n")?;
    }
    stdout.flush()?;
    some_method_call();
    Ok(())
}

Here we need to add that kind of ugly Ok(()) at the end of main. It’s not particularly intuitive why this needs to be there for someone new to Rust, and it’s also really subtle that it becomes unnecessary in the first example due to the omitted semicolon. Ideally this would “just work” without having to return anything special from main.

It might be worth considering if something like the original “checked exception” proposal isn’t more sensible. I think it was @glaebhoerl that first proposed this somewhere. Of course this is nothing like checked exceptions in Java, instead it’s just a little bit more syntax sugar on top of what we already have. The example above would become:

fn main() throws io::Error {
    let mut stdin = io::stdin();
    let mut raw_stdout = io::stdout();
    let mut stdout = raw_stdout.lock();
    for line in stdin.lock().lines() {
        stdout.write(line?.trim().as_bytes())?;
        stdout.write(b"\n")?;
    }
    stdout.flush()?;
    some_method_call();
}

With the exact same semantics. That is the return value of main is still Result<(), io::Error>, just like before. However, the value that the function body evaluates to does not need to be (and perhaps cannot be) wrapped in a Result.

Together with some Trait and adding std::error::Error to the prelude this could become:

fn main() throws some Error {
    let mut stdin = io::stdin();
    let mut raw_stdout = io::stdout();
    let mut stdout = raw_stdout.lock();
    for line in stdin.lock().lines() {
        stdout.write(line?.trim().as_bytes())?;
        stdout.write(b"\n")?;
    }
    stdout.flush()?;
    some_method_call();
}

That looks pretty nice and self-explanatory to me.

Perhaps this could be considered a completely orthogonal issue, but I thought I’d bring it up here anyway.


#104

Yes, I was wondering that, although I don’t think it’s really an alternative per se. That is, I think we still need the machinery that this RFC describes – some way for main to offer more return values. Presumably this throws Foo stuff would be sugar that can be applied to any function, then.


#105

imo the Ok(()) issue is orthogonal to letting main() return non-() values, since that one currently affects all functions, while the ? issue is unique to main(). I also think “making ? work in main” is a huge win even without a solution for Ok(()).

But I do think allowing elision of Ok(()) the same way we allow eliding () today would also be an improvement. We should start a separate thread for that.


#106

Thanks for all the feedback up to this point. I have made revisions, and opened an RFC pull request. A few specific responses:

  • Several people wanted to know more about what needed to be done to support ? in #[test] and doctests, so I’ve added a bunch more detail about that.

  • @scottmcm I was going back and forth on whether Err(()) should cause an unsuccessful exit. The Result<(),()> example has persuaded me it should.

  • @nikomatsakis Despite the above, I currently think we should not impl Termination for everything that is Display. This is a gut feeling and I don’t have any specific argument for it. (I have removed the impl for bool as you suggested, and instead written a whole new section about how this feature can stay out of the way of people trying to conform to specific Unixy return value conventions, without having to put all those conventions into the stdlib.)

  • @jonysy “Just remove the main function” (and what, allow writing actual code at module scope?) would be an entirely different proposal with an entirely different set of issues to work through.

  • @jnicklas In my own code I have a whole lot of functions—not just main—with Ok(()) as their last line. I agree this is ugly, but I think addressing it is out of scope for this proposal. This is what I was trying to get at with the last line of the RFC:

    The ergonomics of ? in general would be improved by autowrapping all off-the-end return values in Ok if they’re not already Results, but that’s another proposal.

  • @alexcrichton I’m afraid I don’t yet understand the guts of the runtime well enough to know what you meant about “updating the lang_start lang item.” I tried to clarify the bit where I talked about compiler changes. I may be hung up on things that are not possible in C(++).


#107

Ughhhhgh if this works in docs and tests you will be my hero !