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

How will the exit status work for different -architectures- (replaced with platform)? Do I have to implement it for every -architecture- (platform) ?

@sebk This is not a CPU-dependent thing as far as I am aware.

Are you imagining contexts that do not provide an equivalent of the POSIX _exit / Windows ExitProcess primitives, both of which take an integer exit status? (The only such case that comes to mind is Plan 9 with its string exit status.) I would suggest that such runtimes would supply their own, deliberately incompatible specification of the ToExitStatus trait, in which exit_status returns some other type or doesn’t exist at all, and suitable #[cfg] tags. Programs that use the stock impls in libstd do not need to change. Programs with their own impl will need to reimplement for the altered signature if they care about the exotic runtimes in question.

1 Like

@zackw architecture wasn’t the correct term there. Maybe platform is better.

I mean the mapping of return code to “semantic meaning” (how it is handled) is not uniform between Linux, Windows, etc.

Would every Err value create the same exit status, or would there be different values for, say OutOfMemory, File::read failed and DNS lookup failure?

If they all share the same numerical value, (assuming Ok => 0, Err => 1 for platform X), do we even need ToExitStatus?

@sebk Unixy systems mostly map all “failure” exits to exit code 1, or sometimes 2 (as with grep, where code 1 means “no matches found”). There have been a few attempts to standardize a richer set of meanings (e.g. BSD sysexits.h) but mostly no one bothers with them. I don’t have enough experience with other platforms to talk about them.

If there is a genuinely widely used convention for mapping system errors to exit statuses on Windows (for instance), then Rust ought to conform - but that would be a separate thing from ToExitStatus. The point of ToExitStatus is that the application can define what it means by a Result returned from main, with a couple of stock options. The mapping from io::Error values to platform-specific process exit statuses is properly io::Error's business.

2 Likes

I do get the idea, but I am not sure ToExitStatus is the optimal solution.

type OsResult = Result<(), OsError>;
fn run<R: Into<OsResult>() -> R;

The above would work too, except one would implement Into<OsError> for the different Error types.

I certainly don’t mean to be saying that I think ToExitStatus is optimal, it was just the first thing I thought of.

Maybe we should back up a bit. As I see it, the design goals here are, in decreasing order of importance:

  1. The ? operator should be usable in main.
  2. Existing code with fn main() -> () should keep working, but it’s ok (IMHO) to have to change the signature of main if you want to use ? inside, and maybe the empty return value can be deprecated.
  3. When ? is used in main, its effects should be consistent with platform conventions for process termination. These often include the ability to pass an “exit status” up to some outer environment, conventions for what that status means, and an expectation that a diagnostic message will be written to stderr when a program fails due to a system error.
  4. Since we are messing with main anyway, let’s make it easier to program to those platform conventions; right now it’s a little clumsy.
  5. We should avoid making life more complicated for people who don’t care; on the other hand, if the Easiest Thing is also the Right Thing according to the platform convention, that is better all around. (I do not like how, at present, “all errors are panics” is the very easiest thing, and “the program returns exit code 0 whether or not an error occurred” is the second easiest thing.)

Can we agree on these, before we go back to talking about how to implement?

1 Like

I do agree on 1, 2, 3 and 5. How № 4 can be solved is unclear to me. Also in my opinion a simple main() signature would be great.

The following might cause confusion, as the return type is not clear.

fn main() -> impl Terminate {
    Ok(2+2)
}

Another idea, that this example shows is the ability to return anything that implements Display.

I believe it is important to ensure that user isn’t required to use some new signature for the main. Other than that i believe alternative signature e.g. Result<(), Err> should be provided by user if he wants to return some error.

This error most likely should be able to provide some short error description which would go to stderr and optional exit code (if there is no exit code then we could just return any non-zero) which is used if possible for target. Preferably that any kind of error would be acceptable without much of user intervention to wrap some API errors. I.e. just use accept object with std::error:Error? Maybe introduction of optional ExitCode trait for Error (is it possible to have optional trait?) if user wish to customize return code, but it must be up to compiler to decide how this code is propagated to system.

1 Like

How about some nifty macro like that:

