a73x

98db6e5d

main: let runScenarios defers run before process exit

a73x   2026-04-19 13:36

Switches runScenarios return type from !void to !u8 and
replaces every std.process.exit call with `return N`. main()
exits with the returned code after control returns.

Fixes a leak of Failure.label strings (via rc.deinit) on the
success path and every mismatch path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git a/src/main.zig b/src/main.zig
index e73dc0e..22028e4 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -180,7 +180,8 @@ pub fn main() !void {
    }

    if (args.len >= 2 and std.mem.eql(u8, args[1], "--scenario")) {
        return runScenarios(alloc, args[1..]);
        const code = try runScenarios(alloc, args[1..]);
        std.process.exit(code);
    }

    return runTerminal(alloc, null);
@@ -205,23 +206,23 @@ fn basenameNoExt(path: []const u8) []const u8 {
///   4 — one or more captures mismatched (unless WAYSTTY_SCENARIO_UPDATE=1)
///   5 — Vulkan-side timeout (mapped from CallbackFailed conservatively)
///   6 — other runtime error
pub fn runScenarios(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void {
pub fn runScenarios(alloc: std.mem.Allocator, argv: []const [:0]const u8) !u8 {
    if (argv.len < 2) {
        std.debug.print("usage: waystty --scenario <path>\n", .{});
        std.process.exit(@intFromEnum(scenario_runtime.ExitCode.parse_error));
        return @intFromEnum(scenario_runtime.ExitCode.parse_error);
    }
    const scenario_path = argv[1];

    const source = std.fs.cwd().readFileAlloc(alloc, scenario_path, 1 * 1024 * 1024) catch |err| {
        std.debug.print("scenario: cannot read {s}: {s}\n", .{ scenario_path, @errorName(err) });
        std.process.exit(@intFromEnum(scenario_runtime.ExitCode.parse_error));
        return @intFromEnum(scenario_runtime.ExitCode.parse_error);
    };
    defer alloc.free(source);

    var diag: scenario.Diagnostic = .{};
    var parsed = scenario.parse(alloc, source, &diag) catch {
        std.debug.print("scenario: {s}:{d}: {s}\n", .{ scenario_path, diag.line, diag.message });
        std.process.exit(@intFromEnum(scenario_runtime.ExitCode.parse_error));
        return @intFromEnum(scenario_runtime.ExitCode.parse_error);
    };
    defer parsed.deinit();

@@ -238,13 +239,13 @@ pub fn runScenarios(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void 

    runTerminal(alloc, &rc) catch |err| {
        std.debug.print("scenario {s}: runtime error: {s}\n", .{ scenario_name, @errorName(err) });
        std.process.exit(@intFromEnum(scenario_runtime.ExitCode.other_error));
        return @intFromEnum(scenario_runtime.ExitCode.other_error);
    };

    if (rc.tick_fatal) |err| {
        const code = scenario_runtime.mapTickError(err);
        std.debug.print("scenario {s}: {s}\n", .{ scenario_name, @errorName(err) });
        std.process.exit(@intFromEnum(code));
        return @intFromEnum(code);
    }

    if (rc.failures.items.len > 0) {
@@ -263,11 +264,11 @@ pub fn runScenarios(alloc: std.mem.Allocator, argv: []const [:0]const u8) !void 
                .{m.label},
            ),
        };
        if (!update) std.process.exit(@intFromEnum(scenario_runtime.ExitCode.assertion_mismatch));
        if (!update) return @intFromEnum(scenario_runtime.ExitCode.assertion_mismatch);
    }

    std.debug.print("scenario {s}: OK\n", .{scenario_name});
    std.process.exit(0);
    return 0;
}

fn runTerminal(alloc: std.mem.Allocator, tick_ctx: ?*scenario_runtime.RunContext) !void {