//! The user-space process entry shim. Every user binary roots `_start` here (via
//! `entry = _start` in build.zig); the shared compilation root, root.zig, forces
//! this file to be analysed with `comptime { _ = &runtime.start._start; }`, so
//! the whole runtime is linked in.

const std = @import("std");
const system = @import("system.zig");
const process = @import("process.zig");

/// The kernel enters at `_start` with rsp 16-aligned, pointing at the System V
/// process-entry block it built: argc, argv pointers, NULL, envp terminator, the
/// auxiliary vector, then the strings (see system/kernel/process.zig,
/// `buildEntryStack`). Capture that address in rdi — the first SysV argument —
/// before `call` disturbs the stack; the call's pushed return address also puts
/// rsp ≡ 8 (mod 16), satisfying the ABI before any Zig frame runs. The `ud2` is a
/// safety net if `rt_start` ever returns.
pub export fn _start() callconv(.naked) noreturn {
    asm volatile (
        \\mov %%rsp, %%rdi
        \\call rt_start
        \\ud2
    );
}

/// The first Zig frame, entered with `stack` pointing at the kernel-built entry
/// block. Build the `process.Init` from it and dispatch to the program's `main`,
/// whose signature is inspected at comptime. The heap is lazy (first alloc grows
/// it), so there is no other runtime init to order here.
export fn rt_start(stack: [*]const u64) callconv(.c) noreturn {
    const init: process.Init = .{ .arguments = .{
        .count = stack[0],
        .vector = @ptrCast(stack + 1),
    } };
    system.exit(callMain(init));
}

/// Comptime-dispatch on root.main's signature, in the spirit of std's start.zig:
/// zero parameters or one `process.Init`; returns void, noreturn, u8, !void, or !u8.
fn callMain(init: process.Init) u8 {
    const root = @import("root"); // root.zig, re-exporting the program's main
    const main_information = @typeInfo(@TypeOf(root.main)).@"fn";

    const call_arguments = switch (main_information.params.len) {
        0 => .{},
        1 => arguments: {
            const Parameter = main_information.params[0].type orelse
                @compileError("main's parameter must be runtime.process.Init (not anytype)");
            if (Parameter != process.Init)
                @compileError("main's parameter must be runtime.process.Init, found " ++ @typeName(Parameter));
            break :arguments .{init};
        },
        else => @compileError("main takes no parameters or a single runtime.process.Init"),
    };

    const ReturnType = main_information.return_type.?;
    switch (@typeInfo(ReturnType)) {
        .noreturn => @call(.auto, root.main, call_arguments),
        .void => {
            @call(.auto, root.main, call_arguments);
            return 0;
        },
        .int => {
            if (ReturnType != u8)
                @compileError("main's integer return type must be u8, found " ++ @typeName(ReturnType));
            return @call(.auto, root.main, call_arguments);
        },
        .error_union => {
            const payload = @call(.auto, root.main, call_arguments) catch |err| {
                var buffer: [128]u8 = undefined;
                const line = std.fmt.bufPrint(&buffer, "main returned error: {s}\n", .{@errorName(err)}) catch "main returned an error\n";
                _ = system.write(line);
                return 1; // distinct from panic's 127
            };
            if (@TypeOf(payload) == void) return 0;
            if (@TypeOf(payload) == u8) return payload;
            @compileError("main's error-union payload must be void or u8, found " ++ @typeName(@TypeOf(payload)));
        },
        else => @compileError("main must return void, noreturn, u8, !void, or !u8, found " ++ @typeName(ReturnType)),
    }
}

/// No runtime to unwind into — report a panic as a nonzero exit code.
pub const panic = std.debug.FullPanic(struct {
    fn panic(_: []const u8, _: ?usize) noreturn {
        system.exit(127);
    }
}.panic);