#[result_to_exit]
fn main() {
    let mut f = File::open("foo")?;
    f.write_all("Hello, world!")?;
}

That would take the main body and make it main_inner and handle the return value.

This way it could be implemented as a standalone crate, we could see what community thinks, and if well received, it could be added to std one day, maybe.

I do like this approach. Aside from the syntax, and not yet supporting a derive macro, that is basically what quick_main from error_chain does.

I think the main point of the RFC is - or should be - that simple examples don’t use unwrap, have per default a sensible error handling with the aid of ? and that beginners can use the signature of main as an example how they can write their first own functions.

Therefore I would strongly prefer if the result value of main would be named after Result, that beginners see this type/name quite early and get a first idea about it.

I would love if beginners would just see something like:

fn main() -> Result {
    ...
}

If they later see things like Result<u32> or Result<u32, SomeError>, they might already think: oh, this result returns a u32, and this other one has some special kind of error.

4 Likes

One caveat with this pattern is that you also need an explicit return value at the end. You can't just let it fall off the end of the function with () anymore -- it has to be Ok(()). In this case you could have just let the write_all result be returned, but you have to remember to do this.

IOW, once you opt into returning errors in some form, you also have to be more explicit about the non-error case. Maybe we could implicitly use Into on return values, then From<T> for Result<T,E> would handle this case.

3 Likes

Argh, yeah. I hate that. I always feel a bit goofy writing Ok(()). Sometimes I wish we had adopted @glaebhoerl's full proposal, including the throws keyword.

If catch were implemented, you could do something like

fn main() -> io::Result<()> {
    catch {
        ...
    }
}

but at that point we're getting pretty close to this prior suggestion by @Nemo157, which I had somewhat discounted at the time but was perhaps prescient:

fn main() {
    catch {
        let mut core = Core::new()?;
        ...
    }.unwrap();
}
1 Like

What if returned values of type T got implicitly turned into Ok(T) in functions that return a Result<T,_>? Then you wouldn’t need a catch in main, and you’d also get some nice ergonomic benefits in all the functions that return a Result. No more manually wrapping “success” values in Ok, including those goofy Ok(()). I can’t really think of any downsides.

2 Likes

I’ve thought about it. In practice it would be a kind of coercion. We could probably add it in a backwards compatible way with a bit of effort – though in closures it would be harder, since sometimes we have to infer the return type there. I imagine ultimately the biggest concern is that currentl Result is not otherwise special to the language itself (though obviously it plays a big role in the libraries).

1 Like

Just posing the question:

Would it be bad to automatically coerce into any enum variant on return, if unambiguous – not just into Ok(..) of Result?

Doing it for any enum sounds like it could result in some very odd things. SeekFrom would magically coerce from u64 but not i64, for example. It could be a strange stability trap too, for people adding variants.

FWIW, I don’t think catch is needed for the case above, since there’s no additional return scope. It could just be spelled (stably!) like this:

fn main() -> io::Result<()> {
    Ok({
        let mut f = File::open("foo")?;
        f.write_all("Hello, world!".as_bytes())?;
    })
}

I guess it’s too late to give any method body containing a ? that wrapper automatically. Error returns could be Err(e)?, giving essentially throws without needing the keyword.

I think by allowing coercion or conversion (through From or something else):

  1. from T to Result<T, !>
  2. from E: Error to !(with panicking) then Result<T, !>

either automatically or semi-automatically(with some syntax sugar), then allowing user to specify either () or Result<(), !> as main()'s return value, this issue might be solved?

Besides that I like the catch {}.unwrap() idea from @Nemo157 too, hopefully auto generated with an annotation on main().

Panic isn’t necessarily the right behavior, though. See what I said back up in #57 and #59.

1 Like

@zackw I’d rather think “exit status” is too much OS-dependent so not portable enough.

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. It an “exit status” is actually needed, i think people always need to turn uncatched panic into defined “exit status” code, which can’t be avoided at all. Providing a code as main()'s return value means that an upcoming panic shall either overwrite it or supplement it, both of which is more complex than people would expect and not obvious. Instead, i think people can just setup to make the global handler of uncatched panics to return such a value.