Returning enum(u8) from main?

I’d imagine the most common signature of the main function is

pub fn main() !void

Or, with the new Juicy Main:

pub fn main(init: std.process.Init) !void

For utilities that return a code to the shell/operating system, main would return a u8:

pub fn main() u8

With that in mind, I’m rewriting a program in Zig and it returns one of 3 values: 0 for success, 1 for connection error, and 2 for application error.

For example, this might look like this:

pub const success: u8 = 0;
pub const connection_error: u8 = 1;
pub const application_error: u8 = 2;

pub fn main() u8 {
    return success;
}

Maybe we could tidy that up a bit by namespacing the result codes, like this:

pub const Result = struct {
    pub const success: u8 = 0;
    pub const connection_error: u8 = 1;
    pub const application_error: u8 = 2;
}

pub fn main() u8 {
    return Result.success;
}

What about, instead of returning a u8, we return an enum whose backing integer is a u8 and just cast that down to the u8?

pub const Result = enum(u8) {
    .success = 0,
    .connection_error = 1,
    .application_error = 2,
};

pub fn main() Result {
    return .success;
}

The changes in the standard library are fairly minimal. I tested this and it works fine:

diff --git a/lib/std/start.zig b/lib/std/start.zig
index e39465fe9b..8aebdf251d 100644
--- a/lib/std/start.zig
+++ b/lib/std/start.zig
@@ -731,10 +731,12 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
 
 inline fn wrapMain(result: anytype) u8 {
     const ReturnType = @TypeOf(result);
-    switch (ReturnType) {
-        void => return 0,
-        noreturn => unreachable,
-        u8 => return result,
+    const ReturnTypeInfo = @typeInfo(ReturnType);
+    switch (ReturnTypeInfo) {
+        .void => return 0,
+        .noreturn => unreachable,
+        .int => |x| if (@TypeOf(x) == u8) return result else {},
+        .@"enum" => |x| if (x.tag_type == u8) return @intFromEnum(result) else {},
         else => {},
     }
     if (@typeInfo(ReturnType) != .error_union) @compileError(bad_main_ret);
@@ -748,10 +750,13 @@ inline fn wrapMain(result: anytype) u8 {
         return 1;
     };
 
-    return switch (@TypeOf(unwrapped_result)) {
-        noreturn => unreachable,
-        void => 0,
-        u8 => unwrapped_result,
+    const UnwrappedResultType = @TypeOf(unwrapped_result);
+    const UnwrappedResultTypeInfo = @typeInfo(UnwrappedResultType);
+    return switch (UnwrappedResultTypeInfo) {
+        .noreturn => unreachable,
+        .void => 0,
+        .int => |x| if (@TypeOf(x) == u8) unwrapped_result else @compileError(bad_main_ret),
+        .@"enum" => |x| if (x.tag_type == u8) @intFromEnum(unwrapped_result) else @compileError(bad_main_ret),
         else => @compileError(bad_main_ret),
     };
 }

This is something I’d personally like to have, but I wonder if it’s something that would be worthwhile for others, or if there’s a reason it’s not been done already (I don’t want to contribute unnecessary noise to the issue tracker, so I’m asking here :slight_smile:)?

6 Likes

Were I writing something with this API, I would want to keep to ZIg’s way of error handling except in the final return. So I’d do something like:

pub fn main() u8 {
    innerMain() catch |err| return switch(err) {
        error.Connection => 1,
        error.Application => 2,
    };
    return 0;
}


fn innerMain() !void {
  // ....
}

This removes the desire for having an enum, I think. The mapping from Zig error to error code is simply defined in one place, like it would be with an enum. This could also be in a function if multiple programs need this, eg, fn mapError(result: error{Connection, Application}!void) u8. As a bonus, this would work better with other Zig constructs, eg errdefer.

10 Likes

Alternatively, if you don’t typically want to do cleanup on the errors, and just want to exit quickly,

pub fn main() void {
  check(someFunction());
}

fn someFunction() !void {
    return error.Application;
}

pub fn check(v: anytype) @typeInfo(@TypeOf(v)).error_union.payload {
    const AllowedErrors = error{Connection, Application};
    return v catch |err| std.process.exit(switch(@as(AllowedErrors,err)) {
        error.Connection => 1,
        error.Application => 2,
    });
}

check can be called deep in the stack, wrapping any function which returns an error to eat the error, returning just the value.

To answer the title of the thread: std.process.exit(status: u8) exists.