600 lines · 20.3 KB · Zig
RawDownload
const std = @import("std");const debug_trace = @import("../core/shared/debug_trace.zig");const record_tape = @import("../core/workspace/record_tape.zig");const shell_runtime = @import("shell_runtime.zig"); pub const EventLoopCallbacks = struct {    ctx: *anyopaque,    collect_facts: *const fn (*anyopaque) anyerror!void,    next_collected_byte: *const fn (*anyopaque) ?u8,    handle_byte: *const fn (*anyopaque, u8) anyerror!void,    settle_delivery_epoch: *const fn (*anyopaque) anyerror!void,    commit_frame: *const fn (*anyopaque) anyerror!void,    poll_timeout_ms: ?*const fn (*anyopaque, i32) i32 = null,}; const max_input_reads_per_fact_collection = 32; fn attemptFinalFrameCommit(callbacks: EventLoopCallbacks, reason: []const u8) void {    callbacks.commit_frame(callbacks.ctx) catch |err| debug_trace.logf(        "event_loop",        "final_frame_commit_failed reason={s} err={s}",        .{ reason, @errorName(err) },    );} pub const ExitCause = enum {    requested_exit,    input_closed,}; pub fn pump_ready_input(terminal: anytype, should_exit: *bool, callbacks: EventLoopCallbacks) !?ExitCause {    var buf: [128]u8 = undefined;    var handled_input = false;     while (callbacks.next_collected_byte(callbacks.ctx)) |byte| {        handled_input = true;        try callbacks.handle_byte(callbacks.ctx, byte);        if (should_exit.*) return .requested_exit;    }     var poll_result = try terminal.pollInput(0);    var input_reads: usize = 0;     while (poll_result.readable and input_reads < max_input_reads_per_fact_collection) : (input_reads += 1) {        const read_len = try terminal.read(&buf);        if (read_len == 0) {            if (input_reads > 0) attemptFinalFrameCommit(callbacks, "eof");            return .input_closed;        }         handled_input = true;        record_tape.recordStdin(buf[0..read_len]);        for (buf[0..read_len]) |byte| {            try callbacks.handle_byte(callbacks.ctx, byte);            if (should_exit.*) return .requested_exit;        }        poll_result = try terminal.pollInput(0);    }     if (poll_result.readable) {        try callbacks.commit_frame(callbacks.ctx);        return null;    }    if (poll_result.closed()) {        if (input_reads > 0) attemptFinalFrameCommit(callbacks, "hangup");        return .input_closed;    }     if (handled_input) {        try callbacks.settle_delivery_epoch(callbacks.ctx);        try callbacks.commit_frame(callbacks.ctx);    }    return null;} pub fn run(terminal: anytype, should_exit: *bool, poll_timeout_ms: i32, callbacks: EventLoopCallbacks) !ExitCause {    var buf: [128]u8 = undefined;     while (!should_exit.*) {        try callbacks.collect_facts(callbacks.ctx);        if (should_exit.*) return .requested_exit;         while (callbacks.next_collected_byte(callbacks.ctx)) |byte| {            try callbacks.handle_byte(callbacks.ctx, byte);            if (should_exit.*) return .requested_exit;        }         const timeout_ms = if (callbacks.poll_timeout_ms) |resolve|            resolve(callbacks.ctx, poll_timeout_ms)        else            poll_timeout_ms;        var poll_result = try terminal.pollInput(timeout_ms);        var input_reads: usize = 0;        while (poll_result.readable and input_reads < max_input_reads_per_fact_collection) : (input_reads += 1) {            const read_len = try terminal.read(&buf);            if (read_len == 0) {                if (input_reads > 0) attemptFinalFrameCommit(callbacks, "eof");                return .input_closed;            }             record_tape.recordStdin(buf[0..read_len]);            for (buf[0..read_len]) |byte| {                try callbacks.handle_byte(callbacks.ctx, byte);                if (should_exit.*) return .requested_exit;            }            poll_result = try terminal.pollInput(0);        }         if (poll_result.readable) {            try callbacks.commit_frame(callbacks.ctx);            continue;        }        if (poll_result.closed()) {            if (input_reads > 0) attemptFinalFrameCommit(callbacks, "hangup");            return .input_closed;        }         try callbacks.settle_delivery_epoch(callbacks.ctx);        try callbacks.commit_frame(callbacks.ctx);    }    return .requested_exit;} test "event loop exposes a post-input commit callback" {    try std.testing.expect(@hasField(EventLoopCallbacks, "commit_frame"));    try std.testing.expect(@hasField(EventLoopCallbacks, "settle_delivery_epoch"));} const EventLoopTestTrace = struct {    bytes: [8]u8 = undefined,    len: usize = 0,    collected: [2]u8 = undefined,    collected_len: usize = 0,    collected_index: usize = 0,    should_exit: *bool,     fn append(self: *EventLoopTestTrace, byte: u8) void {        self.bytes[self.len] = byte;        self.len += 1;    }}; const EventLoopTestTerminal = struct {    polls: *usize,    burst_reads: ?*usize = null,    observed_timeout_ms: ?*i32 = null,     fn pollInput(self: EventLoopTestTerminal, timeout_ms: i32) !shell_runtime.PollResult {        self.polls.* += 1;        if (self.observed_timeout_ms) |observed| {            if (self.polls.* == 1) observed.* = timeout_ms;        }        if (self.burst_reads) |reads| {            if (reads.* < 2) return .{ .readable = true };            return if (timeout_ms == 0) .{} else .{ .hung_up = true };        }        if (self.polls.* > 1) return .{ .hung_up = true };        return .{ .readable = true };    }     fn read(self: EventLoopTestTerminal, buf: []u8) !usize {        const bytes = if (self.burst_reads) |reads| blk: {            const chunk = if (reads.* == 0) "ab" else "de";            reads.* += 1;            break :blk chunk;        } else "ab";        @memcpy(buf[0..bytes.len], bytes);        return bytes.len;    }}; fn collectEventLoopTestFacts(ctx: *anyopaque) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append('t');} fn noCollectedEventLoopByte(_: *anyopaque) ?u8 {    return null;} fn nextCollectedEventLoopByte(ctx: *anyopaque) ?u8 {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    if (trace.collected_index >= trace.collected_len) return null;    const byte = trace.collected[trace.collected_index];    trace.collected_index += 1;    return byte;} fn handleEventLoopTestByte(ctx: *anyopaque, byte: u8) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append(byte);} fn commitEventLoopTestFrame(ctx: *anyopaque) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append('c');    trace.should_exit.* = true;} fn commitEventLoopTestFrameWithoutExit(ctx: *anyopaque) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append('c');} fn settleEventLoopTestDeliveryEpoch(ctx: *anyopaque) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append('s');} fn collectAndExitEventLoopTest(ctx: *anyopaque) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append('t');    trace.should_exit.* = true;} fn handleAndExitEventLoopTestByte(ctx: *anyopaque, byte: u8) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append(byte);    trace.should_exit.* = true;} fn rejectUnexpectedEventLoopByte(_: *anyopaque, _: u8) !void {    return error.UnexpectedByte;} fn rejectUnexpectedEventLoopCommit(_: *anyopaque) !void {    return error.UnexpectedCommit;} fn failEventLoopTestCommit(ctx: *anyopaque) !void {    const trace: *EventLoopTestTrace = @ptrCast(@alignCast(ctx));    trace.append('c');    return error.FinalCommitFailed;} fn useLongerPollTimeout(_: *anyopaque, default_timeout_ms: i32) i32 {    return default_timeout_ms * 2;} test "event loop commits once after every byte from the current read" {    var should_exit = false;    var polls: usize = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = commitEventLoopTestFrame,    });     try std.testing.expectEqual(ExitCause.input_closed, exit_cause);    try std.testing.expectEqualStrings("tabc", trace.bytes[0..trace.len]);} test "event loop batches already readable input before committing a frame" {    var should_exit = false;    var polls: usize = 0;    var reads: usize = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls, .burst_reads = &reads }, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = commitEventLoopTestFrame,    });     try std.testing.expectEqual(ExitCause.requested_exit, exit_cause);    try std.testing.expectEqualStrings("tabdesc", trace.bytes[0..trace.len]);} test "cooperative input pump renders ready input without collecting facts" {    var should_exit = false;    var polls: usize = 0;    var reads: usize = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try pump_ready_input(EventLoopTestTerminal{        .polls = &polls,        .burst_reads = &reads,    }, &should_exit, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = commitEventLoopTestFrameWithoutExit,    });     try std.testing.expectEqual(@as(?ExitCause, null), exit_cause);    try std.testing.expect(!should_exit);    try std.testing.expectEqualStrings("abdesc", trace.bytes[0..trace.len]);} test "cooperative input pump is idle when no input is ready" {    const Terminal = struct {        fn pollInput(_: @This(), timeout_ms: i32) !shell_runtime.PollResult {            try std.testing.expectEqual(@as(i32, 0), timeout_ms);            return .{};        }         fn read(_: @This(), _: []u8) !usize {            return error.UnexpectedRead;        }    };     var should_exit = false;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };    const exit_cause = try pump_ready_input(Terminal{}, &should_exit, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = rejectUnexpectedEventLoopByte,        .settle_delivery_epoch = rejectUnexpectedEventLoopCommit,        .commit_frame = rejectUnexpectedEventLoopCommit,    });     try std.testing.expectEqual(@as(?ExitCause, null), exit_cause);    try std.testing.expect(!should_exit);    try std.testing.expectEqual(@as(usize, 0), trace.len);} test "event loop settles a facts-only delivery epoch before committing" {    const Terminal = struct {        fn pollInput(_: @This(), _: i32) !shell_runtime.PollResult {            return .{};        }         fn read(_: @This(), _: []u8) !usize {            return error.UnexpectedRead;        }    };     var should_exit = false;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };    const exit_cause = try run(Terminal{}, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = commitEventLoopTestFrame,    });     try std.testing.expectEqual(ExitCause.requested_exit, exit_cause);    try std.testing.expectEqualStrings("tsc", trace.bytes[0..trace.len]);} test "event loop resolves a dynamic poll timeout" {    var should_exit = false;    var polls: usize = 0;    var observed_timeout_ms: i32 = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try run(EventLoopTestTerminal{        .polls = &polls,        .observed_timeout_ms = &observed_timeout_ms,    }, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = commitEventLoopTestFrame,        .poll_timeout_ms = useLongerPollTimeout,    });     try std.testing.expectEqual(ExitCause.input_closed, exit_cause);    try std.testing.expectEqual(@as(i32, 16), observed_timeout_ms);} test "event loop dispatches probe-collected input before polled input" {    var should_exit = false;    var polls: usize = 0;    var trace = EventLoopTestTrace{        .collected = .{ 'x', 0 },        .collected_len = 1,        .should_exit = &should_exit,    };     const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = nextCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = commitEventLoopTestFrame,    });     try std.testing.expectEqual(ExitCause.input_closed, exit_cause);    try std.testing.expectEqualStrings("txabc", trace.bytes[0..trace.len]);} test "event loop stops after fact collection requests exit" {    var should_exit = false;    var polls: usize = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectAndExitEventLoopTest,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = rejectUnexpectedEventLoopByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = rejectUnexpectedEventLoopCommit,    });     try std.testing.expectEqual(ExitCause.requested_exit, exit_cause);    try std.testing.expectEqual(@as(usize, 0), polls);    try std.testing.expectEqualStrings("t", trace.bytes[0..trace.len]);} test "event loop skips frame commit after input requests exit" {    var should_exit = false;    var polls: usize = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try run(        EventLoopTestTerminal{ .polls = &polls },        &should_exit,        8,        .{            .ctx = &trace,            .collect_facts = collectEventLoopTestFacts,            .next_collected_byte = noCollectedEventLoopByte,            .handle_byte = handleAndExitEventLoopTestByte,            .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,            .commit_frame = rejectUnexpectedEventLoopCommit,        },    );     try std.testing.expectEqual(ExitCause.requested_exit, exit_cause);    try std.testing.expectEqualStrings("ta", trace.bytes[0..trace.len]);} const EventLoopClosedTerminal = struct {    poll_result: shell_runtime.PollResult,     fn pollInput(self: EventLoopClosedTerminal, _: i32) !shell_runtime.PollResult {        return self.poll_result;    }     fn read(_: EventLoopClosedTerminal, _: []u8) !usize {        return 0;    }}; test "event loop distinguishes requested exit from input closure" {    var should_exit = false;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };    const callbacks = EventLoopCallbacks{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = rejectUnexpectedEventLoopByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = rejectUnexpectedEventLoopCommit,    };     try std.testing.expectEqual(        ExitCause.input_closed,        try run(            EventLoopClosedTerminal{ .poll_result = .{ .readable = true } },            &should_exit,            8,            callbacks,        ),    );    try std.testing.expect(!should_exit);     try std.testing.expectEqual(        ExitCause.input_closed,        try run(            EventLoopClosedTerminal{ .poll_result = .{ .hung_up = true } },            &should_exit,            8,            callbacks,        ),    );    try std.testing.expect(!should_exit);} test "event loop treats a failed final commit after hangup as a clean close" {    var should_exit = false;    var polls: usize = 0;    var trace = EventLoopTestTrace{ .should_exit = &should_exit };     const exit_cause = try run(EventLoopTestTerminal{ .polls = &polls }, &should_exit, 8, .{        .ctx = &trace,        .collect_facts = collectEventLoopTestFacts,        .next_collected_byte = noCollectedEventLoopByte,        .handle_byte = handleEventLoopTestByte,        .settle_delivery_epoch = settleEventLoopTestDeliveryEpoch,        .commit_frame = failEventLoopTestCommit,    });

Lines 1–500 of 600.