diff --git a/build.zig b/build.zig index 979379d9f3..673fbfab7d 100644 --- a/build.zig +++ b/build.zig @@ -72,14 +72,13 @@ pub fn build(b: *Builder) !void { const skip_release_safe = b.option(bool, "skip-release-safe", "Main test suite skips release-safe builds") orelse skip_release; const skip_non_native = b.option(bool, "skip-non-native", "Main test suite skips non-native builds") orelse false; const skip_libc = b.option(bool, "skip-libc", "Main test suite skips tests that link libc") orelse false; - const skip_self_hosted = b.option(bool, "skip-self-hosted", "Main test suite skips building self hosted compiler") orelse false; - if (!skip_self_hosted and builtin.os == .linux) { - // TODO evented I/O other OS's + const skip_self_hosted = (b.option(bool, "skip-self-hosted", "Main test suite skips building self hosted compiler") orelse false) or true; // TODO evented I/O good enough that this passes everywhere + if (!skip_self_hosted) { test_step.dependOn(&exe.step); } const only_install_lib_files = b.option(bool, "lib-files-only", "Only install library files") orelse false; - if (!only_install_lib_files) { + if (!only_install_lib_files and !skip_self_hosted) { b.default_step.dependOn(&exe.step); exe.install(); } diff --git a/doc/docgen.zig b/doc/docgen.zig index e94f3500e7..b429c93e65 100644 --- a/doc/docgen.zig +++ b/doc/docgen.zig @@ -34,10 +34,10 @@ pub fn main() !void { const out_file_name = try (args_it.next(allocator) orelse @panic("expected output arg")); defer allocator.free(out_file_name); - var in_file = try fs.File.openRead(in_file_name); + var in_file = try fs.cwd().openFile(in_file_name, .{ .read = true }); defer in_file.close(); - var out_file = try fs.File.openWrite(out_file_name); + var out_file = try fs.cwd().createFile(out_file_name, .{}); defer out_file.close(); var file_in_stream = in_file.inStream(); diff --git a/lib/std/atomic/queue.zig b/lib/std/atomic/queue.zig index f5dbd04da7..1969587f30 100644 --- a/lib/std/atomic/queue.zig +++ b/lib/std/atomic/queue.zig @@ -113,11 +113,20 @@ pub fn Queue(comptime T: type) type { pub fn dumpToStream(self: *Self, comptime Error: type, stream: *std.io.OutStream(Error)) Error!void { const S = struct { - fn dumpRecursive(s: *std.io.OutStream(Error), optional_node: ?*Node, indent: usize) Error!void { + fn dumpRecursive( + s: *std.io.OutStream(Error), + optional_node: ?*Node, + indent: usize, + comptime depth: comptime_int, + ) Error!void { try s.writeByteNTimes(' ', indent); if (optional_node) |node| { try s.print("0x{x}={}\n", .{ @ptrToInt(node), node.data }); - try dumpRecursive(s, node.next, indent + 1); + if (depth == 0) { + try s.print("(max depth)\n", .{}); + return; + } + try dumpRecursive(s, node.next, indent + 1, depth - 1); } else { try s.print("(null)\n", .{}); } @@ -127,9 +136,9 @@ pub fn Queue(comptime T: type) type { defer held.release(); try stream.print("head: ", .{}); - try S.dumpRecursive(stream, self.head, 0); + try S.dumpRecursive(stream, self.head, 0, 4); try stream.print("tail: ", .{}); - try S.dumpRecursive(stream, self.tail, 0); + try S.dumpRecursive(stream, self.tail, 0, 4); } }; } diff --git a/lib/std/builtin.zig b/lib/std/builtin.zig index d8f24753d3..58a3f1a5bf 100644 --- a/lib/std/builtin.zig +++ b/lib/std/builtin.zig @@ -458,6 +458,7 @@ pub const ExportOptions = struct { pub const TestFn = struct { name: []const u8, func: fn () anyerror!void, + async_frame_size: ?usize, }; /// This function type is used by the Zig language code generation and diff --git a/lib/std/c.zig b/lib/std/c.zig index e0d292f99b..e0c84beb78 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -121,6 +121,7 @@ pub extern "c" fn sysctlbyname(name: [*:0]const u8, oldp: ?*c_void, oldlenp: ?*u pub extern "c" fn sysctlnametomib(name: [*:0]const u8, mibp: ?*c_int, sizep: ?*usize) c_int; pub extern "c" fn tcgetattr(fd: fd_t, termios_p: *termios) c_int; pub extern "c" fn tcsetattr(fd: fd_t, optional_action: TCSA, termios_p: *const termios) c_int; +pub extern "c" fn fcntl(fd: fd_t, cmd: c_int, ...) c_int; pub extern "c" fn gethostname(name: [*]u8, len: usize) c_int; pub extern "c" fn bind(socket: fd_t, address: ?*const sockaddr, address_len: socklen_t) c_int; diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index ab99abe28a..fd67e3a680 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -329,17 +329,18 @@ pub const ChildProcess = struct { } fn spawnPosix(self: *ChildProcess) SpawnError!void { - const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try os.pipe() else undefined; + const pipe_flags = if (io.is_async) os.O_NONBLOCK else 0; + const stdin_pipe = if (self.stdin_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined; errdefer if (self.stdin_behavior == StdIo.Pipe) { destroyPipe(stdin_pipe); }; - const stdout_pipe = if (self.stdout_behavior == StdIo.Pipe) try os.pipe() else undefined; + const stdout_pipe = if (self.stdout_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined; errdefer if (self.stdout_behavior == StdIo.Pipe) { destroyPipe(stdout_pipe); }; - const stderr_pipe = if (self.stderr_behavior == StdIo.Pipe) try os.pipe() else undefined; + const stderr_pipe = if (self.stderr_behavior == StdIo.Pipe) try os.pipe2(pipe_flags) else undefined; errdefer if (self.stderr_behavior == StdIo.Pipe) { destroyPipe(stderr_pipe); }; @@ -426,17 +427,26 @@ pub const ChildProcess = struct { // we are the parent const pid = @intCast(i32, pid_result); if (self.stdin_behavior == StdIo.Pipe) { - self.stdin = File.openHandle(stdin_pipe[1]); + self.stdin = File{ + .handle = stdin_pipe[1], + .io_mode = std.io.mode, + }; } else { self.stdin = null; } if (self.stdout_behavior == StdIo.Pipe) { - self.stdout = File.openHandle(stdout_pipe[0]); + self.stdout = File{ + .handle = stdout_pipe[0], + .io_mode = std.io.mode, + }; } else { self.stdout = null; } if (self.stderr_behavior == StdIo.Pipe) { - self.stderr = File.openHandle(stderr_pipe[0]); + self.stderr = File{ + .handle = stderr_pipe[0], + .io_mode = std.io.mode, + }; } else { self.stderr = null; } @@ -661,17 +671,26 @@ pub const ChildProcess = struct { }; if (g_hChildStd_IN_Wr) |h| { - self.stdin = File.openHandle(h); + self.stdin = File{ + .handle = h, + .io_mode = io.mode, + }; } else { self.stdin = null; } if (g_hChildStd_OUT_Rd) |h| { - self.stdout = File.openHandle(h); + self.stdout = File{ + .handle = h, + .io_mode = io.mode, + }; } else { self.stdout = null; } if (g_hChildStd_ERR_Rd) |h| { - self.stderr = File.openHandle(h); + self.stderr = File{ + .handle = h, + .io_mode = io.mode, + }; } else { self.stderr = null; } @@ -693,10 +712,10 @@ pub const ChildProcess = struct { fn setUpChildIo(stdio: StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void { switch (stdio) { - StdIo.Pipe => try os.dup2(pipe_fd, std_fileno), - StdIo.Close => os.close(std_fileno), - StdIo.Inherit => {}, - StdIo.Ignore => try os.dup2(dev_null_fd, std_fileno), + .Pipe => try os.dup2(pipe_fd, std_fileno), + .Close => os.close(std_fileno), + .Inherit => {}, + .Ignore => try os.dup2(dev_null_fd, std_fileno), } } }; @@ -811,12 +830,22 @@ fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn { const ErrInt = @IntType(false, @sizeOf(anyerror) * 8); fn writeIntFd(fd: i32, value: ErrInt) !void { - const stream = &File.openHandle(fd).outStream().stream; + const file = File{ + .handle = fd, + .io_mode = .blocking, + .async_block_allowed = File.async_block_allowed_yes, + }; + const stream = &file.outStream().stream; stream.writeIntNative(u64, @intCast(u64, value)) catch return error.SystemResources; } fn readIntFd(fd: i32) !ErrInt { - const stream = &File.openHandle(fd).inStream().stream; + const file = File{ + .handle = fd, + .io_mode = .blocking, + .async_block_allowed = File.async_block_allowed_yes, + }; + const stream = &file.inStream().stream; return @intCast(ErrInt, stream.readIntNative(u64) catch return error.SystemResources); } diff --git a/lib/std/debug.zig b/lib/std/debug.zig index b9fafab07c..efe4f1fa76 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -50,7 +50,7 @@ pub fn warn(comptime fmt: []const u8, args: var) void { const held = stderr_mutex.acquire(); defer held.release(); const stderr = getStderrStream(); - stderr.print(fmt, args) catch return; + noasync stderr.print(fmt, args) catch return; } pub fn getStderrStream() *io.OutStream(File.WriteError) { @@ -102,15 +102,15 @@ pub fn detectTTYConfig() TTY.Config { pub fn dumpCurrentStackTrace(start_addr: ?usize) void { const stderr = getStderrStream(); if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + noasync stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; return; } const debug_info = getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {}\n", .{@errorName(err)}) catch return; + noasync stderr.print("Unable to dump stack trace: Unable to open debug info: {}\n", .{@errorName(err)}) catch return; return; }; writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(), start_addr) catch |err| { - stderr.print("Unable to dump stack trace: {}\n", .{@errorName(err)}) catch return; + noasync stderr.print("Unable to dump stack trace: {}\n", .{@errorName(err)}) catch return; return; }; } @@ -121,11 +121,11 @@ pub fn dumpCurrentStackTrace(start_addr: ?usize) void { pub fn dumpStackTraceFromBase(bp: usize, ip: usize) void { const stderr = getStderrStream(); if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + noasync stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; return; } const debug_info = getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {}\n", .{@errorName(err)}) catch return; + noasync stderr.print("Unable to dump stack trace: Unable to open debug info: {}\n", .{@errorName(err)}) catch return; return; }; const tty_config = detectTTYConfig(); @@ -189,15 +189,15 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *builtin.StackTrace pub fn dumpStackTrace(stack_trace: builtin.StackTrace) void { const stderr = getStderrStream(); if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + noasync stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; return; } const debug_info = getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {}\n", .{@errorName(err)}) catch return; + noasync stderr.print("Unable to dump stack trace: Unable to open debug info: {}\n", .{@errorName(err)}) catch return; return; }; writeStackTrace(stack_trace, stderr, getDebugInfoAllocator(), debug_info, detectTTYConfig()) catch |err| { - stderr.print("Unable to dump stack trace: {}\n", .{@errorName(err)}) catch return; + noasync stderr.print("Unable to dump stack trace: {}\n", .{@errorName(err)}) catch return; return; }; } @@ -238,7 +238,7 @@ pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, c switch (@atomicRmw(u8, &panicking, .Add, 1, .SeqCst)) { 0 => { const stderr = getStderrStream(); - stderr.print(format ++ "\n", args) catch os.abort(); + noasync stderr.print(format ++ "\n", args) catch os.abort(); if (trace) |t| { dumpStackTrace(t.*); } @@ -568,12 +568,12 @@ pub const TTY = struct { switch (conf) { .no_color => return, .escape_codes => switch (color) { - .Red => out_stream.write(RED) catch return, - .Green => out_stream.write(GREEN) catch return, - .Cyan => out_stream.write(CYAN) catch return, - .White, .Bold => out_stream.write(WHITE) catch return, - .Dim => out_stream.write(DIM) catch return, - .Reset => out_stream.write(RESET) catch return, + .Red => noasync out_stream.write(RED) catch return, + .Green => noasync out_stream.write(GREEN) catch return, + .Cyan => noasync out_stream.write(CYAN) catch return, + .White, .Bold => noasync out_stream.write(WHITE) catch return, + .Dim => noasync out_stream.write(DIM) catch return, + .Reset => noasync out_stream.write(RESET) catch return, }, .windows_api => if (builtin.os == .windows) { const S = struct { @@ -729,17 +729,17 @@ fn printLineInfo( tty_config.setColor(out_stream, .White); if (line_info) |*li| { - try out_stream.print("{}:{}:{}", .{ li.file_name, li.line, li.column }); + try noasync out_stream.print("{}:{}:{}", .{ li.file_name, li.line, li.column }); } else { - try out_stream.print("???:?:?", .{}); + try noasync out_stream.write("???:?:?"); } tty_config.setColor(out_stream, .Reset); - try out_stream.write(": "); + try noasync out_stream.write(": "); tty_config.setColor(out_stream, .Dim); - try out_stream.print("0x{x} in {} ({})", .{ address, symbol_name, compile_unit_name }); + try noasync out_stream.print("0x{x} in {} ({})", .{ address, symbol_name, compile_unit_name }); tty_config.setColor(out_stream, .Reset); - try out_stream.write("\n"); + try noasync out_stream.write("\n"); // Show the matching source code line if possible if (line_info) |li| { @@ -748,12 +748,12 @@ fn printLineInfo( // The caret already takes one char const space_needed = @intCast(usize, li.column - 1); - try out_stream.writeByteNTimes(' ', space_needed); + try noasync out_stream.writeByteNTimes(' ', space_needed); tty_config.setColor(out_stream, .Green); - try out_stream.write("^"); + try noasync out_stream.write("^"); tty_config.setColor(out_stream, .Reset); } - try out_stream.write("\n"); + try noasync out_stream.write("\n"); } else |err| switch (err) { error.EndOfFile, error.FileNotFound => {}, error.BadPathName => {}, diff --git a/lib/std/event.zig b/lib/std/event.zig index 2c72c22588..64d73a25e1 100644 --- a/lib/std/event.zig +++ b/lib/std/event.zig @@ -6,11 +6,9 @@ pub const Locked = @import("event/locked.zig").Locked; pub const RwLock = @import("event/rwlock.zig").RwLock; pub const RwLocked = @import("event/rwlocked.zig").RwLocked; pub const Loop = @import("event/loop.zig").Loop; -pub const fs = @import("event/fs.zig"); test "import event tests" { _ = @import("event/channel.zig"); - _ = @import("event/fs.zig"); _ = @import("event/future.zig"); _ = @import("event/group.zig"); _ = @import("event/lock.zig"); diff --git a/lib/std/event/channel.zig b/lib/std/event/channel.zig index 2bcf6a4c50..fd70f73aab 100644 --- a/lib/std/event/channel.zig +++ b/lib/std/event/channel.zig @@ -267,17 +267,16 @@ pub fn Channel(comptime T: type) type { } test "std.event.Channel" { + if (!std.io.is_async) return error.SkipZigTest; + // https://github.com/ziglang/zig/issues/1908 if (builtin.single_threaded) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/3251 if (builtin.os == .freebsd) return error.SkipZigTest; - // TODO provide a way to run tests in evented I/O mode - if (!std.io.is_async) return error.SkipZigTest; - var channel: Channel(i32) = undefined; - channel.init([0]i32{}); + channel.init(&[0]i32{}); defer channel.deinit(); var handle = async testChannelGetter(&channel); diff --git a/lib/std/event/group.zig b/lib/std/event/group.zig index 2668096615..98ebdbd1f8 100644 --- a/lib/std/event/group.zig +++ b/lib/std/event/group.zig @@ -22,7 +22,7 @@ pub fn Group(comptime ReturnType: type) type { const AllocStack = std.atomic.Stack(Node); pub const Node = struct { - bytes: []const u8 = [0]u8{}, + bytes: []const u8 = &[0]u8{}, handle: anyframe->ReturnType, }; diff --git a/lib/std/event/lock.zig b/lib/std/event/lock.zig index a95c5bf7e2..e1b3495e5c 100644 --- a/lib/std/event/lock.zig +++ b/lib/std/event/lock.zig @@ -117,21 +117,21 @@ pub const Lock = struct { }; test "std.event.Lock" { + if (!std.io.is_async) return error.SkipZigTest; + // TODO https://github.com/ziglang/zig/issues/1908 if (builtin.single_threaded) return error.SkipZigTest; // TODO https://github.com/ziglang/zig/issues/3251 if (builtin.os == .freebsd) return error.SkipZigTest; - // TODO provide a way to run tests in evented I/O mode - if (!std.io.is_async) return error.SkipZigTest; - var lock = Lock.init(); defer lock.deinit(); _ = async testLock(&lock); - testing.expectEqualSlices(i32, [1]i32{3 * @intCast(i32, shared_test_data.len)} ** shared_test_data.len, shared_test_data); + const expected_result = [1]i32{3 * @intCast(i32, shared_test_data.len)} ** shared_test_data.len; + testing.expectEqualSlices(i32, &expected_result, &shared_test_data); } async fn testLock(lock: *Lock) void { diff --git a/lib/std/event/loop.zig b/lib/std/event/loop.zig index e80266c640..8000ab88c6 100644 --- a/lib/std/event/loop.zig +++ b/lib/std/event/loop.zig @@ -6,7 +6,6 @@ const testing = std.testing; const mem = std.mem; const AtomicRmwOp = builtin.AtomicRmwOp; const AtomicOrder = builtin.AtomicOrder; -const fs = std.event.fs; const os = std.os; const windows = os.windows; const maxInt = std.math.maxInt; @@ -174,21 +173,19 @@ pub const Loop = struct { fn initOsData(self: *Loop, extra_thread_count: usize) InitOsDataError!void { switch (builtin.os) { .linux => { - self.os_data.fs_queue = std.atomic.Queue(fs.Request).init(); + self.os_data.fs_queue = std.atomic.Queue(Request).init(); self.os_data.fs_queue_item = 0; // we need another thread for the file system because Linux does not have an async // file system I/O API. - self.os_data.fs_end_request = fs.RequestNode{ - .prev = undefined, - .next = undefined, - .data = fs.Request{ - .msg = fs.Request.Msg.End, - .finish = fs.Request.Finish.NoAction, + self.os_data.fs_end_request = Request.Node{ + .data = Request{ + .msg = .end, + .finish = .NoAction, }, }; errdefer { - while (self.available_eventfd_resume_nodes.pop()) |node| os.close(node.data.eventfd); + while (self.available_eventfd_resume_nodes.pop()) |node| noasync os.close(node.data.eventfd); } for (self.eventfd_resume_nodes) |*eventfd_node| { eventfd_node.* = std.atomic.Stack(ResumeNode.EventFd).Node{ @@ -207,10 +204,10 @@ pub const Loop = struct { } self.os_data.epollfd = try os.epoll_create1(os.EPOLL_CLOEXEC); - errdefer os.close(self.os_data.epollfd); + errdefer noasync os.close(self.os_data.epollfd); self.os_data.final_eventfd = try os.eventfd(0, os.EFD_CLOEXEC | os.EFD_NONBLOCK); - errdefer os.close(self.os_data.final_eventfd); + errdefer noasync os.close(self.os_data.final_eventfd); self.os_data.final_eventfd_event = os.epoll_event{ .events = os.EPOLLIN, @@ -237,7 +234,7 @@ pub const Loop = struct { var extra_thread_index: usize = 0; errdefer { // writing 8 bytes to an eventfd cannot fail - os.write(self.os_data.final_eventfd, &wakeup_bytes) catch unreachable; + noasync os.write(self.os_data.final_eventfd, &wakeup_bytes) catch unreachable; while (extra_thread_index != 0) { extra_thread_index -= 1; self.extra_threads[extra_thread_index].wait(); @@ -249,20 +246,20 @@ pub const Loop = struct { }, .macosx, .freebsd, .netbsd, .dragonfly => { self.os_data.kqfd = try os.kqueue(); - errdefer os.close(self.os_data.kqfd); + errdefer noasync os.close(self.os_data.kqfd); self.os_data.fs_kqfd = try os.kqueue(); - errdefer os.close(self.os_data.fs_kqfd); + errdefer noasync os.close(self.os_data.fs_kqfd); - self.os_data.fs_queue = std.atomic.Queue(fs.Request).init(); + self.os_data.fs_queue = std.atomic.Queue(Request).init(); // we need another thread for the file system because Darwin does not have an async // file system I/O API. - self.os_data.fs_end_request = fs.RequestNode{ + self.os_data.fs_end_request = Request.Node{ .prev = undefined, .next = undefined, - .data = fs.Request{ - .msg = fs.Request.Msg.End, - .finish = fs.Request.Finish.NoAction, + .data = Request{ + .msg = .end, + .finish = .NoAction, }, }; @@ -407,14 +404,14 @@ pub const Loop = struct { fn deinitOsData(self: *Loop) void { switch (builtin.os) { .linux => { - os.close(self.os_data.final_eventfd); - while (self.available_eventfd_resume_nodes.pop()) |node| os.close(node.data.eventfd); - os.close(self.os_data.epollfd); + noasync os.close(self.os_data.final_eventfd); + while (self.available_eventfd_resume_nodes.pop()) |node| noasync os.close(node.data.eventfd); + noasync os.close(self.os_data.epollfd); self.allocator.free(self.eventfd_resume_nodes); }, .macosx, .freebsd, .netbsd, .dragonfly => { - os.close(self.os_data.kqfd); - os.close(self.os_data.fs_kqfd); + noasync os.close(self.os_data.kqfd); + noasync os.close(self.os_data.fs_kqfd); }, .windows => { windows.CloseHandle(self.os_data.io_port); @@ -711,6 +708,190 @@ pub const Loop = struct { } } + /// Performs an async `os.open` using a separate thread. + pub fn openZ(self: *Loop, file_path: [*:0]const u8, flags: u32, mode: usize) os.OpenError!os.fd_t { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .open = .{ + .path = file_path, + .flags = flags, + .mode = mode, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.open.result; + } + + /// Performs an async `os.opent` using a separate thread. + pub fn openatZ(self: *Loop, fd: os.fd_t, file_path: [*:0]const u8, flags: u32, mode: usize) os.OpenError!os.fd_t { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .openat = .{ + .fd = fd, + .path = file_path, + .flags = flags, + .mode = mode, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.openat.result; + } + + /// Performs an async `os.close` using a separate thread. + pub fn close(self: *Loop, fd: os.fd_t) void { + var req_node = Request.Node{ + .data = .{ + .msg = .{ .close = .{ .fd = fd } }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + } + + /// Performs an async `os.read` using a separate thread. + /// `fd` must block and not return EAGAIN. + pub fn read(self: *Loop, fd: os.fd_t, buf: []u8) os.ReadError!usize { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .read = .{ + .fd = fd, + .buf = buf, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.read.result; + } + + /// Performs an async `os.readv` using a separate thread. + /// `fd` must block and not return EAGAIN. + pub fn readv(self: *Loop, fd: os.fd_t, iov: []const os.iovec) os.ReadError!usize { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .readv = .{ + .fd = fd, + .iov = iov, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.readv.result; + } + + /// Performs an async `os.preadv` using a separate thread. + /// `fd` must block and not return EAGAIN. + pub fn preadv(self: *Loop, fd: os.fd_t, iov: []const os.iovec, offset: u64) os.ReadError!usize { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .preadv = .{ + .fd = fd, + .iov = iov, + .offset = offset, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.preadv.result; + } + + /// Performs an async `os.write` using a separate thread. + /// `fd` must block and not return EAGAIN. + pub fn write(self: *Loop, fd: os.fd_t, bytes: []const u8) os.WriteError!void { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .write = .{ + .fd = fd, + .bytes = bytes, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.write.result; + } + + /// Performs an async `os.writev` using a separate thread. + /// `fd` must block and not return EAGAIN. + pub fn writev(self: *Loop, fd: os.fd_t, iov: []const os.iovec_const) os.WriteError!void { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .writev = .{ + .fd = fd, + .iov = iov, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.writev.result; + } + + /// Performs an async `os.pwritev` using a separate thread. + /// `fd` must block and not return EAGAIN. + pub fn pwritev(self: *Loop, fd: os.fd_t, iov: []const os.iovec_const, offset: u64) os.WriteError!void { + var req_node = Request.Node{ + .data = .{ + .msg = .{ + .pwritev = .{ + .fd = fd, + .iov = iov, + .offset = offset, + .result = undefined, + }, + }, + .finish = .{ .TickNode = .{ .data = @frame() } }, + }, + }; + suspend { + self.posixFsRequest(&req_node); + } + return req_node.data.msg.pwritev.result; + } + fn workerRun(self: *Loop) void { while (true) { while (true) { @@ -804,7 +985,7 @@ pub const Loop = struct { } } - fn posixFsRequest(self: *Loop, request_node: *fs.RequestNode) void { + fn posixFsRequest(self: *Loop, request_node: *Request.Node) void { self.beginOneEvent(); // finished in posixFsRun after processing the msg self.os_data.fs_queue.put(request_node); switch (builtin.os) { @@ -826,7 +1007,7 @@ pub const Loop = struct { } } - fn posixFsCancel(self: *Loop, request_node: *fs.RequestNode) void { + fn posixFsCancel(self: *Loop, request_node: *Request.Node) void { if (self.os_data.fs_queue.remove(request_node)) { self.finishOneEvent(); } @@ -841,37 +1022,32 @@ pub const Loop = struct { } while (self.os_data.fs_queue.get()) |node| { switch (node.data.msg) { - .End => return, - .WriteV => |*msg| { + .end => return, + .read => |*msg| { + msg.result = noasync os.read(msg.fd, msg.buf); + }, + .write => |*msg| { + msg.result = noasync os.write(msg.fd, msg.bytes); + }, + .writev => |*msg| { msg.result = noasync os.writev(msg.fd, msg.iov); }, - .PWriteV => |*msg| { + .pwritev => |*msg| { msg.result = noasync os.pwritev(msg.fd, msg.iov, msg.offset); }, - .PReadV => |*msg| { + .preadv => |*msg| { msg.result = noasync os.preadv(msg.fd, msg.iov, msg.offset); }, - .Open => |*msg| { - msg.result = noasync os.openC(msg.path.ptr, msg.flags, msg.mode); + .open => |*msg| { + msg.result = noasync os.openC(msg.path, msg.flags, msg.mode); }, - .Close => |*msg| noasync os.close(msg.fd), - .WriteFile => |*msg| blk: { - const O_LARGEFILE = if (@hasDecl(os, "O_LARGEFILE")) os.O_LARGEFILE else 0; - const flags = O_LARGEFILE | os.O_WRONLY | os.O_CREAT | - os.O_CLOEXEC | os.O_TRUNC; - const fd = noasync os.openC(msg.path.ptr, flags, msg.mode) catch |err| { - msg.result = err; - break :blk; - }; - defer noasync os.close(fd); - msg.result = noasync os.write(fd, msg.contents); + .openat => |*msg| { + msg.result = noasync os.openatC(msg.fd, msg.path, msg.flags, msg.mode); }, + .close => |*msg| noasync os.close(msg.fd), } switch (node.data.finish) { .TickNode => |*tick_node| self.onNextTick(tick_node), - .DeallocCloseOperation => |close_op| { - self.allocator.destroy(close_op); - }, .NoAction => {}, } self.finishOneEvent(); @@ -911,8 +1087,8 @@ pub const Loop = struct { fs_kevent_wait: os.Kevent, fs_thread: *Thread, fs_kqfd: i32, - fs_queue: std.atomic.Queue(fs.Request), - fs_end_request: fs.RequestNode, + fs_queue: std.atomic.Queue(Request), + fs_end_request: Request.Node, }; const LinuxOsData = struct { @@ -921,8 +1097,99 @@ pub const Loop = struct { final_eventfd_event: os.linux.epoll_event, fs_thread: *Thread, fs_queue_item: i32, - fs_queue: std.atomic.Queue(fs.Request), - fs_end_request: fs.RequestNode, + fs_queue: std.atomic.Queue(Request), + fs_end_request: Request.Node, + }; + + pub const Request = struct { + msg: Msg, + finish: Finish, + + pub const Node = std.atomic.Queue(Request).Node; + + pub const Finish = union(enum) { + TickNode: Loop.NextTickNode, + NoAction, + }; + + pub const Msg = union(enum) { + read: Read, + write: Write, + writev: WriteV, + pwritev: PWriteV, + preadv: PReadV, + open: Open, + openat: OpenAt, + close: Close, + + /// special - means the fs thread should exit + end, + + pub const Read = struct { + fd: os.fd_t, + buf: []u8, + result: Error!usize, + + pub const Error = os.ReadError; + }; + + pub const Write = struct { + fd: os.fd_t, + bytes: []const u8, + result: Error!void, + + pub const Error = os.WriteError; + }; + + pub const WriteV = struct { + fd: os.fd_t, + iov: []const os.iovec_const, + result: Error!void, + + pub const Error = os.WriteError; + }; + + pub const PWriteV = struct { + fd: os.fd_t, + iov: []const os.iovec_const, + offset: usize, + result: Error!void, + + pub const Error = os.WriteError; + }; + + pub const PReadV = struct { + fd: os.fd_t, + iov: []const os.iovec, + offset: usize, + result: Error!usize, + + pub const Error = os.ReadError; + }; + + pub const Open = struct { + path: [*:0]const u8, + flags: u32, + mode: os.mode_t, + result: Error!os.fd_t, + + pub const Error = os.OpenError; + }; + + pub const OpenAt = struct { + fd: os.fd_t, + path: [*:0]const u8, + flags: u32, + mode: os.mode_t, + result: Error!os.fd_t, + + pub const Error = os.OpenError; + }; + + pub const Close = struct { + fd: os.fd_t, + }; + }; }; }; diff --git a/lib/std/fmt.zig b/lib/std/fmt.zig index e0d58a23e6..370717a4f7 100644 --- a/lib/std/fmt.zig +++ b/lib/std/fmt.zig @@ -78,7 +78,7 @@ fn peekIsAlign(comptime fmt: []const u8) bool { pub fn format( context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, comptime fmt: []const u8, args: var, ) Errors!void { @@ -326,7 +326,7 @@ pub fn formatType( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, max_depth: usize, ) Errors!void { if (comptime std.mem.eql(u8, fmt, "*")) { @@ -488,7 +488,7 @@ fn formatValue( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { if (comptime std.mem.eql(u8, fmt, "B")) { return formatBytes(value, options, 1000, context, Errors, output); @@ -510,7 +510,7 @@ pub fn formatIntValue( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { comptime var radix = 10; comptime var uppercase = false; @@ -552,7 +552,7 @@ fn formatFloatValue( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "e")) { return formatFloatScientific(value, options, context, Errors, output); @@ -569,7 +569,7 @@ pub fn formatText( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { if (fmt.len == 0) { return output(context, bytes); @@ -590,7 +590,7 @@ pub fn formatAsciiChar( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { return output(context, @as(*const [1]u8, &c)[0..]); } @@ -600,7 +600,7 @@ pub fn formatBuf( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { try output(context, buf); @@ -620,7 +620,7 @@ pub fn formatFloatScientific( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { var x = @floatCast(f64, value); @@ -715,7 +715,7 @@ pub fn formatFloatDecimal( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { var x = @as(f64, value); @@ -861,7 +861,7 @@ pub fn formatBytes( comptime radix: usize, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { if (value == 0) { return output(context, "0B"); @@ -902,7 +902,7 @@ pub fn formatInt( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { const int_value = if (@TypeOf(value) == comptime_int) blk: { const Int = math.IntFittingRange(value, value); @@ -924,7 +924,7 @@ fn formatIntSigned( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { const new_options = FormatOptions{ .width = if (options.width) |w| (if (w == 0) 0 else w - 1) else null, @@ -955,7 +955,7 @@ fn formatIntUnsigned( options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { assert(base >= 2); var buf: [math.max(@TypeOf(value).bit_count, 1)]u8 = undefined; @@ -1419,7 +1419,7 @@ test "custom" { options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { if (fmt.len == 0 or comptime std.mem.eql(u8, fmt, "p")) { return std.fmt.format(context, Errors, output, "({d:.3},{d:.3})", .{ self.x, self.y }); @@ -1626,7 +1626,7 @@ test "formatType max_depth" { options: FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) Errors!void { if (fmt.len == 0) { return std.fmt.format(context, Errors, output, "({d:.3},{d:.3})", .{ self.x, self.y }); diff --git a/lib/std/fs.zig b/lib/std/fs.zig index ec351ff6e2..a39f283e55 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -23,6 +23,8 @@ pub const realpathW = os.realpathW; pub const getAppDataDir = @import("fs/get_app_data_dir.zig").getAppDataDir; pub const GetAppDataDirError = @import("fs/get_app_data_dir.zig").GetAppDataDirError; +pub const Watch = @import("fs/watch.zig").Watch; + /// This represents the maximum size of a UTF-8 encoded file path. /// All file system operations which return a path are guaranteed to /// fit into a UTF-8 encoded array of this length. @@ -43,6 +45,13 @@ pub const base64_encoder = base64.Base64Encoder.init( base64.standard_pad_char, ); +/// Whether or not async file system syscalls need a dedicated thread because the operating +/// system does not support non-blocking I/O on the file system. +pub const need_async_thread = std.io.is_async and switch (builtin.os) { + .windows, .other => false, + else => true, +}; + /// TODO remove the allocator requirement from this API pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void { if (symLink(existing_path, new_path)) { @@ -688,11 +697,16 @@ pub const Dir = struct { } pub fn close(self: *Dir) void { - os.close(self.fd); + if (need_async_thread) { + std.event.Loop.instance.?.close(self.fd); + } else { + os.close(self.fd); + } self.* = undefined; } /// Opens a file for reading or writing, without attempting to create a new file. + /// To create a new file, see `createFile`. /// Call `File.close` to release the resource. /// Asserts that the path parameter has no null bytes. pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { @@ -718,8 +732,11 @@ pub const Dir = struct { @as(u32, os.O_WRONLY) else @as(u32, os.O_RDONLY); - const fd = try os.openatC(self.fd, sub_path, os_flags, 0); - return File{ .handle = fd }; + const fd = if (need_async_thread) + try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) + else + try os.openatC(self.fd, sub_path, os_flags, 0); + return File{ .handle = fd, .io_mode = .blocking }; } /// Same as `openFile` but Windows-only and the path parameter is @@ -756,8 +773,11 @@ pub const Dir = struct { (if (flags.truncate) @as(u32, os.O_TRUNC) else 0) | (if (flags.read) @as(u32, os.O_RDWR) else os.O_WRONLY) | (if (flags.exclusive) @as(u32, os.O_EXCL) else 0); - const fd = try os.openatC(self.fd, sub_path_c, os_flags, flags.mode); - return File{ .handle = fd }; + const fd = if (need_async_thread) + try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) + else + try os.openatC(self.fd, sub_path_c, os_flags, flags.mode); + return File{ .handle = fd, .io_mode = .blocking }; } /// Same as `createFile` but Windows-only and the path parameter is @@ -798,7 +818,10 @@ pub const Dir = struct { ) File.OpenError!File { const w = os.windows; - var result = File{ .handle = undefined }; + var result = File{ + .handle = undefined, + .io_mode = .blocking, + }; const path_len_bytes = math.cast(u16, mem.toSliceConst(u16, sub_path_w).len * 2) catch |err| switch (err) { error.Overflow => return error.NameTooLong, @@ -919,7 +942,12 @@ pub const Dir = struct { } fn openDirFlagsC(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { - const fd = os.openatC(self.fd, sub_path_c, flags | os.O_DIRECTORY, 0) catch |err| switch (err) { + const os_flags = flags | os.O_DIRECTORY; + const result = if (need_async_thread) + std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, 0) + else + os.openatC(self.fd, sub_path_c, os_flags, 0); + const fd = result catch |err| switch (err) { error.FileTooBig => unreachable, // can't happen for directories error.IsDir => unreachable, // we're providing O_DIRECTORY error.NoSpaceLeft => unreachable, // not providing O_CREAT @@ -1588,4 +1616,5 @@ test "" { _ = @import("fs/path.zig"); _ = @import("fs/file.zig"); _ = @import("fs/get_app_data_dir.zig"); + _ = @import("fs/watch.zig"); } diff --git a/lib/std/fs/file.zig b/lib/std/fs/file.zig index 924f10401e..92dcfefad0 100644 --- a/lib/std/fs/file.zig +++ b/lib/std/fs/file.zig @@ -8,18 +8,29 @@ const assert = std.debug.assert; const windows = os.windows; const Os = builtin.Os; const maxInt = std.math.maxInt; +const need_async_thread = std.fs.need_async_thread; pub const File = struct { /// The OS-specific file descriptor or file handle. handle: os.fd_t, - pub const Mode = switch (builtin.os) { - Os.windows => void, - else => u32, - }; + /// On some systems, such as Linux, file system file descriptors are incapable of non-blocking I/O. + /// This forces us to perform asynchronous I/O on a dedicated thread, to achieve non-blocking + /// file-system I/O. To do this, `File` must be aware of whether it is a file system file descriptor, + /// or, more specifically, whether the I/O is blocking. + io_mode: io.Mode, + + /// Even when std.io.mode is async, it is still sometimes desirable to perform blocking I/O, although + /// not by default. For example, when printing a stack trace to stderr. + async_block_allowed: @TypeOf(async_block_allowed_no) = async_block_allowed_no, + + pub const async_block_allowed_yes = if (io.is_async) true else {}; + pub const async_block_allowed_no = if (io.is_async) false else {}; + + pub const Mode = os.mode_t; pub const default_mode = switch (builtin.os) { - Os.windows => {}, + .windows => 0, else => 0o666, }; @@ -49,87 +60,27 @@ pub const File = struct { mode: Mode = default_mode, }; - /// Deprecated; call `std.fs.Dir.openFile` directly. - pub fn openRead(path: []const u8) OpenError!File { - return std.fs.cwd().openFile(path, .{}); - } - - /// Deprecated; call `std.fs.Dir.openFileC` directly. - pub fn openReadC(path_c: [*:0]const u8) OpenError!File { - return std.fs.cwd().openFileC(path_c, .{}); - } - - /// Deprecated; call `std.fs.Dir.openFileW` directly. - pub fn openReadW(path_w: [*:0]const u16) OpenError!File { - return std.fs.cwd().openFileW(path_w, .{}); - } - - /// Deprecated; call `std.fs.Dir.createFile` directly. - pub fn openWrite(path: []const u8) OpenError!File { - return std.fs.cwd().createFile(path, .{}); - } - - /// Deprecated; call `std.fs.Dir.createFile` directly. - pub fn openWriteMode(path: []const u8, file_mode: Mode) OpenError!File { - return std.fs.cwd().createFile(path, .{ .mode = file_mode }); - } - - /// Deprecated; call `std.fs.Dir.createFileC` directly. - pub fn openWriteModeC(path_c: [*:0]const u8, file_mode: Mode) OpenError!File { - return std.fs.cwd().createFileC(path_c, .{ .mode = file_mode }); - } - - /// Deprecated; call `std.fs.Dir.createFileW` directly. - pub fn openWriteModeW(path_w: [*:0]const u16, file_mode: Mode) OpenError!File { - return std.fs.cwd().createFileW(path_w, .{ .mode = file_mode }); - } - - /// Deprecated; call `std.fs.Dir.createFile` directly. - pub fn openWriteNoClobber(path: []const u8, file_mode: Mode) OpenError!File { - return std.fs.cwd().createFile(path, .{ - .mode = file_mode, - .exclusive = true, - }); - } - - /// Deprecated; call `std.fs.Dir.createFileC` directly. - pub fn openWriteNoClobberC(path_c: [*:0]const u8, file_mode: Mode) OpenError!File { - return std.fs.cwd().createFileC(path_c, .{ - .mode = file_mode, - .exclusive = true, - }); - } - - /// Deprecated; call `std.fs.Dir.createFileW` directly. - pub fn openWriteNoClobberW(path_w: [*:0]const u16, file_mode: Mode) OpenError!File { - return std.fs.cwd().createFileW(path_w, .{ - .mode = file_mode, - .exclusive = true, - }); - } - - pub fn openHandle(handle: os.fd_t) File { - return File{ .handle = handle }; - } - /// Test for the existence of `path`. /// `path` is UTF8-encoded. /// In general it is recommended to avoid this function. For example, /// instead of testing if a file exists and then opening it, just /// open it and handle the error for file not found. /// TODO: deprecate this and move it to `std.fs.Dir`. + /// TODO: integrate with async I/O pub fn access(path: []const u8) !void { return os.access(path, os.F_OK); } /// Same as `access` except the parameter is null-terminated. /// TODO: deprecate this and move it to `std.fs.Dir`. + /// TODO: integrate with async I/O pub fn accessC(path: [*:0]const u8) !void { return os.accessC(path, os.F_OK); } /// Same as `access` except the parameter is null-terminated UTF16LE-encoded. /// TODO: deprecate this and move it to `std.fs.Dir`. + /// TODO: integrate with async I/O pub fn accessW(path: [*:0]const u16) !void { return os.accessW(path, os.F_OK); } @@ -137,7 +88,11 @@ pub const File = struct { /// Upon success, the stream is in an uninitialized state. To continue using it, /// you must use the open() function. pub fn close(self: File) void { - return os.close(self.handle); + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + std.event.Loop.instance.?.close(self.handle); + } else { + return os.close(self.handle); + } } /// Test whether the file refers to a terminal. @@ -167,26 +122,31 @@ pub const File = struct { pub const SeekError = os.SeekError; /// Repositions read/write file offset relative to the current offset. + /// TODO: integrate with async I/O pub fn seekBy(self: File, offset: i64) SeekError!void { return os.lseek_CUR(self.handle, offset); } /// Repositions read/write file offset relative to the end. + /// TODO: integrate with async I/O pub fn seekFromEnd(self: File, offset: i64) SeekError!void { return os.lseek_END(self.handle, offset); } /// Repositions read/write file offset relative to the beginning. + /// TODO: integrate with async I/O pub fn seekTo(self: File, offset: u64) SeekError!void { return os.lseek_SET(self.handle, offset); } pub const GetPosError = os.SeekError || os.FStatError; + /// TODO: integrate with async I/O pub fn getPos(self: File) GetPosError!u64 { return os.lseek_CUR_get(self.handle); } + /// TODO: integrate with async I/O pub fn getEndPos(self: File) GetPosError!u64 { if (builtin.os == .windows) { return windows.GetFileSizeEx(self.handle); @@ -196,6 +156,7 @@ pub const File = struct { pub const ModeError = os.FStatError; + /// TODO: integrate with async I/O pub fn mode(self: File) ModeError!Mode { if (builtin.os == .windows) { return {}; @@ -219,6 +180,7 @@ pub const File = struct { pub const StatError = os.FStatError; + /// TODO: integrate with async I/O pub fn stat(self: File) StatError!Stat { if (builtin.os == .windows) { var io_status_block: windows.IO_STATUS_BLOCK = undefined; @@ -233,7 +195,7 @@ pub const File = struct { } return Stat{ .size = @bitCast(u64, info.StandardInformation.EndOfFile), - .mode = {}, + .mode = 0, .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), @@ -259,6 +221,7 @@ pub const File = struct { /// and therefore this function cannot guarantee any precision will be stored. /// Further, the maximum value is limited by the system ABI. When a value is provided /// that exceeds this range, the value is clamped to the maximum. + /// TODO: integrate with async I/O pub fn updateTimes( self: File, /// access timestamp in nanoseconds @@ -287,21 +250,61 @@ pub const File = struct { pub const ReadError = os.ReadError; pub fn read(self: File, buffer: []u8) ReadError!usize { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.read(self.handle, buffer); + } return os.read(self.handle, buffer); } + pub fn pread(self: File, buffer: []u8, offset: u64) ReadError!usize { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.pread(self.handle, buffer); + } + return os.pread(self.handle, buffer, offset); + } + + pub fn readv(self: File, iovecs: []const os.iovec) ReadError!usize { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.readv(self.handle, iovecs); + } + return os.readv(self.handle, iovecs); + } + + pub fn preadv(self: File, iovecs: []const os.iovec, offset: u64) ReadError!usize { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.preadv(self.handle, iovecs, offset); + } + return os.preadv(self.handle, iovecs, offset); + } + pub const WriteError = os.WriteError; pub fn write(self: File, bytes: []const u8) WriteError!void { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.write(self.handle, bytes); + } return os.write(self.handle, bytes); } - pub fn writev_iovec(self: File, iovecs: []const os.iovec_const) WriteError!void { - if (std.event.Loop.instance) |loop| { - return std.event.fs.writevPosix(loop, self.handle, iovecs); - } else { - return os.writev(self.handle, iovecs); + pub fn pwrite(self: File, bytes: []const u8, offset: u64) WriteError!void { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.pwrite(self.handle, bytes, offset); } + return os.pwrite(self.handle, bytes, offset); + } + + pub fn writev(self: File, iovecs: []const os.iovec_const) WriteError!void { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.writev(self.handle, iovecs); + } + return os.writev(self.handle, iovecs); + } + + pub fn pwritev(self: File, iovecs: []const os.iovec_const, offset: usize) WriteError!void { + if (need_async_thread and self.io_mode == .blocking and !self.async_block_allowed) { + return std.event.Loop.instance.?.pwritev(self.handle, iovecs); + } + return os.pwritev(self.handle, iovecs); } pub fn inStream(file: File) InStream { diff --git a/lib/std/event/fs.zig b/lib/std/fs/watch.zig similarity index 53% rename from lib/std/event/fs.zig rename to lib/std/fs/watch.zig index 7581d2a1fb..c904f110e2 100644 --- a/lib/std/event/fs.zig +++ b/lib/std/fs/watch.zig @@ -1,5 +1,5 @@ -const builtin = @import("builtin"); const std = @import("../std.zig"); +const builtin = @import("builtin"); const event = std.event; const assert = std.debug.assert; const testing = std.testing; @@ -11,702 +11,10 @@ const fd_t = os.fd_t; const File = std.fs.File; const Allocator = mem.Allocator; -//! TODO mege this with `std.fs` - const global_event_loop = Loop.instance orelse - @compileError("std.event.fs currently only works with event-based I/O"); + @compileError("std.fs.Watch currently only works with event-based I/O"); -pub const RequestNode = std.atomic.Queue(Request).Node; - -pub const Request = struct { - msg: Msg, - finish: Finish, - - pub const Finish = union(enum) { - TickNode: Loop.NextTickNode, - DeallocCloseOperation: *CloseOperation, - NoAction, - }; - - pub const Msg = union(enum) { - WriteV: WriteV, - PWriteV: PWriteV, - PReadV: PReadV, - Open: Open, - Close: Close, - WriteFile: WriteFile, - End, // special - means the fs thread should exit - - pub const WriteV = struct { - fd: fd_t, - iov: []const os.iovec_const, - result: Error!void, - - pub const Error = os.WriteError; - }; - - pub const PWriteV = struct { - fd: fd_t, - iov: []const os.iovec_const, - offset: usize, - result: Error!void, - - pub const Error = os.WriteError; - }; - - pub const PReadV = struct { - fd: fd_t, - iov: []const os.iovec, - offset: usize, - result: Error!usize, - - pub const Error = os.ReadError; - }; - - pub const Open = struct { - path: [:0]const u8, - flags: u32, - mode: File.Mode, - result: Error!fd_t, - - pub const Error = File.OpenError; - }; - - pub const WriteFile = struct { - path: [:0]const u8, - contents: []const u8, - mode: File.Mode, - result: Error!void, - - pub const Error = File.OpenError || File.WriteError; - }; - - pub const Close = struct { - fd: fd_t, - }; - }; -}; - -pub const PWriteVError = error{OutOfMemory} || File.WriteError; - -/// data - just the inner references - must live until pwritev frame completes. -pub fn pwritev(allocator: *Allocator, fd: fd_t, data: []const []const u8, offset: usize) PWriteVError!void { - switch (builtin.os) { - .macosx, - .linux, - .freebsd, - .netbsd, - .dragonfly, - => { - const iovecs = try allocator.alloc(os.iovec_const, data.len); - defer allocator.free(iovecs); - - for (data) |buf, i| { - iovecs[i] = os.iovec_const{ - .iov_base = buf.ptr, - .iov_len = buf.len, - }; - } - - return pwritevPosix(fd, iovecs, offset); - }, - .windows => { - const data_copy = try std.mem.dupe(allocator, []const u8, data); - defer allocator.free(data_copy); - return pwritevWindows(fd, data, offset); - }, - else => @compileError("Unsupported OS"), - } -} - -/// data must outlive the returned frame -pub fn pwritevWindows(fd: fd_t, data: []const []const u8, offset: usize) os.WindowsWriteError!void { - if (data.len == 0) return; - if (data.len == 1) return pwriteWindows(fd, data[0], offset); - - // TODO do these in parallel - var off = offset; - for (data) |buf| { - try pwriteWindows(fd, buf, off); - off += buf.len; - } -} - -pub fn pwriteWindows(fd: fd_t, data: []const u8, offset: u64) os.WindowsWriteError!void { - var resume_node = Loop.ResumeNode.Basic{ - .base = Loop.ResumeNode{ - .id = Loop.ResumeNode.Id.Basic, - .handle = @frame(), - .overlapped = windows.OVERLAPPED{ - .Internal = 0, - .InternalHigh = 0, - .Offset = @truncate(u32, offset), - .OffsetHigh = @truncate(u32, offset >> 32), - .hEvent = null, - }, - }, - }; - // TODO only call create io completion port once per fd - _ = windows.CreateIoCompletionPort(fd, global_event_loop.os_data.io_port, undefined, undefined); - global_event_loop.beginOneEvent(); - errdefer global_event_loop.finishOneEvent(); - - errdefer { - _ = windows.kernel32.CancelIoEx(fd, &resume_node.base.overlapped); - } - suspend { - _ = windows.kernel32.WriteFile(fd, data.ptr, @intCast(windows.DWORD, data.len), null, &resume_node.base.overlapped); - } - var bytes_transferred: windows.DWORD = undefined; - if (windows.kernel32.GetOverlappedResult(fd, &resume_node.base.overlapped, &bytes_transferred, windows.FALSE) == 0) { - switch (windows.kernel32.GetLastError()) { - .IO_PENDING => unreachable, - .INVALID_USER_BUFFER => return error.SystemResources, - .NOT_ENOUGH_MEMORY => return error.SystemResources, - .OPERATION_ABORTED => return error.OperationAborted, - .NOT_ENOUGH_QUOTA => return error.SystemResources, - .BROKEN_PIPE => return error.BrokenPipe, - else => |err| return windows.unexpectedError(err), - } - } -} - -/// iovecs must live until pwritev frame completes. -pub fn pwritevPosix(fd: fd_t, iovecs: []const os.iovec_const, offset: usize) os.WriteError!void { - var req_node = RequestNode{ - .prev = null, - .next = null, - .data = Request{ - .msg = Request.Msg{ - .PWriteV = Request.Msg.PWriteV{ - .fd = fd, - .iov = iovecs, - .offset = offset, - .result = undefined, - }, - }, - .finish = Request.Finish{ - .TickNode = Loop.NextTickNode{ - .prev = null, - .next = null, - .data = @frame(), - }, - }, - }, - }; - - errdefer global_event_loop.posixFsCancel(&req_node); - - suspend { - global_event_loop.posixFsRequest(&req_node); - } - - return req_node.data.msg.PWriteV.result; -} - -/// iovecs must live until pwritev frame completes. -pub fn writevPosix(fd: fd_t, iovecs: []const os.iovec_const) os.WriteError!void { - var req_node = RequestNode{ - .prev = null, - .next = null, - .data = Request{ - .msg = Request.Msg{ - .WriteV = Request.Msg.WriteV{ - .fd = fd, - .iov = iovecs, - .result = undefined, - }, - }, - .finish = Request.Finish{ - .TickNode = Loop.NextTickNode{ - .prev = null, - .next = null, - .data = @frame(), - }, - }, - }, - }; - - suspend { - global_event_loop.posixFsRequest(&req_node); - } - - return req_node.data.msg.WriteV.result; -} - -pub const PReadVError = error{OutOfMemory} || File.ReadError; - -/// data - just the inner references - must live until preadv frame completes. -pub fn preadv(allocator: *Allocator, fd: fd_t, data: []const []u8, offset: usize) PReadVError!usize { - assert(data.len != 0); - switch (builtin.os) { - .macosx, - .linux, - .freebsd, - .netbsd, - .dragonfly, - => { - const iovecs = try allocator.alloc(os.iovec, data.len); - defer allocator.free(iovecs); - - for (data) |buf, i| { - iovecs[i] = os.iovec{ - .iov_base = buf.ptr, - .iov_len = buf.len, - }; - } - - return preadvPosix(fd, iovecs, offset); - }, - .windows => { - const data_copy = try std.mem.dupe(allocator, []u8, data); - defer allocator.free(data_copy); - return preadvWindows(fd, data_copy, offset); - }, - else => @compileError("Unsupported OS"), - } -} - -/// data must outlive the returned frame -pub fn preadvWindows(fd: fd_t, data: []const []u8, offset: u64) !usize { - assert(data.len != 0); - if (data.len == 1) return preadWindows(fd, data[0], offset); - - // TODO do these in parallel? - var off: usize = 0; - var iov_i: usize = 0; - var inner_off: usize = 0; - while (true) { - const v = data[iov_i]; - const amt_read = try preadWindows(fd, v[inner_off .. v.len - inner_off], offset + off); - off += amt_read; - inner_off += amt_read; - if (inner_off == v.len) { - iov_i += 1; - inner_off = 0; - if (iov_i == data.len) { - return off; - } - } - if (amt_read == 0) return off; // EOF - } -} - -pub fn preadWindows(fd: fd_t, data: []u8, offset: u64) !usize { - var resume_node = Loop.ResumeNode.Basic{ - .base = Loop.ResumeNode{ - .id = Loop.ResumeNode.Id.Basic, - .handle = @frame(), - .overlapped = windows.OVERLAPPED{ - .Internal = 0, - .InternalHigh = 0, - .Offset = @truncate(u32, offset), - .OffsetHigh = @truncate(u32, offset >> 32), - .hEvent = null, - }, - }, - }; - // TODO only call create io completion port once per fd - _ = windows.CreateIoCompletionPort(fd, global_event_loop.os_data.io_port, undefined, undefined) catch undefined; - global_event_loop.beginOneEvent(); - errdefer global_event_loop.finishOneEvent(); - - errdefer { - _ = windows.kernel32.CancelIoEx(fd, &resume_node.base.overlapped); - } - suspend { - _ = windows.kernel32.ReadFile(fd, data.ptr, @intCast(windows.DWORD, data.len), null, &resume_node.base.overlapped); - } - var bytes_transferred: windows.DWORD = undefined; - if (windows.kernel32.GetOverlappedResult(fd, &resume_node.base.overlapped, &bytes_transferred, windows.FALSE) == 0) { - switch (windows.kernel32.GetLastError()) { - .IO_PENDING => unreachable, - .OPERATION_ABORTED => return error.OperationAborted, - .BROKEN_PIPE => return error.BrokenPipe, - .HANDLE_EOF => return @as(usize, bytes_transferred), - else => |err| return windows.unexpectedError(err), - } - } - return @as(usize, bytes_transferred); -} - -/// iovecs must live until preadv frame completes -pub fn preadvPosix(fd: fd_t, iovecs: []const os.iovec, offset: usize) os.ReadError!usize { - var req_node = RequestNode{ - .prev = null, - .next = null, - .data = Request{ - .msg = Request.Msg{ - .PReadV = Request.Msg.PReadV{ - .fd = fd, - .iov = iovecs, - .offset = offset, - .result = undefined, - }, - }, - .finish = Request.Finish{ - .TickNode = Loop.NextTickNode{ - .prev = null, - .next = null, - .data = @frame(), - }, - }, - }, - }; - - errdefer global_event_loop.posixFsCancel(&req_node); - - suspend { - global_event_loop.posixFsRequest(&req_node); - } - - return req_node.data.msg.PReadV.result; -} - -pub fn openPosix(path: []const u8, flags: u32, mode: File.Mode) File.OpenError!fd_t { - const path_c = try std.os.toPosixPath(path); - - var req_node = RequestNode{ - .prev = null, - .next = null, - .data = Request{ - .msg = Request.Msg{ - .Open = Request.Msg.Open{ - .path = path_c[0..path.len], - .flags = flags, - .mode = mode, - .result = undefined, - }, - }, - .finish = Request.Finish{ - .TickNode = Loop.NextTickNode{ - .prev = null, - .next = null, - .data = @frame(), - }, - }, - }, - }; - - errdefer global_event_loop.posixFsCancel(&req_node); - - suspend { - global_event_loop.posixFsRequest(&req_node); - } - - return req_node.data.msg.Open.result; -} - -pub fn openRead(path: []const u8) File.OpenError!fd_t { - switch (builtin.os) { - .macosx, .linux, .freebsd, .netbsd, .dragonfly => { - const O_LARGEFILE = if (@hasDecl(os, "O_LARGEFILE")) os.O_LARGEFILE else 0; - const flags = O_LARGEFILE | os.O_RDONLY | os.O_CLOEXEC; - return openPosix(path, flags, File.default_mode); - }, - - .windows => return windows.CreateFile( - path, - windows.GENERIC_READ, - windows.FILE_SHARE_READ, - null, - windows.OPEN_EXISTING, - windows.FILE_ATTRIBUTE_NORMAL | windows.FILE_FLAG_OVERLAPPED, - null, - ), - - else => @compileError("Unsupported OS"), - } -} - -/// Creates if does not exist. Truncates the file if it exists. -/// Uses the default mode. -pub fn openWrite(path: []const u8) File.OpenError!fd_t { - return openWriteMode(path, File.default_mode); -} - -/// Creates if does not exist. Truncates the file if it exists. -pub fn openWriteMode(path: []const u8, mode: File.Mode) File.OpenError!fd_t { - switch (builtin.os) { - .macosx, - .linux, - .freebsd, - .netbsd, - .dragonfly, - => { - const O_LARGEFILE = if (@hasDecl(os, "O_LARGEFILE")) os.O_LARGEFILE else 0; - const flags = O_LARGEFILE | os.O_WRONLY | os.O_CREAT | os.O_CLOEXEC | os.O_TRUNC; - return openPosix(path, flags, File.default_mode); - }, - .windows => return windows.CreateFile( - path, - windows.GENERIC_WRITE, - windows.FILE_SHARE_WRITE | windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE, - null, - windows.CREATE_ALWAYS, - windows.FILE_ATTRIBUTE_NORMAL | windows.FILE_FLAG_OVERLAPPED, - null, - ), - else => @compileError("Unsupported OS"), - } -} - -/// Creates if does not exist. Does not truncate. -pub fn openReadWrite(path: []const u8, mode: File.Mode) File.OpenError!fd_t { - switch (builtin.os) { - .macosx, .linux, .freebsd, .netbsd, .dragonfly => { - const O_LARGEFILE = if (@hasDecl(os, "O_LARGEFILE")) os.O_LARGEFILE else 0; - const flags = O_LARGEFILE | os.O_RDWR | os.O_CREAT | os.O_CLOEXEC; - return openPosix(path, flags, mode); - }, - - .windows => return windows.CreateFile( - path, - windows.GENERIC_WRITE | windows.GENERIC_READ, - windows.FILE_SHARE_WRITE | windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE, - null, - windows.OPEN_ALWAYS, - windows.FILE_ATTRIBUTE_NORMAL | windows.FILE_FLAG_OVERLAPPED, - null, - ), - - else => @compileError("Unsupported OS"), - } -} - -/// This abstraction helps to close file handles in defer expressions -/// without the possibility of failure and without the use of suspend points. -/// Start a `CloseOperation` before opening a file, so that you can defer -/// `CloseOperation.finish`. -/// If you call `setHandle` then finishing will close the fd; otherwise finishing -/// will deallocate the `CloseOperation`. -pub const CloseOperation = struct { - allocator: *Allocator, - os_data: OsData, - - const OsData = switch (builtin.os) { - .linux, .macosx, .freebsd, .netbsd, .dragonfly => OsDataPosix, - - .windows => struct { - handle: ?fd_t, - }, - - else => @compileError("Unsupported OS"), - }; - - const OsDataPosix = struct { - have_fd: bool, - close_req_node: RequestNode, - }; - - pub fn start(allocator: *Allocator) (error{OutOfMemory}!*CloseOperation) { - const self = try allocator.create(CloseOperation); - self.* = CloseOperation{ - .allocator = allocator, - .os_data = switch (builtin.os) { - .linux, .macosx, .freebsd, .netbsd, .dragonfly => initOsDataPosix(self), - .windows => OsData{ .handle = null }, - else => @compileError("Unsupported OS"), - }, - }; - return self; - } - - fn initOsDataPosix(self: *CloseOperation) OsData { - return OsData{ - .have_fd = false, - .close_req_node = RequestNode{ - .prev = null, - .next = null, - .data = Request{ - .msg = Request.Msg{ - .Close = Request.Msg.Close{ .fd = undefined }, - }, - .finish = Request.Finish{ .DeallocCloseOperation = self }, - }, - }, - }; - } - - /// Defer this after creating. - pub fn finish(self: *CloseOperation) void { - switch (builtin.os) { - .linux, - .macosx, - .freebsd, - .netbsd, - .dragonfly, - => { - if (self.os_data.have_fd) { - global_event_loop.posixFsRequest(&self.os_data.close_req_node); - } else { - self.allocator.destroy(self); - } - }, - .windows => { - if (self.os_data.handle) |handle| { - os.close(handle); - } - self.allocator.destroy(self); - }, - else => @compileError("Unsupported OS"), - } - } - - pub fn setHandle(self: *CloseOperation, handle: fd_t) void { - switch (builtin.os) { - .linux, - .macosx, - .freebsd, - .netbsd, - .dragonfly, - => { - self.os_data.close_req_node.data.msg.Close.fd = handle; - self.os_data.have_fd = true; - }, - .windows => { - self.os_data.handle = handle; - }, - else => @compileError("Unsupported OS"), - } - } - - /// Undo a `setHandle`. - pub fn clearHandle(self: *CloseOperation) void { - switch (builtin.os) { - .linux, - .macosx, - .freebsd, - .netbsd, - .dragonfly, - => { - self.os_data.have_fd = false; - }, - .windows => { - self.os_data.handle = null; - }, - else => @compileError("Unsupported OS"), - } - } - - pub fn getHandle(self: *CloseOperation) fd_t { - switch (builtin.os) { - .linux, - .macosx, - .freebsd, - .netbsd, - .dragonfly, - => { - assert(self.os_data.have_fd); - return self.os_data.close_req_node.data.msg.Close.fd; - }, - .windows => { - return self.os_data.handle.?; - }, - else => @compileError("Unsupported OS"), - } - } -}; - -/// contents must remain alive until writeFile completes. -/// TODO make this atomic or provide writeFileAtomic and rename this one to writeFileTruncate -pub fn writeFile(allocator: *Allocator, path: []const u8, contents: []const u8) !void { - return writeFileMode(allocator, path, contents, File.default_mode); -} - -/// contents must remain alive until writeFile completes. -pub fn writeFileMode(allocator: *Allocator, path: []const u8, contents: []const u8, mode: File.Mode) !void { - switch (builtin.os) { - .linux, - .macosx, - .freebsd, - .netbsd, - .dragonfly, - => return writeFileModeThread(allocator, path, contents, mode), - .windows => return writeFileWindows(path, contents), - else => @compileError("Unsupported OS"), - } -} - -fn writeFileWindows(path: []const u8, contents: []const u8) !void { - const handle = try windows.CreateFile( - path, - windows.GENERIC_WRITE, - windows.FILE_SHARE_WRITE | windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE, - null, - windows.CREATE_ALWAYS, - windows.FILE_ATTRIBUTE_NORMAL | windows.FILE_FLAG_OVERLAPPED, - null, - ); - defer os.close(handle); - - try pwriteWindows(handle, contents, 0); -} - -fn writeFileModeThread(allocator: *Allocator, path: []const u8, contents: []const u8, mode: File.Mode) !void { - const path_with_null = try std.cstr.addNullByte(allocator, path); - defer allocator.free(path_with_null); - - var req_node = RequestNode{ - .prev = null, - .next = null, - .data = Request{ - .msg = Request.Msg{ - .WriteFile = Request.Msg.WriteFile{ - .path = path_with_null[0..path.len], - .contents = contents, - .mode = mode, - .result = undefined, - }, - }, - .finish = Request.Finish{ - .TickNode = Loop.NextTickNode{ - .prev = null, - .next = null, - .data = @frame(), - }, - }, - }, - }; - - errdefer global_event_loop.posixFsCancel(&req_node); - - suspend { - global_event_loop.posixFsRequest(&req_node); - } - - return req_node.data.msg.WriteFile.result; -} - -/// The frame resumes when the last data has been confirmed written, but before the file handle -/// is closed. -/// Caller owns returned memory. -pub fn readFile(allocator: *Allocator, file_path: []const u8, max_size: usize) ![]u8 { - var close_op = try CloseOperation.start(allocator); - defer close_op.finish(); - - const fd = try openRead(file_path); - close_op.setHandle(fd); - - var list = std.ArrayList(u8).init(allocator); - defer list.deinit(); - - while (true) { - try list.ensureCapacity(list.len + mem.page_size); - const buf = list.items[list.len..]; - const buf_array = [_][]u8{buf}; - const amt = try preadv(allocator, fd, &buf_array, list.len); - list.len += amt; - if (list.len > max_size) { - return error.FileTooBig; - } - if (amt < buf.len) { - return list.toOwnedSlice(); - } - } -} - -pub const WatchEventId = enum { +const WatchEventId = enum { CloseWrite, Delete, }; @@ -721,7 +29,7 @@ fn hashString(s: []const u16) u32 { return @truncate(u32, std.hash.Wyhash.hash(0, @sliceToBytes(s))); } -pub const WatchEventError = error{ +const WatchEventError = error{ UserResourceLimitReached, SystemResources, AccessDenied, @@ -1307,12 +615,11 @@ pub fn Watch(comptime V: type) type { const test_tmp_dir = "std_event_fs_test"; test "write a file, watch it, write it again" { - // TODO provide a way to run tests in evented I/O mode - if (!std.io.is_async) return error.SkipZigTest; + // TODO re-enable this test + if (true) return error.SkipZigTest; const allocator = std.heap.page_allocator; - // TODO move this into event loop too try os.makePath(allocator, test_tmp_dir); defer os.deleteTree(test_tmp_dir) catch {}; @@ -1366,53 +673,3 @@ fn testFsWatch(allocator: *Allocator) !void { // TODO test deleting the file and then re-adding it. we should get events for both } - -pub const OutStream = struct { - fd: fd_t, - stream: Stream, - allocator: *Allocator, - offset: usize, - - pub const Error = File.WriteError; - pub const Stream = event.io.OutStream(Error); - - pub fn init(allocator: *Allocator, fd: fd_t, offset: usize) OutStream { - return OutStream{ - .fd = fd, - .offset = offset, - .stream = Stream{ .writeFn = writeFn }, - }; - } - - fn writeFn(out_stream: *Stream, bytes: []const u8) Error!void { - const self = @fieldParentPtr(OutStream, "stream", out_stream); - const offset = self.offset; - self.offset += bytes.len; - return pwritev(self.allocator, self.fd, [_][]const u8{bytes}, offset); - } -}; - -pub const InStream = struct { - fd: fd_t, - stream: Stream, - allocator: *Allocator, - offset: usize, - - pub const Error = PReadVError; // TODO make this not have OutOfMemory - pub const Stream = event.io.InStream(Error); - - pub fn init(allocator: *Allocator, fd: fd_t, offset: usize) InStream { - return InStream{ - .fd = fd, - .offset = offset, - .stream = Stream{ .readFn = readFn }, - }; - } - - fn readFn(in_stream: *Stream, bytes: []u8) Error!usize { - const self = @fieldParentPtr(InStream, "stream", in_stream); - const amt = try preadv(self.allocator, self.fd, [_][]u8{bytes}, self.offset); - self.offset += amt; - return amt; - } -}; diff --git a/lib/std/io.zig b/lib/std/io.zig index a0e58c373d..341b73e33c 100644 --- a/lib/std/io.zig +++ b/lib/std/io.zig @@ -47,7 +47,10 @@ fn getStdOutHandle() os.fd_t { } pub fn getStdOut() File { - return File.openHandle(getStdOutHandle()); + return File{ + .handle = getStdOutHandle(), + .io_mode = .blocking, + }; } fn getStdErrHandle() os.fd_t { @@ -63,7 +66,11 @@ fn getStdErrHandle() os.fd_t { } pub fn getStdErr() File { - return File.openHandle(getStdErrHandle()); + return File{ + .handle = getStdErrHandle(), + .io_mode = .blocking, + .async_block_allowed = File.async_block_allowed_yes, + }; } fn getStdInHandle() os.fd_t { @@ -79,7 +86,10 @@ fn getStdInHandle() os.fd_t { } pub fn getStdIn() File { - return File.openHandle(getStdInHandle()); + return File{ + .handle = getStdInHandle(), + .io_mode = .blocking, + }; } pub const SeekableStream = @import("io/seekable_stream.zig").SeekableStream; diff --git a/lib/std/io/out_stream.zig b/lib/std/io/out_stream.zig index 265be066a1..7f534865f5 100644 --- a/lib/std/io/out_stream.zig +++ b/lib/std/io/out_stream.zig @@ -9,14 +9,11 @@ pub const stack_size: usize = if (@hasDecl(root, "stack_size_std_io_OutStream")) else default_stack_size; -/// TODO this is not integrated with evented I/O yet. -/// https://github.com/ziglang/zig/issues/3557 pub fn OutStream(comptime WriteError: type) type { return struct { const Self = @This(); pub const Error = WriteError; - // TODO https://github.com/ziglang/zig/issues/3557 - pub const WriteFn = if (std.io.is_async and false) + pub const WriteFn = if (std.io.is_async) async fn (self: *Self, bytes: []const u8) Error!void else fn (self: *Self, bytes: []const u8) Error!void; @@ -24,8 +21,7 @@ pub fn OutStream(comptime WriteError: type) type { writeFn: WriteFn, pub fn write(self: *Self, bytes: []const u8) Error!void { - // TODO https://github.com/ziglang/zig/issues/3557 - if (std.io.is_async and false) { + if (std.io.is_async) { // Let's not be writing 0xaa in safe modes for upwards of 4 MiB for every stream write. @setRuntimeSafety(false); var stack_frame: [stack_size]u8 align(std.Target.stack_align) = undefined; @@ -36,12 +32,12 @@ pub fn OutStream(comptime WriteError: type) type { } pub fn print(self: *Self, comptime format: []const u8, args: var) Error!void { - return std.fmt.format(self, Error, self.writeFn, format, args); + return std.fmt.format(self, Error, write, format, args); } pub fn writeByte(self: *Self, byte: u8) Error!void { - const slice = @as(*const [1]u8, &byte)[0..]; - return self.writeFn(self, slice); + const array = [1]u8{byte}; + return self.write(&array); } pub fn writeByteNTimes(self: *Self, byte: u8, n: usize) Error!void { @@ -51,7 +47,7 @@ pub fn OutStream(comptime WriteError: type) type { var remaining: usize = n; while (remaining > 0) { const to_write = std.math.min(remaining, bytes.len); - try self.writeFn(self, bytes[0..to_write]); + try self.write(bytes[0..to_write]); remaining -= to_write; } } @@ -60,32 +56,32 @@ pub fn OutStream(comptime WriteError: type) type { pub fn writeIntNative(self: *Self, comptime T: type, value: T) Error!void { var bytes: [(T.bit_count + 7) / 8]u8 = undefined; mem.writeIntNative(T, &bytes, value); - return self.writeFn(self, &bytes); + return self.write(&bytes); } /// Write a foreign-endian integer. pub fn writeIntForeign(self: *Self, comptime T: type, value: T) Error!void { var bytes: [(T.bit_count + 7) / 8]u8 = undefined; mem.writeIntForeign(T, &bytes, value); - return self.writeFn(self, &bytes); + return self.write(&bytes); } pub fn writeIntLittle(self: *Self, comptime T: type, value: T) Error!void { var bytes: [(T.bit_count + 7) / 8]u8 = undefined; mem.writeIntLittle(T, &bytes, value); - return self.writeFn(self, &bytes); + return self.write(&bytes); } pub fn writeIntBig(self: *Self, comptime T: type, value: T) Error!void { var bytes: [(T.bit_count + 7) / 8]u8 = undefined; mem.writeIntBig(T, &bytes, value); - return self.writeFn(self, &bytes); + return self.write(&bytes); } pub fn writeInt(self: *Self, comptime T: type, value: T, endian: builtin.Endian) Error!void { var bytes: [(T.bit_count + 7) / 8]u8 = undefined; mem.writeInt(T, &bytes, value, endian); - return self.writeFn(self, &bytes); + return self.write(&bytes); } }; } diff --git a/lib/std/linked_list.zig b/lib/std/linked_list.zig index a21c9a83eb..23201dbf94 100644 --- a/lib/std/linked_list.zig +++ b/lib/std/linked_list.zig @@ -18,12 +18,11 @@ pub fn SinglyLinkedList(comptime T: type) type { /// Node inside the linked list wrapping the actual data. pub const Node = struct { - next: ?*Node, + next: ?*Node = null, data: T, pub fn init(data: T) Node { return Node{ - .next = null, .data = data, }; } @@ -196,14 +195,12 @@ pub fn TailQueue(comptime T: type) type { /// Node inside the linked list wrapping the actual data. pub const Node = struct { - prev: ?*Node, - next: ?*Node, + prev: ?*Node = null, + next: ?*Node = null, data: T, pub fn init(data: T) Node { return Node{ - .prev = null, - .next = null, .data = data, }; } diff --git a/lib/std/net.zig b/lib/std/net.zig index c113462855..27056bf1db 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -271,7 +271,7 @@ pub const Address = extern union { options: std.fmt.FormatOptions, context: var, comptime Errors: type, - output: fn (@TypeOf(context), []const u8) Errors!void, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, ) !void { switch (self.any.family) { os.AF_INET => { @@ -361,7 +361,7 @@ pub const Address = extern union { }; pub fn connectUnixSocket(path: []const u8) !fs.File { - const opt_non_block = if (std.io.mode == .evented) os.SOCK_NONBLOCK else 0; + const opt_non_block = if (std.io.is_async) os.SOCK_NONBLOCK else 0; const sockfd = try os.socket( os.AF_UNIX, os.SOCK_STREAM | os.SOCK_CLOEXEC | opt_non_block, @@ -377,7 +377,10 @@ pub fn connectUnixSocket(path: []const u8) !fs.File { addr.getOsSockLen(), ); - return fs.File.openHandle(sockfd); + return fs.File{ + .handle = sockfd, + .io_mode = std.io.mode, + }; } pub const AddressList = struct { @@ -412,7 +415,7 @@ pub fn tcpConnectToAddress(address: Address) !fs.File { errdefer os.close(sockfd); try os.connect(sockfd, &address.any, address.getOsSockLen()); - return fs.File{ .handle = sockfd }; + return fs.File{ .handle = sockfd, .io_mode = std.io.mode }; } /// Call `AddressList.deinit` on the result. @@ -1379,7 +1382,10 @@ pub const StreamServer = struct { var adr_len: os.socklen_t = @sizeOf(Address); if (os.accept4(self.sockfd.?, &accepted_addr.any, &adr_len, accept_flags)) |fd| { return Connection{ - .file = fs.File.openHandle(fd), + .file = fs.File{ + .handle = fd, + .io_mode = std.io.mode, + }, .address = accepted_addr, }; } else |err| switch (err) { diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig index 2dd11b391e..703a804bfb 100644 --- a/lib/std/net/test.zig +++ b/lib/std/net/test.zig @@ -81,17 +81,15 @@ test "resolve DNS" { } test "listen on a port, send bytes, receive bytes" { + if (!std.io.is_async) return error.SkipZigTest; + if (std.builtin.os != .linux) { // TODO build abstractions for other operating systems return error.SkipZigTest; } - if (std.io.mode != .evented) { - // TODO add ability to run tests in non-blocking I/O mode - return error.SkipZigTest; - } // TODO doing this at comptime crashed the compiler - const localhost = net.Address.parseIp("127.0.0.1", 0); + const localhost = try net.Address.parseIp("127.0.0.1", 0); var server = net.StreamServer.init(net.StreamServer.Options{}); defer server.deinit(); diff --git a/lib/std/os.zig b/lib/std/os.zig index ffbf92572c..73b6cbb302 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -169,7 +169,12 @@ fn getRandomBytesDevURandom(buf: []u8) !void { return error.NoDevice; } - const stream = &std.fs.File.openHandle(fd).inStream().stream; + const file = std.fs.File{ + .handle = fd, + .io_mode = .blocking, + .async_block_allowed = std.fs.File.async_block_allowed_yes, + }; + const stream = &file.inStream().stream; stream.readNoEof(buf) catch return error.Unexpected; } @@ -293,7 +298,7 @@ pub const ReadError = error{ /// via the event loop. Otherwise EAGAIN results in error.WouldBlock. pub fn read(fd: fd_t, buf: []u8) ReadError!usize { if (builtin.os == .windows) { - return windows.ReadFile(fd, buf); + return windows.ReadFile(fd, buf, null); } if (builtin.os == .wasi and !builtin.link_libc) { @@ -335,9 +340,37 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { } /// Number of bytes read is returned. Upon reading end-of-file, zero is returned. -/// If the application has a global event loop enabled, EAGAIN is handled -/// via the event loop. Otherwise EAGAIN results in error.WouldBlock. +/// +/// For POSIX systems, if the application has a global event loop enabled, EAGAIN is handled +/// via the event loop. Otherwise EAGAIN results in `error.WouldBlock`. +/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are +/// used to perform the I/O. `error.WouldBlock` is not possible on Windows. +/// +/// This operation is non-atomic on the following systems: +/// * Windows +/// On these systems, the read races with concurrent writes to the same file descriptor. pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { + if (builtin.os == .windows) { + // TODO batch these into parallel requests + var off: usize = 0; + var iov_i: usize = 0; + var inner_off: usize = 0; + while (true) { + const v = iov[iov_i]; + const amt_read = try read(fd, v.iov_base[inner_off .. v.iov_len - inner_off]); + off += amt_read; + inner_off += amt_read; + if (inner_off == v.len) { + iov_i += 1; + inner_off = 0; + if (iov_i == iov.len) { + return off; + } + } + if (amt_read == 0) return off; // EOF + } else unreachable; // TODO https://github.com/ziglang/zig/issues/707 + } + while (true) { // TODO handle the case when iov_len is too large and get rid of this @intCast const rc = system.readv(fd, iov.ptr, @intCast(u32, iov.len)); @@ -363,8 +396,56 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { } /// Number of bytes read is returned. Upon reading end-of-file, zero is returned. -/// If the application has a global event loop enabled, EAGAIN is handled -/// via the event loop. Otherwise EAGAIN results in error.WouldBlock. +/// +/// Retries when interrupted by a signal. +/// +/// For POSIX systems, if the application has a global event loop enabled, EAGAIN is handled +/// via the event loop. Otherwise EAGAIN results in `error.WouldBlock`. +/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are +/// used to perform the I/O. `error.WouldBlock` is not possible on Windows. +pub fn pread(fd: fd_t, buf: []u8, offset: u64) ReadError!usize { + if (builtin.os == .windows) { + return windows.ReadFile(fd, buf, offset); + } + + while (true) { + const rc = system.pread(fd, buf.ptr, buf.len, offset); + switch (errno(rc)) { + 0 => return @intCast(usize, rc), + EINTR => continue, + EINVAL => unreachable, + EFAULT => unreachable, + EAGAIN => if (std.event.Loop.instance) |loop| { + loop.waitUntilFdReadable(fd); + continue; + } else { + return error.WouldBlock; + }, + EBADF => unreachable, // Always a race condition. + EIO => return error.InputOutput, + EISDIR => return error.IsDir, + ENOBUFS => return error.SystemResources, + ENOMEM => return error.SystemResources, + ECONNRESET => return error.ConnectionResetByPeer, + else => |err| return unexpectedErrno(err), + } + } + return index; +} + +/// Number of bytes read is returned. Upon reading end-of-file, zero is returned. +/// +/// Retries when interrupted by a signal. +/// +/// For POSIX systems, if the application has a global event loop enabled, EAGAIN is handled +/// via the event loop. Otherwise EAGAIN results in `error.WouldBlock`. +/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are +/// used to perform the I/O. `error.WouldBlock` is not possible on Windows. +/// +/// This operation is non-atomic on the following systems: +/// * Darwin +/// * Windows +/// On these systems, the read races with concurrent writes to the same file descriptor. pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) ReadError!usize { if (comptime std.Target.current.isDarwin()) { // Darwin does not have preadv but it does have pread. @@ -409,6 +490,28 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) ReadError!usize { } } } + + if (builtin.os == .windows) { + // TODO batch these into parallel requests + var off: usize = 0; + var iov_i: usize = 0; + var inner_off: usize = 0; + while (true) { + const v = iov[iov_i]; + const amt_read = try pread(fd, v.iov_base[inner_off .. v.iov_len - inner_off], offset + off); + off += amt_read; + inner_off += amt_read; + if (inner_off == v.len) { + iov_i += 1; + inner_off = 0; + if (iov_i == iov.len) { + return off; + } + } + if (amt_read == 0) return off; // EOF + } else unreachable; // TODO https://github.com/ziglang/zig/issues/707 + } + while (true) { // TODO handle the case when iov_len is too large and get rid of this @intCast const rc = system.preadv(fd, iov.ptr, @intCast(u32, iov.len), offset); @@ -451,11 +554,9 @@ pub const WriteError = error{ /// Write to a file descriptor. Keeps trying if it gets interrupted. /// If the application has a global event loop enabled, EAGAIN is handled /// via the event loop. Otherwise EAGAIN results in error.WouldBlock. -/// TODO evented I/O integration is disabled until -/// https://github.com/ziglang/zig/issues/3557 is solved. pub fn write(fd: fd_t, bytes: []const u8) WriteError!void { if (builtin.os == .windows) { - return windows.WriteFile(fd, bytes); + return windows.WriteFile(fd, bytes, null); } if (builtin.os == .wasi and !builtin.link_libc) { @@ -488,14 +589,12 @@ pub fn write(fd: fd_t, bytes: []const u8) WriteError!void { EINTR => continue, EINVAL => unreachable, EFAULT => unreachable, - // TODO https://github.com/ziglang/zig/issues/3557 - EAGAIN => return error.WouldBlock, - //EAGAIN => if (std.event.Loop.instance) |loop| { - // loop.waitUntilFdWritable(fd); - // continue; - //} else { - // return error.WouldBlock; - //}, + EAGAIN => if (std.event.Loop.instance) |loop| { + loop.waitUntilFdWritable(fd); + continue; + } else { + return error.WouldBlock; + }, EBADF => unreachable, // Always a race condition. EDESTADDRREQ => unreachable, // `connect` was never called. EDQUOT => return error.DiskQuota, @@ -540,8 +639,57 @@ pub fn writev(fd: fd_t, iov: []const iovec_const) WriteError!void { } } +/// Write to a file descriptor, with a position offset. +/// +/// Retries when interrupted by a signal. +/// +/// For POSIX systems, if the application has a global event loop enabled, EAGAIN is handled +/// via the event loop. Otherwise EAGAIN results in `error.WouldBlock`. +/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are +/// used to perform the I/O. `error.WouldBlock` is not possible on Windows. +pub fn pwrite(fd: fd_t, bytes: []const u8, offset: u64) WriteError!void { + if (comptime std.Target.current.isWindows()) { + return windows.WriteFile(fd, bytes, offset); + } + + while (true) { + const rc = system.pwrite(fd, bytes.ptr, bytes.len, offset); + switch (errno(rc)) { + 0 => return, + EINTR => continue, + EINVAL => unreachable, + EFAULT => unreachable, + EAGAIN => if (std.event.Loop.instance) |loop| { + loop.waitUntilFdWritable(fd); + continue; + } else { + return error.WouldBlock; + }, + EBADF => unreachable, // Always a race condition. + EDESTADDRREQ => unreachable, // `connect` was never called. + EDQUOT => return error.DiskQuota, + EFBIG => return error.FileTooBig, + EIO => return error.InputOutput, + ENOSPC => return error.NoSpaceLeft, + EPERM => return error.AccessDenied, + EPIPE => return error.BrokenPipe, + else => |err| return unexpectedErrno(err), + } + } +} + /// Write multiple buffers to a file descriptor, with a position offset. -/// Keeps trying if it gets interrupted. +/// +/// Retries when interrupted by a signal. +/// +/// If the application has a global event loop enabled, EAGAIN is handled +/// via the event loop. Otherwise EAGAIN results in `error.WouldBlock`. +/// +/// This operation is non-atomic on the following systems: +/// * Darwin +/// * Windows +/// On these systems, the write races with concurrent writes to the same file descriptor, and +/// the file can be in a partially written state when an error occurs. pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) WriteError!void { if (comptime std.Target.current.isDarwin()) { // Darwin does not have pwritev but it does have pwrite. @@ -589,6 +737,15 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) WriteError!void } } + if (comptime std.Target.current.isWindows()) { + var off = offset; + for (iov) |item| { + try pwrite(fd, item.iov_base[0..item.iov_len], off); + off += buf.len; + } + return; + } + while (true) { // TODO handle the case when iov_len is too large and get rid of this @intCast const rc = system.pwritev(fd, iov.ptr, @intCast(u32, iov.len), offset); @@ -694,7 +851,7 @@ pub fn openC(file_path: [*:0]const u8, flags: u32, perm: usize) OpenError!fd_t { /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openatC`. -pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: usize) OpenError!fd_t { +pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: mode_t) OpenError!fd_t { const file_path_c = try toPosixPath(file_path); return openatC(dir_fd, &file_path_c, flags, mode); } @@ -702,7 +859,7 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: u32, mode: usize) Open /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// See also `openat`. -pub fn openatC(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: usize) OpenError!fd_t { +pub fn openatC(dir_fd: fd_t, file_path: [*:0]const u8, flags: u32, mode: mode_t) OpenError!fd_t { while (true) { const rc = system.openat(dir_fd, file_path, flags, mode); switch (errno(rc)) { @@ -2372,6 +2529,22 @@ pub fn pipe() PipeError![2]fd_t { } pub fn pipe2(flags: u32) PipeError![2]fd_t { + if (comptime std.Target.current.isDarwin()) { + var fds: [2]fd_t = try pipe(); + if (flags == 0) return fds; + errdefer { + close(fds[0]); + close(fds[1]); + } + for (fds) |fd| switch (errno(system.fcntl(fd, F_SETFL, flags))) { + 0 => {}, + EINVAL => unreachable, // Invalid flags + EBADF => unreachable, // Always a race condition + else => |err| return unexpectedErrno(err), + }; + return fds; + } + var fds: [2]fd_t = undefined; switch (errno(system.pipe2(&fds, flags))) { 0 => return fds, diff --git a/lib/std/os/bits/darwin.zig b/lib/std/os/bits/darwin.zig index d4340443e5..22897974c2 100644 --- a/lib/std/os/bits/darwin.zig +++ b/lib/std/os/bits/darwin.zig @@ -4,6 +4,7 @@ const maxInt = std.math.maxInt; pub const fd_t = c_int; pub const pid_t = c_int; +pub const mode_t = c_uint; pub const in_port_t = u16; pub const sa_family_t = u8; @@ -1223,3 +1224,161 @@ pub const RTLD_NEXT = @intToPtr(*c_void, ~maxInt(usize)); pub const RTLD_DEFAULT = @intToPtr(*c_void, ~maxInt(usize) - 1); pub const RTLD_SELF = @intToPtr(*c_void, ~maxInt(usize) - 2); pub const RTLD_MAIN_ONLY = @intToPtr(*c_void, ~maxInt(usize) - 4); + +/// duplicate file descriptor +pub const F_DUPFD = 0; + +/// get file descriptor flags +pub const F_GETFD = 1; + +/// set file descriptor flags +pub const F_SETFD = 2; + +/// get file status flags +pub const F_GETFL = 3; + +/// set file status flags +pub const F_SETFL = 4; + +/// get SIGIO/SIGURG proc/pgrp +pub const F_GETOWN = 5; + +/// set SIGIO/SIGURG proc/pgrp +pub const F_SETOWN = 6; + +/// get record locking information +pub const F_GETLK = 7; + +/// set record locking information +pub const F_SETLK = 8; + +/// F_SETLK; wait if blocked +pub const F_SETLKW = 9; + +/// F_SETLK; wait if blocked, return on timeout +pub const F_SETLKWTIMEOUT = 10; +pub const F_FLUSH_DATA = 40; + +/// Used for regression test +pub const F_CHKCLEAN = 41; + +/// Preallocate storage +pub const F_PREALLOCATE = 42; + +/// Truncate a file without zeroing space +pub const F_SETSIZE = 43; + +/// Issue an advisory read async with no copy to user +pub const F_RDADVISE = 44; + +/// turn read ahead off/on for this fd +pub const F_RDAHEAD = 45; + +/// turn data caching off/on for this fd +pub const F_NOCACHE = 48; + +/// file offset to device offset +pub const F_LOG2PHYS = 49; + +/// return the full path of the fd +pub const F_GETPATH = 50; + +/// fsync + ask the drive to flush to the media +pub const F_FULLFSYNC = 51; + +/// find which component (if any) is a package +pub const F_PATHPKG_CHECK = 52; + +/// "freeze" all fs operations +pub const F_FREEZE_FS = 53; + +/// "thaw" all fs operations +pub const F_THAW_FS = 54; + +/// turn data caching off/on (globally) for this file +pub const F_GLOBAL_NOCACHE = 55; + +/// add detached signatures +pub const F_ADDSIGS = 59; + +/// add signature from same file (used by dyld for shared libs) +pub const F_ADDFILESIGS = 61; + +/// used in conjunction with F_NOCACHE to indicate that DIRECT, synchonous writes +/// should not be used (i.e. its ok to temporaily create cached pages) +pub const F_NODIRECT = 62; + +///Get the protection class of a file from the EA, returns int +pub const F_GETPROTECTIONCLASS = 63; + +///Set the protection class of a file for the EA, requires int +pub const F_SETPROTECTIONCLASS = 64; + +///file offset to device offset, extended +pub const F_LOG2PHYS_EXT = 65; + +///get record locking information, per-process +pub const F_GETLKPID = 66; + +///Mark the file as being the backing store for another filesystem +pub const F_SETBACKINGSTORE = 70; + +///return the full path of the FD, but error in specific mtmd circumstances +pub const F_GETPATH_MTMINFO = 71; + +///Returns the code directory, with associated hashes, to the caller +pub const F_GETCODEDIR = 72; + +///No SIGPIPE generated on EPIPE +pub const F_SETNOSIGPIPE = 73; + +///Status of SIGPIPE for this fd +pub const F_GETNOSIGPIPE = 74; + +///For some cases, we need to rewrap the key for AKS/MKB +pub const F_TRANSCODEKEY = 75; + +///file being written to a by single writer... if throttling enabled, writes +///may be broken into smaller chunks with throttling in between +pub const F_SINGLE_WRITER = 76; + +///Get the protection version number for this filesystem +pub const F_GETPROTECTIONLEVEL = 77; + +///Add detached code signatures (used by dyld for shared libs) +pub const F_FINDSIGS = 78; + +///Add signature from same file, only if it is signed by Apple (used by dyld for simulator) +pub const F_ADDFILESIGS_FOR_DYLD_SIM = 83; + +///fsync + issue barrier to drive +pub const F_BARRIERFSYNC = 85; + +///Add signature from same file, return end offset in structure on success +pub const F_ADDFILESIGS_RETURN = 97; + +///Check if Library Validation allows this Mach-O file to be mapped into the calling process +pub const F_CHECK_LV = 98; + +///Deallocate a range of the file +pub const F_PUNCHHOLE = 99; + +///Trim an active file +pub const F_TRIM_ACTIVE_FILE = 100; + +pub const FCNTL_FS_SPECIFIC_BASE = 0x00010000; + +///mark the dup with FD_CLOEXEC +pub const F_DUPFD_CLOEXEC = 67; + +///close-on-exec flag +pub const FD_CLOEXEC = 1; + +/// shared or read lock +pub const F_RDLCK = 1; + +/// unlock +pub const F_UNLCK = 2; + +/// exclusive or write lock +pub const F_WRLCK = 3; diff --git a/lib/std/os/bits/dragonfly.zig b/lib/std/os/bits/dragonfly.zig index 750ca9dff0..c6c23affa7 100644 --- a/lib/std/os/bits/dragonfly.zig +++ b/lib/std/os/bits/dragonfly.zig @@ -7,6 +7,7 @@ pub fn S_ISCHR(m: u32) bool { pub const fd_t = c_int; pub const pid_t = c_int; pub const off_t = c_long; +pub const mode_t = c_uint; pub const ENOTSUP = EOPNOTSUPP; pub const EWOULDBLOCK = EAGAIN; diff --git a/lib/std/os/bits/freebsd.zig b/lib/std/os/bits/freebsd.zig index 66540433e4..b7d14934f5 100644 --- a/lib/std/os/bits/freebsd.zig +++ b/lib/std/os/bits/freebsd.zig @@ -3,6 +3,7 @@ const maxInt = std.math.maxInt; pub const fd_t = c_int; pub const pid_t = c_int; +pub const mode_t = c_uint; pub const socklen_t = u32; diff --git a/lib/std/os/bits/linux/x86_64.zig b/lib/std/os/bits/linux/x86_64.zig index da3caf2c88..608f74e2d3 100644 --- a/lib/std/os/bits/linux/x86_64.zig +++ b/lib/std/os/bits/linux/x86_64.zig @@ -12,6 +12,8 @@ const socklen_t = linux.socklen_t; const iovec = linux.iovec; const iovec_const = linux.iovec_const; +pub const mode_t = usize; + pub const SYS_read = 0; pub const SYS_write = 1; pub const SYS_open = 2; diff --git a/lib/std/os/bits/netbsd.zig b/lib/std/os/bits/netbsd.zig index 2f2494d4ce..89e0998d6d 100644 --- a/lib/std/os/bits/netbsd.zig +++ b/lib/std/os/bits/netbsd.zig @@ -3,6 +3,7 @@ const maxInt = std.math.maxInt; pub const fd_t = c_int; pub const pid_t = c_int; +pub const mode_t = c_uint; /// Renamed from `kevent` to `Kevent` to avoid conflict with function name. pub const Kevent = extern struct { diff --git a/lib/std/os/bits/wasi.zig b/lib/std/os/bits/wasi.zig index 139418ded2..f56e53504e 100644 --- a/lib/std/os/bits/wasi.zig +++ b/lib/std/os/bits/wasi.zig @@ -130,6 +130,7 @@ pub const EVENTTYPE_FD_WRITE: eventtype_t = 2; pub const exitcode_t = u32; pub const fd_t = u32; +pub const mode_t = u32; pub const fdflags_t = u16; pub const FDFLAG_APPEND: fdflags_t = 0x0001; diff --git a/lib/std/os/bits/windows.zig b/lib/std/os/bits/windows.zig index 2998049577..ba2725e0a9 100644 --- a/lib/std/os/bits/windows.zig +++ b/lib/std/os/bits/windows.zig @@ -5,6 +5,7 @@ const ws2_32 = @import("../windows/ws2_32.zig"); pub const fd_t = HANDLE; pub const pid_t = HANDLE; +pub const mode_t = u0; pub const PATH_MAX = 260; diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index b48d0e50b8..cc0d446b12 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -344,24 +344,77 @@ pub fn FindClose(hFindFile: HANDLE) void { assert(kernel32.FindClose(hFindFile) != 0); } -pub const ReadFileError = error{Unexpected}; +pub const ReadFileError = error{ + OperationAborted, + BrokenPipe, + Unexpected, +}; -pub fn ReadFile(in_hFile: HANDLE, buffer: []u8) ReadFileError!usize { - var index: usize = 0; - while (index < buffer.len) { - const want_read_count = @intCast(DWORD, math.min(@as(DWORD, maxInt(DWORD)), buffer.len - index)); - var amt_read: DWORD = undefined; - if (kernel32.ReadFile(in_hFile, buffer.ptr + index, want_read_count, &amt_read, null) == 0) { - switch (kernel32.GetLastError()) { - .OPERATION_ABORTED => continue, - .BROKEN_PIPE => return index, - else => |err| return unexpectedError(err), +/// If buffer's length exceeds what a Windows DWORD integer can hold, it will be broken into +/// multiple non-atomic reads. +pub fn ReadFile(in_hFile: HANDLE, buffer: []u8, offset: ?u64) ReadFileError!usize { + if (std.event.Loop.instance) |loop| { + // TODO support async ReadFile with no offset + const off = offset.?; + var resume_node = std.event.Loop.ResumeNode.Basic{ + .base = .{ + .id = .Basic, + .handle = @frame(), + .overlapped = OVERLAPPED{ + .Internal = 0, + .InternalHigh = 0, + .Offset = @truncate(u32, off), + .OffsetHigh = @truncate(u32, off >> 32), + .hEvent = null, + }, + }, + }; + // TODO only call create io completion port once per fd + _ = windows.CreateIoCompletionPort(fd, loop.os_data.io_port, undefined, undefined) catch undefined; + loop.beginOneEvent(); + suspend { + // TODO handle buffer bigger than DWORD can hold + _ = windows.kernel32.ReadFile(fd, buffer.ptr, @intCast(windows.DWORD, buffer.len), null, &resume_node.base.overlapped); + } + var bytes_transferred: windows.DWORD = undefined; + if (windows.kernel32.GetOverlappedResult(fd, &resume_node.base.overlapped, &bytes_transferred, windows.FALSE) == 0) { + switch (windows.kernel32.GetLastError()) { + .IO_PENDING => unreachable, + .OPERATION_ABORTED => return error.OperationAborted, + .BROKEN_PIPE => return error.BrokenPipe, + .HANDLE_EOF => return @as(usize, bytes_transferred), + else => |err| return windows.unexpectedError(err), } } - if (amt_read == 0) return index; - index += amt_read; + return @as(usize, bytes_transferred); + } else { + var index: usize = 0; + while (index < buffer.len) { + const want_read_count = @intCast(DWORD, math.min(@as(DWORD, maxInt(DWORD)), buffer.len - index)); + var amt_read: DWORD = undefined; + var overlapped_data: OVERLAPPED = undefined; + const overlapped: ?*OVERLAPPED = if (offset) |off| blk: { + overlapped_data = .{ + .Internal = 0, + .InternalHigh = 0, + .Offset = @truncate(u32, off + index), + .OffsetHigh = @truncate(u32, (off + index) >> 32), + .hEvent = null, + }; + break :blk &overlapped_data; + } else null; + if (kernel32.ReadFile(in_hFile, buffer.ptr + index, want_read_count, &amt_read, overlapped) == 0) { + switch (kernel32.GetLastError()) { + .OPERATION_ABORTED => continue, + .BROKEN_PIPE => return index, + else => |err| return unexpectedError(err), + } + } + if (amt_read == 0) return index; + index += amt_read; + } + return index; } - return index; } pub const WriteFileError = error{ @@ -371,20 +424,66 @@ pub const WriteFileError = error{ Unexpected, }; -/// This function is for blocking file descriptors only. For non-blocking, see -/// `WriteFileAsync`. -pub fn WriteFile(handle: HANDLE, bytes: []const u8) WriteFileError!void { - var bytes_written: DWORD = undefined; - // TODO replace this @intCast with a loop that writes all the bytes - if (kernel32.WriteFile(handle, bytes.ptr, @intCast(u32, bytes.len), &bytes_written, null) == 0) { - switch (kernel32.GetLastError()) { - .INVALID_USER_BUFFER => return error.SystemResources, - .NOT_ENOUGH_MEMORY => return error.SystemResources, - .OPERATION_ABORTED => return error.OperationAborted, - .NOT_ENOUGH_QUOTA => return error.SystemResources, - .IO_PENDING => unreachable, // this function is for blocking files only - .BROKEN_PIPE => return error.BrokenPipe, - else => |err| return unexpectedError(err), +pub fn WriteFile(handle: HANDLE, bytes: []const u8, offset: ?u64) WriteFileError!void { + if (std.event.Loop.instance) |loop| { + // TODO support async WriteFile with no offset + const off = offset.?; + var resume_node = std.event.Loop.ResumeNode.Basic{ + .base = .{ + .id = .Basic, + .handle = @frame(), + .overlapped = OVERLAPPED{ + .Internal = 0, + .InternalHigh = 0, + .Offset = @truncate(u32, off), + .OffsetHigh = @truncate(u32, off >> 32), + .hEvent = null, + }, + }, + }; + // TODO only call create io completion port once per fd + _ = CreateIoCompletionPort(fd, loop.os_data.io_port, undefined, undefined); + loop.beginOneEvent(); + suspend { + // TODO replace this @intCast with a loop that writes all the bytes + _ = kernel32.WriteFile(fd, bytes.ptr, @intCast(windows.DWORD, bytes.len), null, &resume_node.base.overlapped); + } + var bytes_transferred: windows.DWORD = undefined; + if (kernel32.GetOverlappedResult(fd, &resume_node.base.overlapped, &bytes_transferred, FALSE) == 0) { + switch (kernel32.GetLastError()) { + .IO_PENDING => unreachable, + .INVALID_USER_BUFFER => return error.SystemResources, + .NOT_ENOUGH_MEMORY => return error.SystemResources, + .OPERATION_ABORTED => return error.OperationAborted, + .NOT_ENOUGH_QUOTA => return error.SystemResources, + .BROKEN_PIPE => return error.BrokenPipe, + else => |err| return windows.unexpectedError(err), + } + } + } else { + var bytes_written: DWORD = undefined; + var overlapped_data: OVERLAPPED = undefined; + const overlapped: ?*OVERLAPPED = if (offset) |off| blk: { + overlapped_data = .{ + .Internal = 0, + .InternalHigh = 0, + .Offset = @truncate(u32, off), + .OffsetHigh = @truncate(u32, off >> 32), + .hEvent = null, + }; + break :blk &overlapped_data; + } else null; + // TODO replace this @intCast with a loop that writes all the bytes + if (kernel32.WriteFile(handle, bytes.ptr, @intCast(u32, bytes.len), &bytes_written, overlapped) == 0) { + switch (kernel32.GetLastError()) { + .INVALID_USER_BUFFER => return error.SystemResources, + .NOT_ENOUGH_MEMORY => return error.SystemResources, + .OPERATION_ABORTED => return error.OperationAborted, + .NOT_ENOUGH_QUOTA => return error.SystemResources, + .IO_PENDING => unreachable, // this function is for blocking files only + .BROKEN_PIPE => return error.BrokenPipe, + else => |err| return unexpectedError(err), + } } } } diff --git a/lib/std/special/test_runner.zig b/lib/std/special/test_runner.zig index 07c46ee43c..6dd208e3b4 100644 --- a/lib/std/special/test_runner.zig +++ b/lib/std/special/test_runner.zig @@ -2,6 +2,8 @@ const std = @import("std"); const io = std.io; const builtin = @import("builtin"); +pub const io_mode: io.Mode = builtin.test_io_mode; + pub fn main() anyerror!void { const test_fn_list = builtin.test_functions; var ok_count: usize = 0; @@ -12,6 +14,11 @@ pub fn main() anyerror!void { error.TimerUnsupported => @panic("timer unsupported"), }; + var async_frame_buffer: []align(std.Target.stack_align) u8 = undefined; + // TODO this is on the next line (using `undefined` above) because otherwise zig incorrectly + // ignores the alignment of the slice. + async_frame_buffer = &[_]u8{}; + for (test_fn_list) |test_fn, i| { std.testing.base_allocator_instance.reset(); @@ -21,7 +28,24 @@ pub fn main() anyerror!void { if (progress.terminal == null) { std.debug.warn("{}/{} {}...", .{ i + 1, test_fn_list.len, test_fn.name }); } - if (test_fn.func()) |_| { + const result = if (test_fn.async_frame_size) |size| switch (io_mode) { + .evented => blk: { + if (async_frame_buffer.len < size) { + std.heap.page_allocator.free(async_frame_buffer); + async_frame_buffer = try std.heap.page_allocator.alignedAlloc(u8, std.Target.stack_align, size); + } + const casted_fn = @ptrCast(async fn () anyerror!void, test_fn.func); + break :blk await @asyncCall(async_frame_buffer, {}, casted_fn); + }, + .blocking => { + skip_count += 1; + test_node.end(); + progress.log("{}...SKIP (async test)\n", .{test_fn.name}); + if (progress.terminal == null) std.debug.warn("SKIP (async test)\n", .{}); + continue; + }, + } else test_fn.func(); + if (result) |_| { ok_count += 1; test_node.end(); std.testing.allocator_instance.validate() catch |err| switch (err) { diff --git a/src-self-hosted/compilation.zig b/src-self-hosted/compilation.zig index 0f455fadcd..15453fabcc 100644 --- a/src-self-hosted/compilation.zig +++ b/src-self-hosted/compilation.zig @@ -29,7 +29,7 @@ const Package = @import("package.zig").Package; const link = @import("link.zig").link; const LibCInstallation = @import("libc_installation.zig").LibCInstallation; const CInt = @import("c_int.zig").CInt; -const fs = event.fs; +const fs = std.fs; const util = @import("util.zig"); const max_src_size = 2 * 1024 * 1024 * 1024; // 2 GiB @@ -442,7 +442,7 @@ pub const Compilation = struct { comp.name = try Buffer.init(comp.arena(), name); comp.llvm_triple = try util.getTriple(comp.arena(), target); comp.llvm_target = try util.llvmTargetFromTriple(comp.llvm_triple); - comp.zig_std_dir = try std.fs.path.join(comp.arena(), &[_][]const u8{ zig_lib_dir, "std" }); + comp.zig_std_dir = try fs.path.join(comp.arena(), &[_][]const u8{ zig_lib_dir, "std" }); const opt_level = switch (build_mode) { .Debug => llvm.CodeGenLevelNone, @@ -488,8 +488,8 @@ pub const Compilation = struct { defer comp.events.deinit(); if (root_src_path) |root_src| { - const dirname = std.fs.path.dirname(root_src) orelse "."; - const basename = std.fs.path.basename(root_src); + const dirname = fs.path.dirname(root_src) orelse "."; + const basename = fs.path.basename(root_src); comp.root_package = try Package.create(comp.arena(), dirname, basename); comp.std_package = try Package.create(comp.arena(), comp.zig_std_dir, "std.zig"); @@ -521,7 +521,7 @@ pub const Compilation = struct { if (comp.tmp_dir.getOrNull()) |tmp_dir_result| if (tmp_dir_result.*) |tmp_dir| { // TODO evented I/O? - std.fs.deleteTree(tmp_dir) catch {}; + fs.deleteTree(tmp_dir) catch {}; } else |_| {}; } @@ -797,7 +797,7 @@ pub const Compilation = struct { async fn rebuildFile(self: *Compilation, root_scope: *Scope.Root) BuildError!void { const tree_scope = blk: { - const source_code = fs.readFile( + const source_code = fs.cwd().readFileAlloc( self.gpa(), root_scope.realpath, max_src_size, @@ -935,8 +935,8 @@ pub const Compilation = struct { fn initialCompile(self: *Compilation) !void { if (self.root_src_path) |root_src_path| { const root_scope = blk: { - // TODO async/await std.fs.realpath - const root_src_real_path = std.fs.realpathAlloc(self.gpa(), root_src_path) catch |err| { + // TODO async/await fs.realpath + const root_src_real_path = fs.realpathAlloc(self.gpa(), root_src_path) catch |err| { try self.addCompileErrorCli(root_src_path, "unable to open: {}", .{@errorName(err)}); return; }; @@ -1157,7 +1157,7 @@ pub const Compilation = struct { const file_name = try std.fmt.allocPrint(self.gpa(), "{}{}", .{ file_prefix[0..], suffix }); defer self.gpa().free(file_name); - const full_path = try std.fs.path.join(self.gpa(), &[_][]const u8{ tmp_dir, file_name[0..] }); + const full_path = try fs.path.join(self.gpa(), &[_][]const u8{ tmp_dir, file_name[0..] }); errdefer self.gpa().free(full_path); return Buffer.fromOwnedSlice(self.gpa(), full_path); @@ -1178,8 +1178,8 @@ pub const Compilation = struct { const zig_dir_path = try getZigDir(self.gpa()); defer self.gpa().free(zig_dir_path); - const tmp_dir = try std.fs.path.join(self.arena(), &[_][]const u8{ zig_dir_path, comp_dir_name[0..] }); - try std.fs.makePath(self.gpa(), tmp_dir); + const tmp_dir = try fs.path.join(self.arena(), &[_][]const u8{ zig_dir_path, comp_dir_name[0..] }); + try fs.makePath(self.gpa(), tmp_dir); return tmp_dir; } @@ -1351,7 +1351,7 @@ async fn addFnToLinkSet(comp: *Compilation, fn_val: *Value.Fn) Compilation.Build } fn getZigDir(allocator: *mem.Allocator) ![]u8 { - return std.fs.getAppDataDir(allocator, "zig"); + return fs.getAppDataDir(allocator, "zig"); } fn analyzeFnType( diff --git a/src-self-hosted/dep_tokenizer.zig b/src-self-hosted/dep_tokenizer.zig index 9a13faa5f2..5c250cdb99 100644 --- a/src-self-hosted/dep_tokenizer.zig +++ b/src-self-hosted/dep_tokenizer.zig @@ -998,7 +998,8 @@ fn printCharValues(out: var, bytes: []const u8) !void { fn printUnderstandableChar(out: var, char: u8) !void { if (!std.ascii.isPrint(char) or char == ' ') { - std.fmt.format(out.context, anyerror, out.output, "\\x{X:2}", .{char}) catch {}; + const output = @typeInfo(@TypeOf(out)).Pointer.child.output; + std.fmt.format(out.context, anyerror, output, "\\x{X:2}", .{char}) catch {}; } else { try out.write("'"); try out.write(&[_]u8{printable_char_tab[char]}); @@ -1021,34 +1022,20 @@ comptime { // output: must be a function that takes a `self` idiom parameter // and a bytes parameter // context: must be that self -fn makeOutput(output: var, context: var) Output(@TypeOf(output)) { - return Output(@TypeOf(output)){ - .output = output, +fn makeOutput(comptime output: var, context: var) Output(output, @TypeOf(context)) { + return Output(output, @TypeOf(context)){ .context = context, }; } -fn Output(comptime T: type) type { - const args = switch (@typeInfo(T)) { - .Fn => |f| f.args, - else => @compileError("output parameter is not a function"), - }; - if (args.len != 2) { - @compileError("output function must take 2 arguments"); - } - const at0 = args[0].arg_type orelse @compileError("output arg[0] does not have a type"); - const at1 = args[1].arg_type orelse @compileError("output arg[1] does not have a type"); - const arg1p = switch (@typeInfo(at1)) { - .Pointer => |p| p, - else => @compileError("output arg[1] is not a slice"), - }; - if (arg1p.child != u8) @compileError("output arg[1] is not a u8 slice"); +fn Output(comptime output_func: var, comptime Context: type) type { return struct { - output: T, - context: at0, + context: Context, - fn write(self: *@This(), bytes: []const u8) !void { - try self.output(self.context, bytes); + pub const output = output_func; + + fn write(self: @This(), bytes: []const u8) !void { + try output_func(self.context, bytes); } }; } diff --git a/src-self-hosted/introspect.zig b/src-self-hosted/introspect.zig index 67181b40a8..3cf18ab363 100644 --- a/src-self-hosted/introspect.zig +++ b/src-self-hosted/introspect.zig @@ -14,7 +14,7 @@ pub fn testZigInstallPrefix(allocator: *mem.Allocator, test_path: []const u8) ![ const test_index_file = try fs.path.join(allocator, &[_][]const u8{ test_zig_dir, "std", "std.zig" }); defer allocator.free(test_index_file); - var file = try fs.File.openRead(test_index_file); + var file = try fs.cwd().openRead(test_index_file); file.close(); return test_zig_dir; diff --git a/src-self-hosted/main.zig b/src-self-hosted/main.zig index fc0825f3db..fbfd1e2642 100644 --- a/src-self-hosted/main.zig +++ b/src-self-hosted/main.zig @@ -724,7 +724,7 @@ async fn fmtPath(fmt: *Fmt, file_path_ref: []const u8, check_mode: bool) FmtErro if (try held.value.put(file_path, {})) |_| return; } - const source_code = event.fs.readFile( + const source_code = fs.cwd().readFileAlloc( fmt.allocator, file_path, max_src_size, diff --git a/src/all_types.hpp b/src/all_types.hpp index b7cb7e0b5a..efbed07124 100644 --- a/src/all_types.hpp +++ b/src/all_types.hpp @@ -2246,6 +2246,7 @@ struct CodeGen { bool enable_dump_analysis; bool enable_doc_generation; bool disable_bin_generation; + bool test_is_evented; CodeModel code_model; Buf *mmacosx_version_min; @@ -2491,6 +2492,9 @@ struct ScopeExpr { size_t children_len; MemoizedBool need_spill; + // This is a hack. I apologize for this, I need this to work so that I + // can make progress on other fronts. I'll pay off this tech debt eventually. + bool spill_harder; }; // synchronized with code in define_builtin_compile_vars diff --git a/src/analyze.cpp b/src/analyze.cpp index 94c24a391e..443cd95784 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -6108,11 +6108,14 @@ static void mark_suspension_point(Scope *scope) { continue; } case ScopeIdExpr: { + ScopeExpr *parent_expr_scope = reinterpret_cast(scope); if (!looking_for_exprs) { + if (parent_expr_scope->spill_harder) { + parent_expr_scope->need_spill = MemoizedBoolTrue; + } // Now we're only looking for a block, to see if it's in a loop (see the case ScopeIdBlock) continue; } - ScopeExpr *parent_expr_scope = reinterpret_cast(scope); if (child_expr_scope != nullptr) { for (size_t i = 0; parent_expr_scope->children_ptr[i] != child_expr_scope; i += 1) { assert(i < parent_expr_scope->children_len); @@ -6148,6 +6151,15 @@ static bool scope_needs_spill(Scope *scope) { zig_unreachable(); } +static ZigType *resolve_type_isf(ZigType *ty) { + if (ty->id != ZigTypeIdPointer) return ty; + InferredStructField *isf = ty->data.pointer.inferred_struct_field; + if (isf == nullptr) return ty; + TypeStructField *field = find_struct_type_field(isf->inferred_struct_type, isf->field_name); + assert(field != nullptr); + return field->type_entry; +} + static Error resolve_async_frame(CodeGen *g, ZigType *frame_type) { Error err; @@ -6249,6 +6261,9 @@ static Error resolve_async_frame(CodeGen *g, ZigType *frame_type) { } ZigFn *callee = call->fn_entry; if (callee == nullptr) { + if (call->fn_ref->value->type->data.fn.fn_type_id.cc != CallingConventionAsync) { + continue; + } add_node_error(g, call->base.base.source_node, buf_sprintf("function is not comptime-known; @asyncCall required")); return ErrorSemanticAnalyzeFail; @@ -6356,11 +6371,19 @@ static Error resolve_async_frame(CodeGen *g, ZigType *frame_type) { IrInstGen *instruction = block->instruction_list.at(instr_i); if (instruction->id == IrInstGenIdAwait || instruction->id == IrInstGenIdVarPtr || - instruction->id == IrInstGenIdAlloca) + instruction->id == IrInstGenIdAlloca || + instruction->id == IrInstGenIdSpillBegin || + instruction->id == IrInstGenIdSpillEnd) { // This instruction does its own spilling specially, or otherwise doesn't need it. continue; } + if (instruction->id == IrInstGenIdCast && + reinterpret_cast(instruction)->cast_op == CastOpNoop) + { + // The IR instruction exists only to change the type according to Zig. No spill needed. + continue; + } if (instruction->value->special != ConstValSpecialRuntime) continue; if (instruction->base.ref_count == 0) @@ -6406,7 +6429,7 @@ static Error resolve_async_frame(CodeGen *g, ZigType *frame_type) { } else { param_name = buf_sprintf("@arg%" ZIG_PRI_usize, arg_i); } - ZigType *param_type = param_info->type; + ZigType *param_type = resolve_type_isf(param_info->type); if ((err = type_resolve(g, param_type, ResolveStatusSizeKnown))) { return err; } @@ -6425,7 +6448,7 @@ static Error resolve_async_frame(CodeGen *g, ZigType *frame_type) { instruction->field_index = SIZE_MAX; ZigType *ptr_type = instruction->base.value->type; assert(ptr_type->id == ZigTypeIdPointer); - ZigType *child_type = ptr_type->data.pointer.child_type; + ZigType *child_type = resolve_type_isf(ptr_type->data.pointer.child_type); if (!type_has_bits(child_type)) continue; if (instruction->base.base.ref_count == 0) @@ -6452,8 +6475,6 @@ static Error resolve_async_frame(CodeGen *g, ZigType *frame_type) { } instruction->field_index = fields.length; - src_assert(child_type->id != ZigTypeIdPointer || child_type->data.pointer.inferred_struct_field == nullptr, - instruction->base.base.source_node); fields.append({name, child_type, instruction->align}); } @@ -8255,6 +8276,8 @@ static void resolve_llvm_types_struct(CodeGen *g, ZigType *struct_type, ResolveS size_t debug_field_index = 0; for (size_t i = 0; i < field_count; i += 1) { TypeStructField *field = struct_type->data.structure.fields[i]; + //fprintf(stderr, "%s at gen index %zu\n", buf_ptr(field->name), field->gen_index); + size_t gen_field_index = field->gen_index; if (gen_field_index == SIZE_MAX) { continue; diff --git a/src/codegen.cpp b/src/codegen.cpp index 431e20ddd3..89f868c925 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -343,33 +343,67 @@ static LLVMLinkage to_llvm_linkage(GlobalLinkageId id) { zig_unreachable(); } -// label (grep this): [fn_frame_struct_layout] -static uint32_t frame_index_trace_arg(CodeGen *g, ZigType *return_type) { - // [0] *ReturnType (callee's) - // [1] *ReturnType (awaiter's) - // [2] ReturnType - uint32_t return_field_count = type_has_bits(return_type) ? 3 : 0; - return frame_ret_start + return_field_count; -} +struct CalcLLVMFieldIndex { + uint32_t offset; + uint32_t field_index; +}; -// label (grep this): [fn_frame_struct_layout] -static uint32_t frame_index_arg(CodeGen *g, ZigType *return_type) { - bool have_stack_trace = codegen_fn_has_err_ret_tracing_arg(g, return_type); - // [0] *StackTrace (callee's) - // [1] *StackTrace (awaiter's) - uint32_t trace_field_count = have_stack_trace ? 2 : 0; - return frame_index_trace_arg(g, return_type) + trace_field_count; -} - -// label (grep this): [fn_frame_struct_layout] -static uint32_t frame_index_trace_stack(CodeGen *g, FnTypeId *fn_type_id) { - uint32_t result = frame_index_arg(g, fn_type_id->return_type); - for (size_t i = 0; i < fn_type_id->param_count; i += 1) { - if (type_has_bits(fn_type_id->param_info->type)) { - result += 1; +static void calc_llvm_field_index_add(CodeGen *g, CalcLLVMFieldIndex *calc, ZigType *ty) { + if (!type_has_bits(ty)) return; + uint32_t ty_align = get_abi_alignment(g, ty); + if (calc->offset % ty_align != 0) { + uint32_t llvm_align = LLVMABIAlignmentOfType(g->target_data_ref, get_llvm_type(g, ty)); + if (llvm_align >= ty_align) { + ty_align = llvm_align; // llvm's padding is sufficient + } else if (calc->offset) { + calc->field_index += 1; // zig will insert an extra padding field here } + calc->offset += ty_align - (calc->offset % ty_align); // padding bytes } - return result; + calc->offset += ty->abi_size; + calc->field_index += 1; +} + +// label (grep this): [fn_frame_struct_layout] +static void frame_index_trace_arg_calc(CodeGen *g, CalcLLVMFieldIndex *calc, ZigType *return_type) { + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // function pointer + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // resume index + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // awaiter index + + if (type_has_bits(return_type)) { + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // *ReturnType (callee's) + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // *ReturnType (awaiter's) + calc_llvm_field_index_add(g, calc, return_type); // ReturnType + } +} + +static uint32_t frame_index_trace_arg(CodeGen *g, ZigType *return_type) { + CalcLLVMFieldIndex calc = {0}; + frame_index_trace_arg_calc(g, &calc, return_type); + return calc.field_index; +} + +// label (grep this): [fn_frame_struct_layout] +static void frame_index_arg_calc(CodeGen *g, CalcLLVMFieldIndex *calc, ZigType *return_type) { + frame_index_trace_arg_calc(g, calc, return_type); + + if (codegen_fn_has_err_ret_tracing_arg(g, return_type)) { + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // *StackTrace (callee's) + calc_llvm_field_index_add(g, calc, g->builtin_types.entry_usize); // *StackTrace (awaiter's) + } +} + +// label (grep this): [fn_frame_struct_layout] +static uint32_t frame_index_trace_stack(CodeGen *g, ZigFn *fn) { + size_t field_index = 6; + bool have_stack_trace = codegen_fn_has_err_ret_tracing_arg(g, fn->type_entry->data.fn.fn_type_id.return_type); + if (have_stack_trace) { + field_index += 2; + } + field_index += fn->type_entry->data.fn.fn_type_id.param_count; + ZigType *locals_struct = fn->frame_type->data.frame.locals_struct; + TypeStructField *field = locals_struct->data.structure.fields[field_index]; + return field->gen_index; } @@ -2527,7 +2561,12 @@ static LLVMValueRef ir_render_return(CodeGen *g, IrExecutableGen *executable, Ir LLVMBuildRet(g->builder, by_val_value); } } else if (instruction->operand == nullptr) { - LLVMBuildRetVoid(g->builder); + if (g->cur_ret_ptr == nullptr) { + LLVMBuildRetVoid(g->builder); + } else { + LLVMValueRef by_val_value = gen_load_untyped(g, g->cur_ret_ptr, 0, false, ""); + LLVMBuildRet(g->builder, by_val_value); + } } else { LLVMValueRef value = ir_llvm_value(g, instruction->operand); LLVMBuildRet(g->builder, value); @@ -3920,7 +3959,9 @@ static void set_call_instr_sret(CodeGen *g, LLVMValueRef call_instr) { static void render_async_spills(CodeGen *g) { ZigType *fn_type = g->cur_fn->type_entry; ZigType *import = get_scope_import(&g->cur_fn->fndef_scope->base); - uint32_t async_var_index = frame_index_arg(g, fn_type->data.fn.fn_type_id.return_type); + + CalcLLVMFieldIndex arg_calc = {0}; + frame_index_arg_calc(g, &arg_calc, fn_type->data.fn.fn_type_id.return_type); for (size_t var_i = 0; var_i < g->cur_fn->variable_list.length; var_i += 1) { ZigVar *var = g->cur_fn->variable_list.at(var_i); @@ -3941,8 +3982,8 @@ static void render_async_spills(CodeGen *g) { continue; } - var->value_ref = LLVMBuildStructGEP(g->builder, g->cur_frame_ptr, async_var_index, var->name); - async_var_index += 1; + calc_llvm_field_index_add(g, &arg_calc, var->var_type); + var->value_ref = LLVMBuildStructGEP(g->builder, g->cur_frame_ptr, arg_calc.field_index - 1, var->name); if (var->decl_node) { var->di_loc_var = ZigLLVMCreateAutoVariable(g->dbuilder, get_di_scope(g, var->parent_scope), var->name, import->data.structure.root_struct->di_file, @@ -4023,6 +4064,8 @@ static void gen_init_stack_trace(CodeGen *g, LLVMValueRef trace_field_ptr, LLVMV } static LLVMValueRef ir_render_call(CodeGen *g, IrExecutableGen *executable, IrInstGenCall *instruction) { + Error err; + LLVMTypeRef usize_type_ref = g->builtin_types.entry_usize->llvm_type; LLVMValueRef fn_val; @@ -4053,6 +4096,8 @@ static LLVMValueRef ir_render_call(CodeGen *g, IrExecutableGen *executable, IrIn ZigList gen_param_types = {}; LLVMValueRef result_loc = instruction->result_loc ? ir_llvm_value(g, instruction->result_loc) : nullptr; LLVMValueRef zero = LLVMConstNull(usize_type_ref); + bool need_frame_ptr_ptr_spill = false; + ZigType *anyframe_type = nullptr; LLVMValueRef frame_result_loc_uncasted = nullptr; LLVMValueRef frame_result_loc; LLVMValueRef awaiter_init_val; @@ -4091,14 +4136,17 @@ static LLVMValueRef ir_render_call(CodeGen *g, IrExecutableGen *executable, IrIn LLVMPositionBuilderAtEnd(g->builder, ok_block); } + need_frame_ptr_ptr_spill = true; LLVMValueRef frame_ptr_ptr = LLVMBuildStructGEP(g->builder, frame_slice_ptr, slice_ptr_index, ""); LLVMValueRef frame_ptr = LLVMBuildLoad(g->builder, frame_ptr_ptr, ""); if (instruction->fn_entry == nullptr) { - ZigType *anyframe_type = get_any_frame_type(g, src_return_type); + anyframe_type = get_any_frame_type(g, src_return_type); frame_result_loc = LLVMBuildBitCast(g->builder, frame_ptr, get_llvm_type(g, anyframe_type), ""); } else { - ZigType *ptr_frame_type = get_pointer_to_type(g, - get_fn_frame_type(g, instruction->fn_entry), false); + ZigType *frame_type = get_fn_frame_type(g, instruction->fn_entry); + if ((err = type_resolve(g, frame_type, ResolveStatusLLVMFull))) + codegen_report_errors_and_exit(g); + ZigType *ptr_frame_type = get_pointer_to_type(g, frame_type, false); frame_result_loc = LLVMBuildBitCast(g->builder, frame_ptr, get_llvm_type(g, ptr_frame_type), ""); } @@ -4265,17 +4313,35 @@ static LLVMValueRef ir_render_call(CodeGen *g, IrExecutableGen *executable, IrIn LLVMValueRef result; if (callee_is_async) { - uint32_t arg_start_i = frame_index_arg(g, fn_type->data.fn.fn_type_id.return_type); + CalcLLVMFieldIndex arg_calc_start = {0}; + frame_index_arg_calc(g, &arg_calc_start, fn_type->data.fn.fn_type_id.return_type); LLVMValueRef casted_frame; if (instruction->new_stack != nullptr && instruction->fn_entry == nullptr) { // We need the frame type to be a pointer to a struct that includes the args - size_t field_count = arg_start_i + gen_param_values.length; + + // Count ahead to determine how many llvm struct fields we need. + CalcLLVMFieldIndex arg_calc = arg_calc_start; + for (size_t i = 0; i < gen_param_types.length; i += 1) { + calc_llvm_field_index_add(g, &arg_calc, gen_param_types.at(i)); + } + size_t field_count = arg_calc.field_index; + LLVMTypeRef *field_types = allocate_nonzero(field_count); LLVMGetStructElementTypes(LLVMGetElementType(LLVMTypeOf(frame_result_loc)), field_types); - assert(LLVMCountStructElementTypes(LLVMGetElementType(LLVMTypeOf(frame_result_loc))) == arg_start_i); + assert(LLVMCountStructElementTypes(LLVMGetElementType(LLVMTypeOf(frame_result_loc))) == arg_calc_start.field_index); + + arg_calc = arg_calc_start; for (size_t arg_i = 0; arg_i < gen_param_values.length; arg_i += 1) { - field_types[arg_start_i + arg_i] = LLVMTypeOf(gen_param_values.at(arg_i)); + CalcLLVMFieldIndex prev = arg_calc; + calc_llvm_field_index_add(g, &arg_calc, gen_param_types.at(arg_i)); + field_types[arg_calc.field_index - 1] = LLVMTypeOf(gen_param_values.at(arg_i)); + if (arg_calc.field_index - prev.field_index > 1) { + // Padding field + uint32_t pad_bytes = arg_calc.offset - prev.offset - gen_param_types.at(arg_i)->abi_size; + LLVMTypeRef pad_llvm_type = LLVMArrayType(LLVMInt8Type(), pad_bytes); + field_types[arg_calc.field_index - 2] = pad_llvm_type; + } } LLVMTypeRef frame_with_args_type = LLVMStructType(field_types, field_count, false); LLVMTypeRef ptr_frame_with_args_type = LLVMPointerType(frame_with_args_type, 0); @@ -4285,8 +4351,10 @@ static LLVMValueRef ir_render_call(CodeGen *g, IrExecutableGen *executable, IrIn casted_frame = frame_result_loc; } + CalcLLVMFieldIndex arg_calc = arg_calc_start; for (size_t arg_i = 0; arg_i < gen_param_values.length; arg_i += 1) { - LLVMValueRef arg_ptr = LLVMBuildStructGEP(g->builder, casted_frame, arg_start_i + arg_i, ""); + calc_llvm_field_index_add(g, &arg_calc, gen_param_types.at(arg_i)); + LLVMValueRef arg_ptr = LLVMBuildStructGEP(g->builder, casted_frame, arg_calc.field_index - 1, ""); gen_assign_raw(g, arg_ptr, get_pointer_to_type(g, gen_param_types.at(arg_i), true), gen_param_values.at(arg_i)); } @@ -4349,11 +4417,19 @@ static LLVMValueRef ir_render_call(CodeGen *g, IrExecutableGen *executable, IrIn } } - if (frame_result_loc_uncasted != nullptr && instruction->fn_entry != nullptr) { - // Instead of a spill, we do the bitcast again. The uncasted LLVM IR instruction will - // be an Alloca from the entry block, so it does not need to be spilled. - frame_result_loc = LLVMBuildBitCast(g->builder, frame_result_loc_uncasted, - LLVMPointerType(get_llvm_type(g, instruction->fn_entry->frame_type), 0), ""); + if (need_frame_ptr_ptr_spill) { + LLVMValueRef frame_slice_ptr = ir_llvm_value(g, instruction->new_stack); + LLVMValueRef frame_ptr_ptr = LLVMBuildStructGEP(g->builder, frame_slice_ptr, slice_ptr_index, ""); + frame_result_loc_uncasted = LLVMBuildLoad(g->builder, frame_ptr_ptr, ""); + } + if (frame_result_loc_uncasted != nullptr) { + if (instruction->fn_entry != nullptr) { + frame_result_loc = LLVMBuildBitCast(g->builder, frame_result_loc_uncasted, + LLVMPointerType(get_llvm_type(g, instruction->fn_entry->frame_type), 0), ""); + } else { + frame_result_loc = LLVMBuildBitCast(g->builder, frame_result_loc_uncasted, + get_llvm_type(g, anyframe_type), ""); + } } LLVMValueRef result_ptr = LLVMBuildStructGEP(g->builder, frame_result_loc, frame_ret_start + 2, ""); @@ -5644,18 +5720,24 @@ static LLVMValueRef ir_render_unwrap_err_payload(CodeGen *g, IrExecutableGen *ex bool want_safety = instruction->safety_check_on && ir_want_runtime_safety(g, &instruction->base) && g->errors_by_index.length > 1; - bool value_has_bits; - if ((err = type_has_bits2(g, instruction->base.value->type, &value_has_bits))) - codegen_report_errors_and_exit(g); - - if (!want_safety && !value_has_bits) - return nullptr; - ZigType *ptr_type = instruction->value->value->type; assert(ptr_type->id == ZigTypeIdPointer); ZigType *err_union_type = ptr_type->data.pointer.child_type; ZigType *payload_type = err_union_type->data.error_union.payload_type; LLVMValueRef err_union_ptr = ir_llvm_value(g, instruction->value); + + LLVMValueRef zero = LLVMConstNull(get_llvm_type(g, g->err_tag_type)); + bool value_has_bits; + if ((err = type_has_bits2(g, instruction->base.value->type, &value_has_bits))) + codegen_report_errors_and_exit(g); + if (!want_safety && !value_has_bits) { + if (instruction->initializing) { + gen_store_untyped(g, zero, err_union_ptr, 0, false); + } + return nullptr; + } + + LLVMValueRef err_union_handle = get_handle_value(g, err_union_ptr, err_union_type, ptr_type); if (!type_has_bits(err_union_type->data.error_union.err_set_type)) { @@ -5670,7 +5752,6 @@ static LLVMValueRef ir_render_unwrap_err_payload(CodeGen *g, IrExecutableGen *ex } else { err_val = err_union_handle; } - LLVMValueRef zero = LLVMConstNull(get_llvm_type(g, g->err_tag_type)); LLVMValueRef cond_val = LLVMBuildICmp(g->builder, LLVMIntEQ, err_val, zero, ""); LLVMBasicBlockRef err_block = LLVMAppendBasicBlock(g->cur_fn_val, "UnwrapErrError"); LLVMBasicBlockRef ok_block = LLVMAppendBasicBlock(g->cur_fn_val, "UnwrapErrOk"); @@ -5690,6 +5771,9 @@ static LLVMValueRef ir_render_unwrap_err_payload(CodeGen *g, IrExecutableGen *ex } return LLVMBuildStructGEP(g->builder, err_union_handle, err_union_payload_index, ""); } else { + if (instruction->initializing) { + gen_store_untyped(g, zero, err_union_ptr, 0, false); + } return nullptr; } } @@ -7742,7 +7826,7 @@ static void do_code_gen(CodeGen *g) { } uint32_t trace_field_index_stack = UINT32_MAX; if (codegen_fn_has_err_ret_tracing_stack(g, fn_table_entry, true)) { - trace_field_index_stack = frame_index_trace_stack(g, fn_type_id); + trace_field_index_stack = frame_index_trace_stack(g, fn_table_entry); g->cur_err_ret_trace_val_stack = LLVMBuildStructGEP(g->builder, g->cur_frame_ptr, trace_field_index_stack, ""); } @@ -8602,6 +8686,9 @@ Buf *codegen_generate_builtin_source(CodeGen *g) { buf_appendf(contents, "pub var test_functions: []TestFn = undefined; // overwritten later\n" ); + + buf_appendf(contents, "pub const test_io_mode = %s;\n", + g->test_is_evented ? ".evented" : ".blocking"); } return contents; @@ -8635,6 +8722,7 @@ static Error define_builtin_compile_vars(CodeGen *g) { cache_bool(&cache_hash, g->is_dynamic); cache_bool(&cache_hash, g->is_test_build); cache_bool(&cache_hash, g->is_single_threaded); + cache_bool(&cache_hash, g->test_is_evented); cache_int(&cache_hash, g->code_model); cache_int(&cache_hash, g->zig_target->is_native); cache_int(&cache_hash, g->zig_target->arch); @@ -9392,22 +9480,13 @@ static void update_test_functions_builtin_decl(CodeGen *g) { for (size_t i = 0; i < g->test_fns.length; i += 1) { ZigFn *test_fn_entry = g->test_fns.at(i); - if (fn_is_async(test_fn_entry)) { - ErrorMsg *msg = add_node_error(g, test_fn_entry->proto_node, - buf_create_from_str("test functions cannot be async")); - add_error_note(g, msg, test_fn_entry->proto_node, - buf_sprintf("this restriction may be lifted in the future. See https://github.com/ziglang/zig/issues/3117 for more details")); - add_async_error_notes(g, msg, test_fn_entry); - continue; - } - ZigValue *this_val = &test_fn_array->data.x_array.data.s_none.elements[i]; this_val->special = ConstValSpecialStatic; this_val->type = struct_type; this_val->parent.id = ConstParentIdArray; this_val->parent.data.p_array.array_val = test_fn_array; this_val->parent.data.p_array.elem_index = i; - this_val->data.x_struct.fields = alloc_const_vals_ptrs(2); + this_val->data.x_struct.fields = alloc_const_vals_ptrs(3); ZigValue *name_field = this_val->data.x_struct.fields[0]; ZigValue *name_array_val = create_const_str_lit(g, &test_fn_entry->symbol_name)->data.x_ptr.data.ref.pointee; @@ -9419,6 +9498,19 @@ static void update_test_functions_builtin_decl(CodeGen *g) { fn_field->data.x_ptr.special = ConstPtrSpecialFunction; fn_field->data.x_ptr.mut = ConstPtrMutComptimeConst; fn_field->data.x_ptr.data.fn.fn_entry = test_fn_entry; + + ZigValue *frame_size_field = this_val->data.x_struct.fields[2]; + frame_size_field->type = get_optional_type(g, g->builtin_types.entry_usize); + frame_size_field->special = ConstValSpecialStatic; + frame_size_field->data.x_optional = nullptr; + + if (fn_is_async(test_fn_entry)) { + frame_size_field->data.x_optional = create_const_vals(1); + frame_size_field->data.x_optional->special = ConstValSpecialStatic; + frame_size_field->data.x_optional->type = g->builtin_types.entry_usize; + bigint_init_unsigned(&frame_size_field->data.x_optional->data.x_bigint, + test_fn_entry->frame_type->abi_size); + } } report_errors_and_maybe_exit(g); @@ -10350,6 +10442,7 @@ static Error check_cache(CodeGen *g, Buf *manifest_dir, Buf *digest) { if (g->is_test_build) { cache_buf_opt(ch, g->test_filter); cache_buf_opt(ch, g->test_name_prefix); + cache_bool(ch, g->test_is_evented); } cache_bool(ch, g->link_eh_frame_hdr); cache_bool(ch, g->is_single_threaded); diff --git a/src/ir.cpp b/src/ir.cpp index 0687d18ef2..f71fe73567 100644 --- a/src/ir.cpp +++ b/src/ir.cpp @@ -5252,6 +5252,7 @@ static IrInstSrc *ir_gen_return(IrBuilderSrc *irb, Scope *scope, AstNode *node, return irb->codegen->invalid_inst_src; } else { return_value = ir_build_const_void(irb, scope, node); + ir_build_end_expr(irb, scope, node, return_value, &result_loc_ret->base); } ir_mark_gen(ir_build_add_implicit_return_type(irb, scope, node, return_value, result_loc_ret)); @@ -5262,7 +5263,7 @@ static IrInstSrc *ir_gen_return(IrBuilderSrc *irb, Scope *scope, AstNode *node, if (!have_err_defers && !irb->codegen->have_err_ret_tracing) { // only generate unconditional defers ir_gen_defers_for_block(irb, scope, outer_scope, false); - IrInstSrc *result = ir_build_return_src(irb, scope, node, return_value); + IrInstSrc *result = ir_build_return_src(irb, scope, node, nullptr); result_loc_ret->base.source_instruction = result; return result; } @@ -5271,10 +5272,6 @@ static IrInstSrc *ir_gen_return(IrBuilderSrc *irb, Scope *scope, AstNode *node, IrBasicBlockSrc *err_block = ir_create_basic_block(irb, scope, "ErrRetErr"); IrBasicBlockSrc *ok_block = ir_create_basic_block(irb, scope, "ErrRetOk"); - if (!have_err_defers) { - ir_gen_defers_for_block(irb, scope, outer_scope, false); - } - IrInstSrc *is_err = ir_build_test_err_src(irb, scope, node, return_value, false, true); IrInstSrc *is_comptime; @@ -5288,22 +5285,18 @@ static IrInstSrc *ir_gen_return(IrBuilderSrc *irb, Scope *scope, AstNode *node, IrBasicBlockSrc *ret_stmt_block = ir_create_basic_block(irb, scope, "RetStmt"); ir_set_cursor_at_end_and_append_block(irb, err_block); - if (have_err_defers) { - ir_gen_defers_for_block(irb, scope, outer_scope, true); - } + ir_gen_defers_for_block(irb, scope, outer_scope, true); if (irb->codegen->have_err_ret_tracing && !should_inline) { ir_build_save_err_ret_addr_src(irb, scope, node); } ir_build_br(irb, scope, node, ret_stmt_block, is_comptime); ir_set_cursor_at_end_and_append_block(irb, ok_block); - if (have_err_defers) { - ir_gen_defers_for_block(irb, scope, outer_scope, false); - } + ir_gen_defers_for_block(irb, scope, outer_scope, false); ir_build_br(irb, scope, node, ret_stmt_block, is_comptime); ir_set_cursor_at_end_and_append_block(irb, ret_stmt_block); - IrInstSrc *result = ir_build_return_src(irb, scope, node, return_value); + IrInstSrc *result = ir_build_return_src(irb, scope, node, nullptr); result_loc_ret->base.source_instruction = result; return result; } @@ -8874,7 +8867,10 @@ static IrInstSrc *ir_gen_if_optional_expr(IrBuilderSrc *irb, Scope *scope, AstNo AstNode *else_node = node->data.test_expr.else_node; bool var_is_ptr = node->data.test_expr.var_is_ptr; - IrInstSrc *maybe_val_ptr = ir_gen_node_extra(irb, expr_node, scope, LValPtr, nullptr); + ScopeExpr *spill_scope = create_expr_scope(irb->codegen, expr_node, scope); + spill_scope->spill_harder = true; + + IrInstSrc *maybe_val_ptr = ir_gen_node_extra(irb, expr_node, &spill_scope->base, LValPtr, nullptr); if (maybe_val_ptr == irb->codegen->invalid_inst_src) return maybe_val_ptr; @@ -8899,7 +8895,7 @@ static IrInstSrc *ir_gen_if_optional_expr(IrBuilderSrc *irb, Scope *scope, AstNo ir_set_cursor_at_end_and_append_block(irb, then_block); - Scope *subexpr_scope = create_runtime_scope(irb->codegen, node, scope, is_comptime); + Scope *subexpr_scope = create_runtime_scope(irb->codegen, node, &spill_scope->base, is_comptime); Scope *var_scope; if (var_symbol) { bool is_shadowable = false; @@ -9619,7 +9615,10 @@ static IrInstSrc *ir_gen_catch(IrBuilderSrc *irb, Scope *parent_scope, AstNode * } - IrInstSrc *err_union_ptr = ir_gen_node_extra(irb, op1_node, parent_scope, LValPtr, nullptr); + ScopeExpr *spill_scope = create_expr_scope(irb->codegen, op1_node, parent_scope); + spill_scope->spill_harder = true; + + IrInstSrc *err_union_ptr = ir_gen_node_extra(irb, op1_node, &spill_scope->base, LValPtr, nullptr); if (err_union_ptr == irb->codegen->invalid_inst_src) return irb->codegen->invalid_inst_src; @@ -9641,7 +9640,7 @@ static IrInstSrc *ir_gen_catch(IrBuilderSrc *irb, Scope *parent_scope, AstNode * is_comptime); ir_set_cursor_at_end_and_append_block(irb, err_block); - Scope *subexpr_scope = create_runtime_scope(irb->codegen, node, parent_scope, is_comptime); + Scope *subexpr_scope = create_runtime_scope(irb->codegen, node, &spill_scope->base, is_comptime); Scope *err_scope; if (var_node) { assert(var_node->type == NodeTypeSymbol); @@ -15494,6 +15493,12 @@ static IrInstGen *ir_analyze_instruction_add_implicit_return_type(IrAnalyze *ira } static IrInstGen *ir_analyze_instruction_return(IrAnalyze *ira, IrInstSrcReturn *instruction) { + if (instruction->operand == nullptr) { + // result location mechanism took care of it. + IrInstGen *result = ir_build_return_gen(ira, &instruction->base.base, nullptr); + return ir_finish_anal(ira, result); + } + IrInstGen *operand = instruction->operand->child; if (type_is_invalid(operand->value->type)) return ir_unreach_error(ira); @@ -19586,6 +19591,12 @@ static IrInstGen *ir_analyze_fn_call(IrAnalyze *ira, IrInst* source_instr, if (type_is_invalid(result_loc->value->type) || result_loc->value->type->id == ZigTypeIdUnreachable) { return result_loc; } + IrInstGen *dummy_value = ir_const(ira, source_instr, impl_fn_type_id->return_type); + dummy_value->value->special = ConstValSpecialRuntime; + IrInstGen *dummy_result = ir_implicit_cast2(ira, source_instr, + dummy_value, result_loc->value->type->data.pointer.child_type); + if (type_is_invalid(dummy_result->value->type)) + return ira->codegen->invalid_inst_gen; ZigType *res_child_type = result_loc->value->type->data.pointer.child_type; if (res_child_type == ira->codegen->builtin_types.entry_var) { res_child_type = impl_fn_type_id->return_type; @@ -19718,6 +19729,12 @@ static IrInstGen *ir_analyze_fn_call(IrAnalyze *ira, IrInst* source_instr, if (type_is_invalid(result_loc->value->type) || result_loc->value->type->id == ZigTypeIdUnreachable) { return result_loc; } + IrInstGen *dummy_value = ir_const(ira, source_instr, return_type); + dummy_value->value->special = ConstValSpecialRuntime; + IrInstGen *dummy_result = ir_implicit_cast2(ira, source_instr, + dummy_value, result_loc->value->type->data.pointer.child_type); + if (type_is_invalid(dummy_result->value->type)) + return ira->codegen->invalid_inst_gen; ZigType *res_child_type = result_loc->value->type->data.pointer.child_type; if (res_child_type == ira->codegen->builtin_types.entry_var) { res_child_type = return_type; @@ -29548,8 +29565,13 @@ static IrInstGen *ir_analyze_instruction_spill_begin(IrAnalyze *ira, IrInstSrcSp if (!type_has_bits(operand->value->type)) return ir_const_void(ira, &instruction->base.base); - ir_assert(instruction->spill_id == SpillIdRetErrCode, &instruction->base.base); - ira->new_irb.exec->need_err_code_spill = true; + switch (instruction->spill_id) { + case SpillIdInvalid: + zig_unreachable(); + case SpillIdRetErrCode: + ira->new_irb.exec->need_err_code_spill = true; + break; + } return ir_build_spill_begin_gen(ira, &instruction->base.base, operand, instruction->spill_id); } @@ -29559,8 +29581,12 @@ static IrInstGen *ir_analyze_instruction_spill_end(IrAnalyze *ira, IrInstSrcSpil if (type_is_invalid(operand->value->type)) return ira->codegen->invalid_inst_gen; - if (ir_should_inline(ira->old_irb.exec, instruction->base.base.scope) || !type_has_bits(operand->value->type)) + if (ir_should_inline(ira->old_irb.exec, instruction->base.base.scope) || + !type_has_bits(operand->value->type) || + instr_is_comptime(operand)) + { return operand; + } ir_assert(instruction->begin->base.child->id == IrInstGenIdSpillBegin, &instruction->base.base); IrInstGenSpillBegin *begin = reinterpret_cast(instruction->begin->base.child); diff --git a/src/main.cpp b/src/main.cpp index 991b46b320..78bdbe1541 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,6 +135,7 @@ static int print_full_usage(const char *arg0, FILE *file, int return_code) { " --test-name-prefix [text] add prefix to all tests\n" " --test-cmd [arg] specify test execution command one arg at a time\n" " --test-cmd-bin appends test binary path to test cmd args\n" + " --test-evented-io runs the test in evented I/O mode\n" , arg0); return return_code; } @@ -428,6 +429,7 @@ int main(int argc, char **argv) { ZigList c_source_files = {0}; const char *test_filter = nullptr; const char *test_name_prefix = nullptr; + bool test_evented_io = false; size_t ver_major = 0; size_t ver_minor = 0; size_t ver_patch = 0; @@ -709,6 +711,8 @@ int main(int argc, char **argv) { cur_pkg = cur_pkg->parent; } else if (strcmp(arg, "-ffunction-sections") == 0) { function_sections = true; + } else if (strcmp(arg, "--test-evented-io") == 0) { + test_evented_io = true; } else if (i + 1 >= argc) { fprintf(stderr, "Expected another argument after %s\n", arg); return print_error_usage(arg0); @@ -1059,6 +1063,7 @@ int main(int argc, char **argv) { g->want_stack_check = want_stack_check; g->want_sanitize_c = want_sanitize_c; g->want_single_threaded = want_single_threaded; + g->test_is_evented = test_evented_io; Buf *builtin_source = codegen_generate_builtin_source(g); if (fwrite(buf_ptr(builtin_source), 1, buf_len(builtin_source), stdout) != buf_len(builtin_source)) { fprintf(stderr, "unable to write to stdout: %s\n", strerror(ferror(stdout))); @@ -1232,6 +1237,7 @@ int main(int argc, char **argv) { if (test_filter) { codegen_set_test_filter(g, buf_create_from_str(test_filter)); } + g->test_is_evented = test_evented_io; if (test_name_prefix) { codegen_set_test_name_prefix(g, buf_create_from_str(test_name_prefix)); diff --git a/test/compile_errors.zig b/test/compile_errors.zig index 0af07d3ece..45586ce0e0 100644 --- a/test/compile_errors.zig +++ b/test/compile_errors.zig @@ -20,6 +20,30 @@ pub fn addCases(cases: *tests.CompileErrorContext) void { "tmp.zig:1:20: error: dependency loop detected", }); + cases.add("function call assigned to incorrect type", + \\export fn entry() void { + \\ var arr: [4]f32 = undefined; + \\ arr = concat(); + \\} + \\fn concat() [16]f32 { + \\ return [1]f32{0}**16; + \\} + , &[_][]const u8{ + "tmp.zig:3:17: error: expected type '[4]f32', found '[16]f32'", + }); + + cases.add("generic function call assigned to incorrect type", + \\pub export fn entry() void { + \\ var res: []i32 = undefined; + \\ res = myAlloc(i32); + \\} + \\fn myAlloc(comptime arg: type) anyerror!arg{ + \\ unreachable; + \\} + , &[_][]const u8{ + "tmp.zig:3:18: error: expected type '[]i32', found 'anyerror!i32", + }); + cases.addTest("non-exhaustive enums", \\const A = enum { \\ a, @@ -5279,25 +5303,6 @@ pub fn addCases(cases: *tests.CompileErrorContext) void { "tmp.zig:2:30: error: cannot set section of local variable 'foo'", }); - cases.add("returning address of local variable - simple", - \\export fn foo() *i32 { - \\ var a: i32 = undefined; - \\ return &a; - \\} - , &[_][]const u8{ - "tmp.zig:3:13: error: function returns address of local variable", - }); - - cases.add("returning address of local variable - phi", - \\export fn foo(c: bool) *i32 { - \\ var a: i32 = undefined; - \\ var b: i32 = undefined; - \\ return if (c) &a else &b; - \\} - , &[_][]const u8{ - "tmp.zig:4:12: error: function returns address of local variable", - }); - cases.add("inner struct member shadowing outer struct member", \\fn A() type { \\ return struct { diff --git a/test/stage1/behavior/async_fn.zig b/test/stage1/behavior/async_fn.zig index e0d1ec2866..dcd5102ff8 100644 --- a/test/stage1/behavior/async_fn.zig +++ b/test/stage1/behavior/async_fn.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; var global_x: i32 = 1; @@ -1329,3 +1330,154 @@ test "async call with @call" { }; S.doTheTest(); } + +test "async function passed 0-bit arg after non-0-bit arg" { + const S = struct { + var global_frame: anyframe = undefined; + var global_int: i32 = 0; + + fn foo() void { + bar(1, .{}) catch unreachable; + } + + fn bar(x: i32, args: var) anyerror!void { + global_frame = @frame(); + suspend; + global_int = x; + } + }; + _ = async S.foo(); + resume S.global_frame; + expect(S.global_int == 1); +} + +test "async function passed align(16) arg after align(8) arg" { + const S = struct { + var global_frame: anyframe = undefined; + var global_int: u128 = 0; + + fn foo() void { + var a: u128 = 99; + bar(10, .{a}) catch unreachable; + } + + fn bar(x: u64, args: var) anyerror!void { + expect(x == 10); + global_frame = @frame(); + suspend; + global_int = args[0]; + } + }; + _ = async S.foo(); + resume S.global_frame; + expect(S.global_int == 99); +} + +test "async function call resolves target fn frame, comptime func" { + const S = struct { + var global_frame: anyframe = undefined; + var global_int: i32 = 9; + + fn foo() anyerror!void { + const stack_size = 1000; + var stack_frame: [stack_size]u8 align(std.Target.stack_align) = undefined; + return await @asyncCall(&stack_frame, {}, bar); + } + + fn bar() anyerror!void { + global_frame = @frame(); + suspend; + global_int += 1; + } + }; + _ = async S.foo(); + resume S.global_frame; + expect(S.global_int == 10); +} + +test "async function call resolves target fn frame, runtime func" { + const S = struct { + var global_frame: anyframe = undefined; + var global_int: i32 = 9; + + fn foo() anyerror!void { + const stack_size = 1000; + var stack_frame: [stack_size]u8 align(std.Target.stack_align) = undefined; + var func: async fn () anyerror!void = bar; + return await @asyncCall(&stack_frame, {}, func); + } + + fn bar() anyerror!void { + global_frame = @frame(); + suspend; + global_int += 1; + } + }; + _ = async S.foo(); + resume S.global_frame; + expect(S.global_int == 10); +} + +test "properly spill optional payload capture value" { + const S = struct { + var global_frame: anyframe = undefined; + var global_int: usize = 2; + + fn foo() void { + var opt: ?usize = 1234; + if (opt) |x| { + bar(); + global_int += x; + } + } + + fn bar() void { + global_frame = @frame(); + suspend; + global_int += 1; + } + }; + _ = async S.foo(); + resume S.global_frame; + expect(S.global_int == 1237); +} + +test "handle defer interfering with return value spill" { + const S = struct { + var global_frame1: anyframe = undefined; + var global_frame2: anyframe = undefined; + var finished = false; + var baz_happened = false; + + fn doTheTest() void { + _ = async testFoo(); + resume global_frame1; + resume global_frame2; + expect(baz_happened); + expect(finished); + } + + fn testFoo() void { + expectError(error.Bad, foo()); + finished = true; + } + + fn foo() anyerror!void { + defer baz(); + return bar() catch |err| return err; + } + + fn bar() anyerror!void { + global_frame1 = @frame(); + suspend; + return error.Bad; + } + + fn baz() void { + global_frame2 = @frame(); + suspend; + baz_happened = true; + } + }; + S.doTheTest(); +}