diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 12:40:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 12:46:51 -0300 |
| commit | f5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch) | |
| tree | 2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/io | |
| download | esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip | |
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig
turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask
ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker.
src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers;
src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip;
src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/io')
| -rw-r--r-- | src/io/chip.zig | 100 | ||||
| -rw-r--r-- | src/io/context.zig | 212 | ||||
| -rw-r--r-- | src/io/host.zig | 76 | ||||
| -rw-r--r-- | src/io/p4.zig | 1475 | ||||
| -rw-r--r-- | src/io/unimplemented.zig | 490 |
5 files changed, 2353 insertions, 0 deletions
diff --git a/src/io/chip.zig b/src/io/chip.zig new file mode 100644 index 0000000..c5e4127 --- /dev/null +++ b/src/io/chip.zig @@ -0,0 +1,100 @@ +//! The ESP32-P4 half of the scheduler's machine seam: time, critical sections, idling, entropy and +//! a way to print when nothing else works. +//! +//! This is the only file in `src/io/` that touches the chip, and it is deliberately thin - eight +//! functions - because everything above it is portable and gets tested on the host through +//! `host.zig`, which implements the same eight. Nothing here re-derives a register address or a +//! clock: the timebase is `hal.systimer`, the critical section is `hal.intr`'s (which is +//! `clkrst.Guard` under another name, so a critical section written against either module is the +//! same one), and the console is the mask-ROM `ets_printf` that `soc.rom` already declares. + +const std = @import("std"); +const hal = @import("hal"); +const soc = @import("soc"); +const regs = @import("regs"); +const mmio = @import("mmio"); + +/// The one timebase on this board that does not move. SYSTIMER is XTAL/2.5 = 16 MHz, fixed +/// (`clk_tree_defs.h:196-198`, and `hal/systimer.zig:28-31`): it is not derived from the CPU clock, +/// which the bootloader left at a measured ~90 MHz and which nothing here reconfigures. Using the +/// cycle counter instead would make every timeout in the stack wrong by a factor of four the moment +/// somebody raises the PLL. +pub const ticks_hz: u64 = hal.systimer.hz; + +/// Last value the counter gave us, so `ticks` can be monotonic even when the read fails. +var last_ticks: u64 = 0; + +/// Bring the timebase up. Idempotent, and specifically does *not* reprogram the clock source or +/// divider - `hal.systimer.init` documents why: re-running that on a live counter makes the +/// timebase jump, which would corrupt every deadline already computed from it. +pub fn init() void { + hal.systimer.init(); + last_ticks = hal.systimer.read(.unit0) orelse 0; +} + +/// The 52-bit counter, through the update/valid handshake `hal.systimer.read` implements. +/// +/// A gated-off systimer never sets VALUE_VALID, and `hal.systimer.read` reports that as `null` +/// rather than hanging. Returning the previous value there is the only safe answer: returning zero +/// would send time backwards, and every deadline in the scheduler is an unsigned comparison against +/// it, so one backwards step would turn every pending sleep into "already expired". +pub fn ticks() u64 { + const t = hal.systimer.read(.unit0) orelse return last_ticks; + last_ticks = t; + return t; +} + +/// A critical section against interrupt handlers. `hal.intr.Guard` nests correctly - `release` only +/// sets mstatus.MIE if MIE was set on entry - so the scheduler can take one inside a handler. +pub const Guard = hal.intr.Guard; + +pub inline fn mask() Guard { + return hal.intr.mask(); +} + +/// Wait for something to change. Called with interrupts in whatever state the caller had them, +/// which is normally enabled, and free to return at any time: every caller re-checks its condition. +/// +/// This **spins** rather than issuing `wfi`, and that is a decision worth stating. `wfi` is what a +/// power-managed system would do, but it can only be woken by an interrupt, and on this board there +/// is no timer interrupt to wake it: `hal/systimer.zig` exposes the counters and none of the six +/// comparators, and nothing in `hal.intr` is wired to them. A `wfi` with a pending deadline and no +/// alarm configured is a hang, and a `wfi` with no deadline at all is a hang that the scheduler's +/// deadlock watchdog cannot even report, because the watchdog needs to keep running to fire. So +/// this spins on the counter, which costs power and finds every bug. +/// +/// The follow-up is small and worth doing when power matters: a SYSTIMER comparator (`TARGET0`, +/// `SYSTIMER_TARGET0_INT`) routed through `hal.intr` would let this be `wfi` with an exact wake. +pub fn idle(deadline: ?u64) void { + _ = deadline; + // One counter read is ~20 cycles of handshake, which is a fine spin quantum and re-reads the + // register the caller is about to compare against anyway. + _ = ticks(); +} + +/// Print, when the machinery that would normally print has failed. `ets_printf` is a mask ROM +/// address (`esp32p4.rom.ld:24`, re-declared by this project's linker script), so it allocates +/// nothing, takes no lock, and works before or after any of this project's code is functional. +pub inline fn print(comptime fmt: [*:0]const u8, args: anytype) void { + soc.rom.print(fmt, args); +} + +/// The hardware random number register: `WDEV_RND_REG`, which on a pre-v3 P4 die is +/// `LP_SYSTEM_REG_RNG_DATA_REG` (`components/soc/esp32p4/register/hw_ver1/soc/wdev_reg.h:16`) at +/// `DR_REG_LP_SYS_BASE + 0x1a4` = 0x501101a4. `esp_random` reads exactly this register and nothing +/// else (`components/esp_hw_support/hw_random.c:78,86`). +/// +/// How much entropy is behind it is a separate question, and the answer for this image is "not +/// established" - see `p4.zig`'s `random` for what is done about that. The register itself is real. +const rng_data = mmio.Reg.at(regs.LP_SYSTEM_REG_RNG_DATA_REG); + +pub inline fn entropyWord() u32 { + return rng_data.raw(); +} + +/// Anything else the machine can contribute to a seed. The cycle counter is not a second timebase - +/// it is the same instant measured with a different, unknown divisor - but its low bits carry the +/// jitter of however many bus stalls happened since reset, which is exactly what a seed wants. +pub inline fn noise() u64 { + return soc.cycles(); +} diff --git a/src/io/context.zig b/src/io/context.zig new file mode 100644 index 0000000..eee8d26 --- /dev/null +++ b/src/io/context.zig @@ -0,0 +1,212 @@ +//! The machine half of the scheduler: what a suspended task is, and how to hand it the CPU. +//! +//! A suspended task is **three words**: `sp`, `fp`, `pc`. Nothing else is stored, and that is not a +//! shortcut - it is the same trick `std.Io.fiber` uses for aarch64, riscv64 and x86_64 +//! (`/usr/lib/zig/std/Io/fiber.zig:7-24`), and it is worth understanding before reading the asm, +//! because "the context switch saves twelve callee-saved registers" is the shape everybody expects +//! and it is not what happens here. +//! +//! The switch declares **every register it does not save as clobbered**. The register allocator +//! then spills whatever is live across the switch onto the switching task's own stack, as part of +//! the calling function's frame, and reloads it when that frame is resumed. So the callee-saved +//! registers *are* saved - by the compiler, into the stack the task already owns, and only the ones +//! that actually hold something. Compiled for this target with `-OReleaseSmall` the spill set +//! around one switch is `ra, s1-s11, fs0-fs11` (verified by reading `-femit-asm` output for +//! riscv32 with `+f`), i.e. 24 words, and it shrinks to nothing in a leaf that holds no live state. +//! A hand-written switch that stores all 24 unconditionally would be both bigger and slower. +//! +//! The exact register set, for the ESP32-P4's rv32imafc: +//! +//! * **saved by this file, in `Context`:** `sp` (x2), `fp` (x8), and the resume address. +//! * **saved by the compiler, because they are clobbered:** `ra` (x1), `t0-t2` (x5-x7), +//! `s1` (x9), `a0` and `a2-a7` (x10, x12-x17), `s2-s11` (x18-x27), `t3-t6` (x28-x31), all 32 +//! `f` registers, and `fflags`/`frm`. `a1` (x11) is the asm's own in/out operand. +//! * **not switched at all, deliberately:** `x0` is hardwired zero. `gp` (x3) is the linker's +//! global pointer - one value for the whole image, never written by a task. `tp` (x4) is the +//! thread pointer; this image has no thread-locals and one hart, so there is nothing per-task +//! to point at. Listing either as a clobber would ask the register allocator to spill a +//! register it can never reload. +//! +//! **First entry differs from a resume in exactly one way: the resume address.** A resume jumps to +//! the label inside the asm block, lands back inside `contextSwitch`, and returns to a frame whose +//! spills are all where the compiler left them. A first entry jumps to a function's first +//! instruction on a stack that contains nothing at all - so on first entry: +//! +//! * `sp` must satisfy the ABI's entry condition (16-byte aligned on RISC-V; 16-byte aligned +//! *minus one word* on x86-64, where a `call` would have pushed a return address); +//! * `fp` is zero, which terminates a frame-pointer walk rather than following garbage; +//! * `ra` is **garbage**, because there is nowhere to return to. The entry function is therefore +//! `noreturn`: it must end in another context switch, and the compiler emits no `ret` for it. +//! On x86-64, where the return address is a stack slot rather than a register, the slot is +//! filled with `returnTrap` so that a mistake is a named panic instead of a wild jump. +//! +//! Nothing is passed to the entry function in a register. There is no way to: on the resumed side +//! the only defined register is the asm's in/out operand, which holds the *switching* task's +//! `Switch` pointer. `p4.zig` reads the current task from the scheduler instead, which it has +//! already set before switching. + +const std = @import("std"); +const builtin = @import("builtin"); + +/// The ESP32-P4 itself. `std.Io.fiber` covers aarch64, riscv64 and x86_64 but not riscv32 +/// (`fiber.zig:1-4`), so the chip's switch is written here and the host's is borrowed from std - +/// which is what lets the scheduler's tests run on the host at all. +pub const rv32 = builtin.cpu.arch == .riscv32; + +pub const supported = rv32 or std.Io.fiber.supported; + +/// A suspended task's whole machine state. 12 bytes on rv32, 24 on the 64-bit hosts. +pub const Context = if (rv32) extern struct { + sp: usize, + fp: usize, + pc: usize, +} else std.Io.fiber.Context; + +/// Layout-identical to `std.Io.fiber.Switch`; a distinct type only so the riscv32 path does not +/// have to name a std type that does not describe it. +pub const Switch = extern struct { old: *Context, new: *Context }; + +/// What the ABI requires of `sp` at a function's first instruction, on every target here. +pub const stack_align = 16; + +/// Save the current machine state into `s.old` and resume the state in `s.new`. +/// +/// Returns when something switches back to `s.old`. Never returns if nothing does - which is the +/// scheduler's problem, not this function's. +pub inline fn contextSwitch(s: *const Switch) void { + if (rv32) { + _ = asm volatile ( + // a1 holds `s` on the way in, and on the way out holds the `Switch` of whoever resumed us. + // The loads must all happen before `sp` moves: after `lw sp, 0(a2)` this frame is gone. + \\ lw a0, 0(a1) + \\ lw a2, 4(a1) + \\ lla a3, 0f + \\ sw sp, 0(a0) + \\ sw fp, 4(a0) + \\ sw a3, 8(a0) + \\ lw sp, 0(a2) + \\ lw fp, 4(a2) + \\ lw a3, 8(a2) + \\ jr a3 + \\0: + : [received] "={a1}" (-> *const Switch), + : [send] "{a1}" (s), + : .{ + .x1 = true, + .x5 = true, + .x6 = true, + .x7 = true, + .x9 = true, + .x10 = true, + .x12 = true, + .x13 = true, + .x14 = true, + .x15 = true, + .x16 = true, + .x17 = true, + .x18 = true, + .x19 = true, + .x20 = true, + .x21 = true, + .x22 = true, + .x23 = true, + .x24 = true, + .x25 = true, + .x26 = true, + .x27 = true, + .x28 = true, + .x29 = true, + .x30 = true, + .x31 = true, + .f0 = true, + .f1 = true, + .f2 = true, + .f3 = true, + .f4 = true, + .f5 = true, + .f6 = true, + .f7 = true, + .f8 = true, + .f9 = true, + .f10 = true, + .f11 = true, + .f12 = true, + .f13 = true, + .f14 = true, + .f15 = true, + .f16 = true, + .f17 = true, + .f18 = true, + .f19 = true, + .f20 = true, + .f21 = true, + .f22 = true, + .f23 = true, + .f24 = true, + .f25 = true, + .f26 = true, + .f27 = true, + .f28 = true, + .f29 = true, + .f30 = true, + .f31 = true, + .fflags = true, + .frm = true, + .memory = true, + }); + } else { + _ = std.Io.fiber.contextSwitch(@ptrCast(s)); + } +} + +/// The `Context` for a task that has never run, which will enter `entry` on the stack ending at +/// `stack_top`. `stack_top` is exclusive, i.e. one past the last writable byte. +/// +/// Writes to the top of the stack on x86-64 (see the file comment); reads nothing. +pub fn initial(stack_top: usize, entry: *const fn () callconv(.c) noreturn) Context { + const top = stack_top & ~@as(usize, stack_align - 1); + return switch (builtin.cpu.arch) { + .x86_64 => sp: { + // System V x86-64 guarantees `rsp % 16 == 0` *before* the `call` that enters a + // function, so a callee's first instruction sees `rsp % 16 == 8` with the return + // address at `[rsp]`. We arrive by `jmp`, so both halves have to be faked, or the + // first 16-byte-aligned spill in the entry function faults. + const sp = top - 8; + @as(*usize, @ptrFromInt(sp)).* = @intFromPtr(&returnTrap); + break :sp .{ .rsp = sp, .rbp = 0, .rip = @intFromPtr(entry) }; + }, + else => .{ .sp = top, .fp = 0, .pc = @intFromPtr(entry) }, + }; +} + +/// How much of the top of a stack `initial` consumes before the entry function's first frame. +pub const initial_stack_overhead: usize = switch (builtin.cpu.arch) { + .x86_64 => 8, + else => 0, +}; + +fn returnTrap() callconv(.c) noreturn { + @panic("io.p4: a task returned from its entry function; the entry function must be noreturn"); +} + +test "initial context points at the entry function on an aligned stack" { + if (!supported) return error.SkipZigTest; + var stack: [256]u8 align(stack_align) = undefined; + const c = initial(@intFromPtr(&stack) + stack.len, returnTrap); + const sp = switch (builtin.cpu.arch) { + .x86_64 => c.rsp, + else => c.sp, + }; + const pc = switch (builtin.cpu.arch) { + .x86_64 => c.rip, + else => c.pc, + }; + try std.testing.expectEqual(@intFromPtr(&returnTrap), pc); + try std.testing.expect(sp <= @intFromPtr(&stack) + stack.len); + try std.testing.expect(sp > @intFromPtr(&stack)); + // The entry condition the ABI states, per target. + switch (builtin.cpu.arch) { + .x86_64 => try std.testing.expectEqual(@as(usize, 8), sp % stack_align), + else => try std.testing.expectEqual(@as(usize, 0), sp % stack_align), + } +} diff --git a/src/io/host.zig b/src/io/host.zig new file mode 100644 index 0000000..7239f1a --- /dev/null +++ b/src/io/host.zig @@ -0,0 +1,76 @@ +//! The host half of the scheduler's machine seam: the same eight declarations as `chip.zig`, for a +//! machine that is not the chip. +//! +//! This exists so the scheduler itself can be tested where it can be debugged. Everything above +//! `context.zig` and this file is portable - the run queue, the futex, cancellation, deadline +//! arithmetic - and all of it is the part where a mistake is a silent hang on the die. +//! +//! The clock here is **virtual**, not the wall clock, and that is the point. `ticks` returns a +//! counter that only advances when the scheduler idles, so a test that sleeps three tasks for +//! 5 ms, 1 ms and 3 ms finishes instantly and in a fixed order, with no tolerance windows and no +//! flakiness under load. It also means the scheduler's own deadlock watchdog is testable: the +//! no-deadline idle advances the clock too, so a watchdog measured in ticks still fires. + +const std = @import("std"); + +/// The chip's rate, kept identically here so the host exercises the same tick/nanosecond arithmetic +/// (`* 125 / 2`) rather than a rounder number that would hide a division bug. +pub const ticks_hz: u64 = 16_000_000; + +var virtual: u64 = 0; + +pub fn init() void { + virtual = 0; +} + +pub fn ticks() u64 { + return virtual; +} + +/// No interrupts to mask on the host, and the scheduler is single-threaded, so this is a shape +/// rather than a mechanism. It still has to exist: it is what makes the chip's masked regions +/// reachable in a host test. +pub const Guard = struct { + pub inline fn release(_: Guard) void {} +}; + +pub inline fn mask() Guard { + return .{}; +} + +/// Advance the virtual clock. With a deadline, jump straight to it - nothing else can happen in +/// between on a host with no interrupts, so waiting is pure delay. Without one, advance by a +/// millisecond so that a caller spinning on "nothing runnable, nothing scheduled" reaches its +/// watchdog instead of looping forever. +pub fn idle(deadline: ?u64) void { + if (deadline) |d| { + if (d > virtual) virtual = d; + } else { + virtual += ticks_hz / 1000; + } +} + +/// Enough of `ets_printf`'s shape to typecheck the arguments, printed in Zig's own spelling. The +/// crash path is the only caller, and on the host the interesting question is whether it runs at +/// all, not how it looks. +pub inline fn print(comptime fmt: [*:0]const u8, args: anytype) void { + std.debug.print("{s} <- {any}\n", .{ std.mem.span(fmt), args }); +} + +/// No hardware RNG. Returns zero rather than something plausible: `p4.zig` mixes this with the +/// clock and its own state, and a zero here makes it obvious in a test that the hardware +/// contribution is absent instead of quietly supplying the entropy the chip is being asked about. +pub inline fn entropyWord() u32 { + return 0; +} + +/// Deliberately constant: a host test that asserts something about the generator's output wants a +/// fixed seed, and the chip's own seed noise is `soc.cycles()`, which no host has. +pub inline fn noise() u64 { + return 0x0420_0cafe_0420; +} + +/// Test-only: move the virtual clock forward by hand. +pub fn advance(n: u64) void { + virtual += n; +} diff --git a/src/io/p4.zig b/src/io/p4.zig new file mode 100644 index 0000000..ef82e02 --- /dev/null +++ b/src/io/p4.zig @@ -0,0 +1,1475 @@ +//! `std.Io` for the ESP32-P4: a cooperative scheduler over caller-provided static stacks. +//! +//! This is the "FreeRTOS for Zig" layer, except that it is not a framework and there is nothing to +//! port to: `std.Io` is an interface, and filling in enough of its vtable buys the whole ecosystem +//! that is written against it. Implementing `futexWait`/`futexWake` and `async`/`await` here is +//! what makes `std.Io.Mutex`, `std.Io.Condition`, `std.Io.Semaphore`, `std.Io.RwLock`, +//! `std.Io.Queue(T)` and `std.Io.Future(T)` work on this chip - none of which appear in this file, +//! because they are built on those primitives in std and need nothing from us +//! (`/usr/lib/zig/std/Io.zig:1587` Mutex, `:1653` Condition, `:1874` Queue). +//! +//! ## What is real and what is not +//! +//! `std.Io.VTable` has 109 entries. Sixteen are implemented here; the other 93 come from +//! `unimplemented.zig`, where each one panics with its own name. That is the deliberate shape of +//! this file: a partial implementation whose gaps announce themselves, rather than a plausible +//! stub that returns zero. Implemented: +//! +//! now clockResolution sleep - the timebase, from hal.systimer +//! async concurrent await cancel - cooperative tasks on static stacks +//! futexWait futexWaitUncancelable futexWake +//! checkCancel recancel swapCancelProtection +//! crashHandler random randomSecure - randomSecure reports EntropyUnavailable; see `random` +//! +//! Not implemented, and therefore a named panic: every `dir*`, `file*`, `net*`, `process*` and +//! `child*` entry, `operate`, `batchAwait*`/`batchCancel`, the four `group*` entries, +//! `lockStderr`/`tryLockStderr`/`unlockStderr`, and `progressParentFile`. So `std.Io.Group`, +//! `std.Io.Select`, and any Reader or Writer that reaches the OS are out; the concurrency and +//! synchronisation types listed above are in. +//! +//! ## The model +//! +//! One hart, no preemption, no timer interrupt. A task runs until it calls something that blocks - +//! `sleep`, `futexWait`, `await`, `cancel`, or `yield` - and the scheduler is entered from inside +//! that call. There is no scheduler task and no scheduler stack: the context that called +//! `Runtime.init` is itself a task (the "main" slot, id 0), so a switch is always task-to-task and +//! the first blocking call in `main` is what starts everything else. `io.async` only *assigns* a +//! slot and marks it ready; the body first executes when the spawning context blocks or yields. +//! That is within the contract - `async` promises a unit of concurrency, not that the work started +//! (`Io.zig:54-58`) - and it is why a `main` that spawns seven tasks and then returns runs none of +//! them. +//! +//! ## Why a futex reduces to "park this task" here, and where that stops being true +//! +//! A futex exists to close a race: between "I checked the value" and "I went to sleep", a waker can +//! change the value and wake nothing, and the waiter sleeps forever. Linux closes it by doing the +//! compare and the enqueue inside the kernel, atomically. +//! +//! On this chip the whole window is closed by the machine. One hart and no preemption means no +//! other *task* can run between the compare and the enqueue - there is no instant at which control +//! could pass to a waker - so a plain compare followed by a plain enqueue is already atomic with +//! respect to every task. The only thing that can intervene is an **interrupt handler**, and +//! ESP-Hosted does post from one (`_h_post_semaphore_from_isr`), so the compare-and-enqueue runs +//! inside `hal.intr.mask()` - two CSR instructions - and `futexWake` runs inside the same critical +//! section. That makes the pair atomic against handlers too, and `futexWake` safe to call from a +//! CLIC handler. +//! +//! This stops being sound the moment either assumption goes: +//! +//! * **A second core.** The P4 has two HP cores and an LP core. A task on core 1 could observe +//! the value, be preempted by nothing at all, and still race a waker on core 0, because +//! masking interrupts on one hart says nothing about the other. A cross-core version needs a +//! real wait queue per address plus an inter-core interrupt, not this. +//! * **Preemption.** Add a timer interrupt that switches tasks and the "no other task can run" +//! argument is gone; the mask covers it, but only because the mask is what the preemption +//! would arrive through. Any preemption source that is not maskable here breaks it. +//! +//! Two further honest limits: waiters are found by scanning the task array rather than by a +//! per-address wait queue (correct, O(tasks), and tasks are counted in single digits), and the scan +//! is in slot order, so `futexWake(ptr, 1)` picks the lowest-numbered waiter rather than the +//! longest-waiting one. Under a contended `std.Io.Mutex` that is a fairness wart, not a +//! correctness one - the run queue itself is FIFO - but a heavily contended lock could starve a +//! high-numbered slot. +//! +//! ## Static footprint +//! +//! Nothing here allocates, and nothing here is `comptime`-sized by a count. Measured on a real +//! riscv32 build of this file (`footprint` below asserts both columns): +//! +//! @sizeOf(Task) 80 bytes per slot, in .bss - 12 of them are the suspended machine state +//! @sizeOf(Runtime) 152 bytes including the embedded main task +//! stack caller's whatever the slot declares +//! +//! On top of a slot's stack, and only while a slot is running an `io.async`/`io.concurrent` body, +//! sit two copies carved off the top of that same stack: the argument tuple and the result. Both +//! are a few tens of bytes for a typical call, and `claim` refuses a slot whose stack cannot hold +//! them and still leave `min_stack_bytes`, in which case `async` runs the work eagerly instead. +//! +//! So a 7-task ESP-Hosted configuration with 5 KiB stacks is 35 KiB of stack + 560 bytes of slots +//! + 152 bytes of runtime = 35.7 KiB, against the ~128 KiB of L2MEM. +//! +//! Code, from `nm` on a riscv32 `-OReleaseSmall` build of this file against the real `hal`, `soc`, +//! `regs` and `mmio` modules: 2,402 bytes of out-of-line `.text` in 21 symbols for the scheduler +//! and the sixteen implemented entries (`reschedule`, `claim`, `joinChild` and `futexBlock` are +//! private and get inlined into their callers, so a caller pays a little more), plus 1,860 bytes +//! of `.text.unlikely` and 3,722 bytes of `.rodata` strings for the 93 panicking stubs. The stub +//! strings are the single largest line item here, and they are the price of a gap that announces +//! itself: they only survive if the app's root declares a panic handler that prints the message, +//! which `src/main.zig` does. +//! +//! One number worth having when sizing a stack: a task suspended inside the switch is holding +//! about 160 bytes of its own stack for the spills the switch's clobber list forces, on top of +//! whatever its own call chain is using (read off the `addi sp, sp, 160` frame of `reschedule` in +//! the riscv32 asm). `stackUsed` reports the real high-water mark of any slot at any time, from +//! the paint `init` lays down, and `dump` prints it for every task. +//! +//! ## Integrating this +//! +//! Module imports: `hal` (systimer, and `intr.mask` for the critical section), `soc` (`rom.print` +//! and `cycles`), and `regs` + `mmio` (the one RNG register). All four already exist in build.zig. +//! +//! One requirement that is std's rather than this file's: the app's **root module** must declare +//! +//! pub const std_options: std.Options = .{ .page_size_min = 4096 }; +//! +//! Five of the vtable's entries name `Io.File.MemoryMap`, whose `memory` field is +//! `[]align(std.heap.page_size_min) u8` (`/usr/lib/zig/std/Io/File/MemoryMap.zig:18`), and +//! riscv32-freestanding has no default for that (`/usr/lib/zig/std/heap.zig:48`). Defining a +//! function whose return type is `File.MemoryMap.CreateError!File.MemoryMap` is what forces the +//! struct to be laid out, so no stub body can dodge it, and any `std.Io` implementation on this +//! target hits the same wall. It has to be the *root* module, because `std.options` is read as +//! `@import("root").std_options` (`/usr/lib/zig/std/std.zig:112`) - putting the declaration in +//! this file has no effect, which is measured rather than assumed. The value is arbitrary: +//! nothing in this image pages, and it is only ever read for that one field's alignment. +//! +//! ## What is missing that a caller will notice +//! +//! `idle` spins instead of sleeping, because there is no timer interrupt to wake a `wfi` (see +//! `chip.zig`). There is no priority: the run queue is FIFO. And nothing here bounds how long a +//! task may hold the CPU, so one task that never blocks starves the rest - which is the deal with +//! cooperative scheduling, and the reason `sleep` yields even when the deadline has already passed. + +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Alignment = std.mem.Alignment; +const assert = std.debug.assert; + +const context = @import("context.zig"); +const unimplemented = @import("unimplemented.zig"); + +/// The chip, or the host that tests the chip's scheduler. Both sides implement the same eight +/// declarations; see `chip.zig` for what each one costs on the die. +const machine = if (builtin.os.tag == .freestanding) @import("chip.zig") else @import("host.zig"); + +const Context = context.Context; +const Switch = context.Switch; + +/// Written into every byte of a task's stack at `init`, so that `stackUsed` can report a real +/// high-water mark and `checkStack` can catch an overflow at the next switch instead of at the next +/// mystery. 0xC5 rather than 0 because a zeroed stack is indistinguishable from `.bss`. +const paint: u8 = 0xC5; + +/// The least a slot's stack may be for `async` to use it, over and above the argument and result +/// copies. Below this the slot is skipped and the work runs eagerly on the caller's stack, which is +/// always legal (`Io.zig:54-56`) and better than a stack overflow. +pub const min_stack_bytes: usize = 512; + +/// The runtime that owns the currently running task. A cooperative scheduler needs exactly one +/// global: a task's entry function is jumped to with no arguments - the machine has nowhere to put +/// them, see `context.zig` - so it has to find itself somehow, and this is the pointer it finds +/// itself through. FreeRTOS spells the same thing `pxCurrentTCB`. +/// +/// One `Runtime` per hart, therefore. Installing a second one while the first has live tasks is a +/// programming error, and `init` asserts against it. +var installed: ?*Runtime = null; + +pub const State = enum(u8) { + /// The slot holds no task. `async` may claim it. + free, + /// Runnable, and on the run queue. + ready, + /// Has the CPU. + running, + /// Waiting for a futex, a deadline, or a child task. + blocked, + /// Ran to completion; the result is in `result`, waiting for `await` or `cancel` to collect it. + done, +}; + +/// One unit of concurrency: the stack a task runs on, plus what the scheduler needs to know about +/// it. The caller owns `stack` and nothing else here. +pub const Task = struct { + /// The task's stack. Caller-provided and caller-sized, `.bss` or a static array; the scheduler + /// aligns the top down to the ABI's 16 bytes itself, so any byte slice will do. + /// + /// The main task's stack is empty: it runs on whatever stack `_start` established, and the + /// scheduler never needs to know where that is. + stack: []u8, + + // -------- scheduler-private from here down -------- + + /// `sp`, `fp` and the resume address. Meaningful only while suspended. + ctx: Context = undefined, + state: State = .free, + /// Run queue link. Single-linked FIFO; the queue is only ever walked forwards. + next: ?*Task = null, + /// The address this task is parked on, if it is parked on a futex. + futex: ?*const u32 = null, + /// Machine ticks at which the scheduler must make this task ready again. + deadline: ?u64 = null, + /// The child this task is inside `await`/`cancel` for. + join: ?*Task = null, + /// Who to make ready when this task finishes. + awaiter: ?*Task = null, + /// Whether a cancel request should end the current block. False inside an uncancelable wait. + interruptible: bool = false, + /// A cancel request has been made and not yet delivered. See `Future.cancel`'s doc comment + /// (`Io.zig:1181-1190`): only the *next* cancelation point returns `error.Canceled`. + cancel_pending: bool = false, + /// A cancelation point in this task consumed a request. `await` needs this to decide whether a + /// cancelation it forwarded to a child was actually delivered. + cancel_acked: bool = false, + protection: Io.CancelProtection = .unblocked, + /// The type-erased body, its argument copy and its result storage, as `async` was handed them. + start: *const fn (context: *const anyopaque, result: *anyopaque) void = undefined, + arg: *const anyopaque = undefined, + result: []u8 = &.{}, + /// Slot number, for diagnostics. 0 is the main task. + id: u8 = 0, +}; + +pub const Options = struct { + /// Nanoseconds to add to the systimer to answer `Clock.real`. Zero - the default - means + /// `Clock.real` reads as nanoseconds since the counter started, i.e. since boot: this board has + /// no RTC that survives a reset and no NTP. Set it once real time is known from outside (an + /// SNTP exchange, an HTTP `Date` header) and `Clock.real` becomes real. + real_epoch_offset_ns: i64 = 0, + /// How long the scheduler may find nothing runnable *and* no deadline pending before it + /// declares deadlock and panics with a dump of every task. + /// + /// That state is a deadlock unless an interrupt handler is about to wake somebody, which is a + /// legitimate thing to be waiting for - hence a timeout rather than an immediate panic. Five + /// seconds is long enough for any SDIO transaction on this board and short enough that a lost + /// wakeup is reported rather than looking like a hang. + deadlock_timeout_ms: u32 = 5_000, +}; + +pub const Runtime = struct { + /// Caller-provided slots, one per concurrent task. Index 0 of `tasks()` is `main`, not this. + slots: []Task, + /// The context that called `init`. Never started, only ever resumed. + main: Task, + current: *Task, + ready_head: ?*Task, + ready_tail: ?*Task, + real_epoch_offset_ns: i64, + deadlock_ticks: u64, + rng: Rng, + + /// Take over as this hart's `std.Io`. `slots` must outlive the runtime, and so must `rt` + /// itself: the runtime points at its own `main` field. + /// + /// Brings the timebase up (`hal.systimer.init`, which deliberately does not reprogram the + /// clock source - see `chip.zig`). Touches nothing else on the chip: no interrupt is enabled, + /// no pad is configured, and mstatus.MIE is left exactly as the caller had it. + pub fn init(rt: *Runtime, slots: []Task, opts: Options) void { + comptime { + if (!context.supported) @compileError( + "io/p4.zig: no context switch for this architecture; see context.zig", + ); + } + // `Task.id` is a u8 and 0 is the main task, so 254 slots is the ceiling. Anything close to + // it would have run out of L2MEM for stacks long before. + assert(slots.len < 255); + if (installed) |old| { + if (old != rt) { + var it = old.tasks(); + while (it.next()) |t| assert(t.state == .free or t == &old.main); + } + } + machine.init(); + rt.* = .{ + .slots = slots, + .main = .{ .stack = &.{}, .state = .running, .id = 0 }, + .current = undefined, + .ready_head = null, + .ready_tail = null, + .real_epoch_offset_ns = opts.real_epoch_offset_ns, + .deadlock_ticks = @as(u64, opts.deadlock_timeout_ms) * machine.ticks_hz / 1000, + .rng = .{}, + }; + rt.current = &rt.main; + for (slots, 0..) |*slot, i| { + const stack = slot.stack; + slot.* = .{ .stack = stack, .id = @intCast(i + 1) }; + @memset(stack, paint); + } + installed = rt; + } + + pub fn io(rt: *Runtime) Io { + return .{ .userdata = rt, .vtable = &vtable }; + } + + /// Put the current task at the back of the run queue and let everything else have a turn. + /// + /// Not a `std.Io` entry - there is none - but a cooperative scheduler needs a yield, and code + /// that only has an `Io` can get the same effect from `io.sleep(.zero, .awake)`, which is + /// documented to yield exactly once even for a deadline that has already passed. + pub fn yield(rt: *Runtime) void { + const me = rt.current; + { + const guard = machine.mask(); + defer guard.release(); + me.state = .ready; + rt.push(me); + } + rt.reschedule(); + } + + /// Bytes of `t`'s stack that have ever been written, from the paint laid down by `init`. + /// Zero for the main task, whose stack this scheduler does not own. + pub fn stackUsed(_: *const Runtime, t: *const Task) usize { + var i: usize = 0; + while (i < t.stack.len and t.stack[i] == paint) i += 1; + return t.stack.len - i; + } + + pub fn stackFree(rt: *const Runtime, t: *const Task) usize { + return t.stack.len - rt.stackUsed(t); + } + + /// Every task, main first. + pub fn tasks(rt: *Runtime) Iterator { + return .{ .rt = rt }; + } + + pub const Iterator = struct { + rt: *Runtime, + i: usize = 0, + + pub fn next(it: *Iterator) ?*Task { + const n = it.i; + it.i += 1; + if (n == 0) return &it.rt.main; + if (n - 1 < it.rt.slots.len) return &it.rt.slots[n - 1]; + return null; + } + }; + + /// One line per task, on the ROM console. Allocates nothing and takes no lock, so it is safe + /// from the crash path. + pub fn dump(rt: *Runtime) void { + machine.print("MARK IO_P4 current=%u slots=%u tick=%u\r\n", .{ + @as(u32, rt.current.id), + @as(u32, @intCast(rt.slots.len)), + @as(u32, @truncate(machine.ticks())), + }); + var it = rt.tasks(); + while (it.next()) |t| { + machine.print( + "MARK IO_P4_TASK id=%u state=%u futex=0x%08x deadline=%u join=%u cancel=%u stack=%u/%u\r\n", + .{ + @as(u32, t.id), + @as(u32, @intFromEnum(t.state)), + @as(u32, if (t.futex) |p| @truncate(@intFromPtr(p)) else 0), + @as(u32, @truncate(t.deadline orelse 0)), + @as(u32, if (t.join) |j| j.id else 255), + @as(u32, @intFromBool(t.cancel_pending)), + @as(u32, @intCast(rt.stackUsed(t))), + @as(u32, @intCast(t.stack.len)), + }, + ); + } + } + + // ---------------------------------------------------------------- the run queue + + /// Append to the run queue. Caller holds the interrupt mask. + fn push(rt: *Runtime, t: *Task) void { + t.next = null; + if (rt.ready_tail) |tail| tail.next = t else rt.ready_head = t; + rt.ready_tail = t; + } + + /// Take the front of the run queue. Caller holds the interrupt mask. + fn pop(rt: *Runtime) ?*Task { + const t = rt.ready_head orelse return null; + rt.ready_head = t.next; + if (rt.ready_head == null) rt.ready_tail = null; + t.next = null; + return t; + } + + /// Make a blocked task runnable. Caller holds the interrupt mask. + /// + /// Leaves `futex` and `deadline` set: the waiting task clears its own, after it resumes, and + /// the `.blocked` check here is what stops a second waker counting the same waiter twice. + fn wake(rt: *Runtime, t: *Task) void { + assert(t.state == .blocked); + t.state = .ready; + rt.push(t); + } + + fn free(rt: *Runtime) ?*Task { + for (rt.slots) |*t| if (t.state == .free) return t; + return null; + } + + /// Wake every task whose deadline has arrived. Caller holds the interrupt mask. + fn expire(rt: *Runtime, now_ticks: u64) void { + var it = rt.tasks(); + while (it.next()) |t| { + if (t.state != .blocked) continue; + const d = t.deadline orelse continue; + if (now_ticks >= d) rt.wake(t); + } + } + + /// The soonest deadline anybody is waiting for. Caller holds the interrupt mask. + fn earliest(rt: *Runtime) ?u64 { + var best: ?u64 = null; + var it = rt.tasks(); + while (it.next()) |t| { + if (t.state != .blocked) continue; + const d = t.deadline orelse continue; + if (best == null or d < best.?) best = d; + } + return best; + } + + // ---------------------------------------------------------------- the switch + + /// Give up the CPU. The caller must already have put itself into the state it wants to be found + /// in: `.ready` and on the queue (a yield), or `.blocked` with a wake condition set. + /// + /// Returns when something switches back to this task. `rt.current` is set by whoever switches, + /// before the switch, so on the way back it is already correct. + fn reschedule(rt: *Runtime) void { + const me = rt.current; + assert(me.state != .running); // would be a task that gave up the CPU and stayed runnable + var idle_since: ?u64 = null; + while (true) { + const guard = machine.mask(); + rt.expire(machine.ticks()); + const next = rt.pop(); + if (next) |n| { + if (n == me) { + // An interrupt handler re-queued us before we got here, or we were the only + // runnable task. Either way there is nothing to switch to. + me.state = .running; + guard.release(); + return; + } + rt.current = n; + n.state = .running; + guard.release(); + rt.checkStack(me); + var s: Switch = .{ .old = &me.ctx, .new = &n.ctx }; + context.contextSwitch(&s); + return; + } + const deadline = rt.earliest(); + guard.release(); + rt.waitIdle(deadline, &idle_since); + } + } + + /// Like `reschedule`, for a task that is never coming back. Its `Context` is still needed as + /// somewhere for the switch to write, and is dead the instant the switch completes. + fn retire(rt: *Runtime, me: *Task) noreturn { + var idle_since: ?u64 = null; + while (true) { + const guard = machine.mask(); + rt.expire(machine.ticks()); + const next = rt.pop(); + if (next) |n| { + assert(n != me); // a finished task cannot be runnable + rt.current = n; + n.state = .running; + guard.release(); + var s: Switch = .{ .old = &me.ctx, .new = &n.ctx }; + context.contextSwitch(&s); + @panic("io.p4: a finished task was resumed"); + } + const deadline = rt.earliest(); + guard.release(); + rt.waitIdle(deadline, &idle_since); + } + } + + /// Nothing is runnable. With a deadline that is an ordinary sleep. Without one, the only thing + /// that can save this hart is an interrupt handler calling `futexWake` - a legitimate thing to + /// be waiting for, on a chip whose SDIO slave signals on a GPIO - so give it a bounded chance + /// and then say what happened, loudly, rather than spinning silently forever. + fn waitIdle(rt: *Runtime, deadline: ?u64, idle_since: *?u64) void { + if (deadline == null) { + const now_ticks = machine.ticks(); + if (idle_since.*) |since| { + if (now_ticks -% since > rt.deadlock_ticks) rt.deadlock(); + } else { + idle_since.* = now_ticks; + } + } else { + idle_since.* = null; + } + machine.idle(deadline); + } + + fn checkStack(rt: *Runtime, t: *Task) void { + if (t.stack.len == 0) return; + if (t.stack[0] == paint) return; + machine.print("MARK IO_P4_STACK_OVERFLOW id=%u size=%u\r\n", .{ + @as(u32, t.id), @as(u32, @intCast(t.stack.len)), + }); + rt.dump(); + @panic("io.p4: task stack overflow"); + } + + fn deadlock(rt: *Runtime) noreturn { + machine.print("MARK IO_P4_DEADLOCK no runnable task and no deadline\r\n", .{}); + rt.dump(); + @panic("io.p4: deadlock - every task is blocked and nothing is scheduled to wake them"); + } + + // ---------------------------------------------------------------- time + + fn clockOffset(rt: *const Runtime, clock: Io.Clock) i96 { + return switch (clock) { + .real => rt.real_epoch_offset_ns, + // The systimer's own epoch. `awake` and `boot` are the same thing on a chip that does + // not suspend, and there is no per-task CPU accounting, so the two `cpu_*` clocks + // answer with elapsed time; `clockResolution` reports 0 for them to say so. + .awake, .boot, .cpu_process, .cpu_thread => 0, + }; + } + + /// Absolute machine tick a `Timeout` expires at, or null for `.none`. + fn deadlineOf(rt: *const Runtime, timeout: Io.Timeout) ?u64 { + return switch (timeout) { + .none => null, + .duration => |d| machine.ticks() +| ticksFromNs(d.raw.nanoseconds), + .deadline => |ts| ticksFromNs(ts.raw.nanoseconds - rt.clockOffset(ts.clock)), + }; + } + + // ---------------------------------------------------------------- tasks + + /// Reserve a slot for `start`, with its argument tuple and result storage carved off the top of + /// the slot's own stack. Returns null if there is no free slot, or the slot's stack is too + /// small to hold the copies and still be a stack - in both cases the caller's fallback is to + /// run the work eagerly. + fn claim( + rt: *Runtime, + result_len: usize, + result_alignment: Alignment, + arg: []const u8, + arg_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + ) ?*Task { + const guard = machine.mask(); + defer guard.release(); + + const t = rt.free() orelse return null; + + const base = @intFromPtr(t.stack.ptr); + const result_at = result_alignment.backward(base + t.stack.len - result_len); + const arg_at = arg_alignment.backward(result_at - arg.len); + if (arg_at < base or arg_at - base < min_stack_bytes + context.initial_stack_overhead) + return null; + + const arg_ptr: [*]u8 = @ptrFromInt(arg_at); + @memcpy(arg_ptr[0..arg.len], arg); + const result_ptr: [*]u8 = @ptrFromInt(result_at); + + t.state = .ready; + t.next = null; + t.futex = null; + t.deadline = null; + t.join = null; + t.awaiter = null; + t.interruptible = false; + t.cancel_pending = false; + t.cancel_acked = false; + t.protection = .unblocked; + t.start = start; + t.arg = @ptrCast(arg_ptr); + t.result = result_ptr[0..result_len]; + t.ctx = context.initial(arg_at, taskEntry); + rt.push(t); + return t; + } + + /// Wait for `child` to finish, collect its result, and give the slot back. + /// + /// `request` is `cancel` rather than `await`. The subtle case is the other one: a cancel + /// request that lands on *us* while we are waiting. `await` has no error to return it through, + /// so the request is forwarded to the child instead, and afterwards re-armed on us if the child + /// never actually observed it - which is what `Threaded.await` does with `recancelInner` + /// (`/usr/lib/zig/std/Io/Threaded.zig:2465-2473`). + fn joinChild(rt: *Runtime, child: *Task, result: []u8, request: bool) void { + const me = rt.current; + assert(child != me); + assert(child.result.len == result.len); + + if (request) rt.requestCancel(child); + var inherited = false; + + while (true) { + if (!request and !inherited and consumeCancel(me)) { + inherited = true; + rt.requestCancel(child); + } + const guard = machine.mask(); + if (child.state == .done) { + guard.release(); + break; + } + child.awaiter = me; + me.join = child; + me.interruptible = !(request or inherited); + me.state = .blocked; + guard.release(); + rt.reschedule(); + me.join = null; + } + + if (result.len != 0) @memcpy(result, child.result); + const acked = child.cancel_acked; + child.awaiter = null; + child.state = .free; + if (inherited and !acked) me.cancel_pending = true; + } + + /// Arm a cancel request on `t`, and end its current wait if that wait is cancelable. + fn requestCancel(rt: *Runtime, t: *Task) void { + const guard = machine.mask(); + defer guard.release(); + switch (t.state) { + .free, .done => return, + .ready, .running, .blocked => {}, + } + t.cancel_pending = true; + if (t.state == .blocked and t.interruptible) rt.wake(t); + } + + /// Park until somebody wakes `ptr`, the deadline arrives, or - if `interruptible` - a cancel + /// request lands. Returns without parking if the value already differs from `expected`, which + /// is a futex's `EAGAIN` and is what keeps a waker that got there first from being lost. + fn futexBlock( + rt: *Runtime, + me: *Task, + ptr: *const u32, + expected: u32, + deadline: ?u64, + interruptible: bool, + ) void { + const guard = machine.mask(); + if (@atomicLoad(u32, ptr, .acquire) != expected) { + guard.release(); + return; + } + if (deadline) |d| if (machine.ticks() >= d) { + guard.release(); + return; + }; + me.futex = ptr; + me.deadline = deadline; + me.interruptible = interruptible; + me.state = .blocked; + guard.release(); + rt.reschedule(); + me.futex = null; + me.deadline = null; + } +}; + +/// Where a task begins. Jumped to, not called: there is no return address and no argument, so the +/// task finds itself through `installed` (see that declaration for why). +fn taskEntry() callconv(.c) noreturn { + const rt = installed.?; + const me = rt.current; + me.start(me.arg, @ptrCast(me.result.ptr)); + + { + const guard = machine.mask(); + defer guard.release(); + me.state = .done; + if (me.awaiter) |a| if (a.state == .blocked and a.join == me) rt.wake(a); + } + rt.checkStack(me); + rt.retire(me); +} + +/// True if `t` has an undelivered cancel request that is not blocked by cancel protection, in which +/// case the request is consumed: only the *next* cancelation point reports it (`Io.zig:1185-1188`). +fn consumeCancel(t: *Task) bool { + if (t.protection == .blocked) return false; + if (!t.cancel_pending) return false; + t.cancel_pending = false; + t.cancel_acked = true; + return true; +} + +fn cast(userdata: ?*anyopaque) *Runtime { + return @ptrCast(@alignCast(userdata)); +} + +// -------------------------------------------------------------------- tick arithmetic + +/// Machine ticks to nanoseconds, exactly. 16 MHz is 62.5 ns per tick, so the conversion is +/// `* 125 / 2` and not a multiply by 62 - which would drift by 0.8%, i.e. 43 seconds a day. +fn nsFromTicks(t: u64) i96 { + return @divTrunc(@as(i96, t) * 125, 2); +} + +/// Nanoseconds to machine ticks, rounded **up**, so a deadline is never reached early. Saturates +/// rather than wrapping or trapping: `Duration.max` is `maxInt(i96)` nanoseconds, which is a +/// request never to wake, and the largest tick count this returns is 36,000 years at 16 MHz. +fn ticksFromNs(ns: i96) u64 { + if (ns <= 0) return 0; + const limit: i96 = @divFloor(@as(i96, std.math.maxInt(u64)) * 125, 2); + if (ns >= limit) return std.math.maxInt(u64); + return @intCast(@divFloor(ns * 2 + 124, 125)); +} + +// -------------------------------------------------------------------- entropy + +/// The random source behind `random`. +/// +/// `WDEV_RND_REG` is real on this die - it is `LP_SYSTEM_REG_RNG_DATA_REG`, and `esp_random` reads +/// exactly it (`chip.zig`) - but the *entropy* behind it is not established for this image, and +/// this file will not claim otherwise. ESP-IDF's own documentation is precise about the conditions +/// (`docs/en/api-reference/system/random.rst:37-46`): the hardware produces true random numbers +/// while the RF subsystem is enabled, or while `bootloader_random_enable` has the SAR ADC entropy +/// source on, or while the IDF second-stage bootloader is running - "if none of the above +/// conditions are true, the output of the RNG should be considered as pseudo-random only". The P4 +/// has no RF at all, this image is not started by the IDF bootloader, and nothing here calls +/// `bootloader_random_enable`. There is a hardware secondary source that is always mixed in - an +/// asynchronous oscillator sampled for metastability (`random.rst:98-101`) - which is why the +/// register is worth reading at all, but "always mixed in" is not the same as "measured on this +/// board", and I cannot measure this board. +/// +/// So: **`random` is not cryptographic**, and says so. It is a PCG32 (`std.Random.Pcg`) seeded from +/// four hardware words mixed with the systimer and the cycle counter, re-stirred with a fresh +/// hardware word whenever the pacing interval has elapsed. `randomSecure` reports +/// `error.EntropyUnavailable` rather than pretending. Closing that gap is a small, separate job: +/// port `bootloader_random_enable`, then measure. +const Rng = struct { + prng: std.Random.Pcg = undefined, + seeded: bool = false, + last_sample: u64 = 0, + + /// ESP-IDF paces `esp_random` at one *byte* per 14 microseconds on this part - + /// `APB_CYCLE_WAIT_NUM` is `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ * 14` APB cycles per byte + /// (`hw_random.c:41-44`), which at the P4's 1:1 CPU:APB ratio is the ~75 kHz byte rate the + /// comment there says the RNG was tested at. Draining it faster returns the generator's state + /// rather than new entropy. Expressed in systimer ticks because that is the clock this file + /// trusts, and because the constant in IDF assumes a 360 MHz CPU that this board is not + /// running. + const sample_ticks: u64 = machine.ticks_hz * 14 / 1_000_000; + + fn fill(r: *Rng, buffer: []u8) void { + if (!r.seeded) r.seed(); + r.stir(); + r.prng.fill(buffer); + } + + /// Four paced hardware words, which costs ~56 microseconds of spinning, once, on the first call + /// to `random`. Deliberately not done in `init`: a runtime that never asks for randomness + /// should not pay for it, and `init` runs before anything can tolerate a delay. + fn seed(r: *Rng) void { + var s: u64 = machine.noise() ^ (machine.ticks() << 17); + for (0..4) |_| { + s = s *% 0x9E37_79B9_7F4A_7C15 ^ r.sample(); + } + r.prng = .init(s); + r.seeded = true; + } + + /// Mix in a fresh hardware word if one is due. Never waits: the caller asked for random bytes, + /// not for a delay, and the generator is sound without it. + fn stir(r: *Rng) void { + const now_ticks = machine.ticks(); + if (now_ticks -% r.last_sample < sample_ticks) return; + r.last_sample = now_ticks; + r.prng.s ^= machine.entropyWord(); + } + + /// One hardware word, waiting out the pacing interval first. + fn sample(r: *Rng) u32 { + const target = r.last_sample +% sample_ticks; + while (machine.ticks() -% r.last_sample < sample_ticks) machine.idle(target); + r.last_sample = machine.ticks(); + return machine.entropyWord(); + } +}; + +// -------------------------------------------------------------------- the vtable + +/// Start from "nothing is implemented" and overwrite what is. The list of assignments below is the +/// authoritative answer to "what works": anything not named here panics with its own name. +const vtable: Io.VTable = build: { + var v = unimplemented.vtable; + + v.now = now; + v.clockResolution = clockResolution; + v.sleep = sleep; + + v.async = asyncTask; + v.concurrent = concurrent; + v.await = awaitTask; + v.cancel = cancelTask; + + v.futexWait = futexWait; + v.futexWaitUncancelable = futexWaitUncancelable; + v.futexWake = futexWake; + + v.checkCancel = checkCancel; + v.recancel = recancel; + v.swapCancelProtection = swapCancelProtection; + + v.crashHandler = crashHandler; + v.random = random; + v.randomSecure = randomSecure; + + break :build v; +}; + +fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Timestamp { + const rt = cast(userdata); + return .{ .nanoseconds = nsFromTicks(machine.ticks()) + rt.clockOffset(clock) }; +} + +fn clockResolution(_: ?*anyopaque, clock: Io.Clock) Io.Clock.ResolutionError!Io.Duration { + return switch (clock) { + // 1e9/16e6 = 62.5 ns, truncated: `Duration` counts whole nanoseconds and the tick is not a + // whole number of them. The conversion in `nsFromTicks` keeps the half. + .real, .awake, .boot => .fromNanoseconds(@divTrunc(std.time.ns_per_s, @as(i96, machine.ticks_hz))), + // Zero means "unsupported" (`Io.zig:787-790`). There is one process and no per-task CPU + // accounting, so `now` answers these with elapsed time and this says not to believe it. + .cpu_process, .cpu_thread => .zero, + }; +} + +fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.Cancelable!void { + // `.none` means no timeout at all, i.e. nothing to wait for. Matches `Threaded.sleep` + // (`Threaded.zig:11577`), and notably is *not* a cancelation point. + if (timeout == .none) return; + + const rt = cast(userdata); + const me = rt.current; + if (consumeCancel(me)) return error.Canceled; + const deadline = rt.deadlineOf(timeout).?; + + while (true) { + { + const guard = machine.mask(); + defer guard.release(); + if (machine.ticks() >= deadline) { + // The deadline has already passed, but a `sleep` that returns without ever leaving + // the CPU makes `io.sleep(.zero, .awake)` useless as a yield - and it is the only + // yield `std.Io` exposes. So go round the run queue exactly once. + me.deadline = null; + me.interruptible = true; + me.state = .ready; + rt.push(me); + } else { + me.deadline = deadline; + me.interruptible = true; + me.state = .blocked; + } + } + rt.reschedule(); + me.deadline = null; + if (consumeCancel(me)) return error.Canceled; + if (machine.ticks() >= deadline) return; + } +} + +fn asyncTask( + userdata: ?*anyopaque, + result: []u8, + result_alignment: Alignment, + arg: []const u8, + arg_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*Io.AnyFuture { + const rt = cast(userdata); + const t = rt.claim(result.len, result_alignment, arg, arg_alignment, start) orelse { + // No slot: run it here and now. `await` will be a no-op (`Io.zig:54-56`). + start(arg.ptr, result.ptr); + return null; + }; + return @ptrCast(t); +} + +fn concurrent( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: Alignment, + arg: []const u8, + arg_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) Io.ConcurrentError!*Io.AnyFuture { + const rt = cast(userdata); + // `concurrent` promises the caller can block on something the task will unblock. A cooperative + // task can do that - it runs whenever the caller blocks - so the only failure is running out of + // slots. Unlike `async` there is no eager fallback: running the body inline is exactly the + // guarantee `concurrent` exists to rule out. + const t = rt.claim(result_len, result_alignment, arg, arg_alignment, start) orelse + return error.ConcurrencyUnavailable; + return @ptrCast(t); +} + +fn awaitTask(userdata: ?*anyopaque, any_future: *Io.AnyFuture, result: []u8, _: Alignment) void { + const rt = cast(userdata); + rt.joinChild(@ptrCast(@alignCast(any_future)), result, false); +} + +fn cancelTask(userdata: ?*anyopaque, any_future: *Io.AnyFuture, result: []u8, _: Alignment) void { + const rt = cast(userdata); + rt.joinChild(@ptrCast(@alignCast(any_future)), result, true); +} + +fn futexWait(userdata: ?*anyopaque, ptr: *const u32, expected: u32, timeout: Io.Timeout) Io.Cancelable!void { + const rt = cast(userdata); + const me = rt.current; + // Cancelation is checked before the value, which is the order `Threaded` gets from calling + // `Syscall.start()` before the futex syscall (`Threaded.zig:986`): an already-canceled task + // does not acquire a lock on its way out. + if (consumeCancel(me)) return error.Canceled; + rt.futexBlock(me, ptr, expected, rt.deadlineOf(timeout), true); + if (consumeCancel(me)) return error.Canceled; +} + +fn futexWaitUncancelable(userdata: ?*anyopaque, ptr: *const u32, expected: u32) void { + const rt = cast(userdata); + rt.futexBlock(rt.current, ptr, expected, null, false); +} + +fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void { + const rt = cast(userdata); + const guard = machine.mask(); + defer guard.release(); + var woken: u32 = 0; + var it = rt.tasks(); + while (it.next()) |t| { + if (woken >= max_waiters) break; + if (t.state != .blocked) continue; + if (t.futex != ptr) continue; + rt.wake(t); + woken += 1; + } +} + +fn checkCancel(userdata: ?*anyopaque) Io.Cancelable!void { + if (consumeCancel(cast(userdata).current)) return error.Canceled; +} + +fn recancel(userdata: ?*anyopaque) void { + const me = cast(userdata).current; + assert(!me.cancel_pending); // `recancel` with a request already pending + me.cancel_pending = true; +} + +fn swapCancelProtection(userdata: ?*anyopaque, new: Io.CancelProtection) Io.CancelProtection { + const me = cast(userdata).current; + const old = me.protection; + me.protection = new; + return old; +} + +/// Called from `std.debug`'s panic path (`/usr/lib/zig/std/debug.zig:536`) and from the segfault +/// handler (`:1641`), both of which go on to print the panic themselves. +/// +/// So this prints and **returns**; it does not park. Parking here would swallow the panic message, +/// which is the one thing worth having. No allocation, no lock, no scheduling: `dump` is straight +/// `ets_printf`, and marking the crashing task cancel-protected keeps any cleanup that runs after +/// this from being interrupted by a cancelation - the same thing `Threaded.crashHandler` does +/// (`Threaded.zig:2066-2072`). +fn crashHandler(userdata: ?*anyopaque) void { + const rt = cast(userdata); + rt.current.cancel_pending = false; + rt.current.protection = .blocked; + machine.print("MARK IO_P4_CRASH\r\n", .{}); + rt.dump(); +} + +fn random(userdata: ?*anyopaque, buffer: []u8) void { + cast(userdata).rng.fill(buffer); +} + +/// See `Rng`: the hardware register is real, its entropy on this image is not established, and +/// guessing is worse than saying so. +fn randomSecure(_: ?*anyopaque, _: []u8) Io.RandomSecureError!void { + return error.EntropyUnavailable; +} + +// -------------------------------------------------------------------- static declaration helper + +/// A whole runtime as one `.bss` object: `task_count` stacks of `stack_bytes` each, the slots that +/// describe them, and the `Runtime`. +/// +/// ``` +/// var pool: p4.Static(7, 5 * 1024) = .{}; +/// const io = pool.init(.{}).io(); +/// ``` +pub fn Static(comptime task_count: usize, comptime stack_bytes: usize) type { + comptime { + if (stack_bytes < min_stack_bytes) @compileError("stack_bytes is smaller than min_stack_bytes"); + if (stack_bytes % context.stack_align != 0) @compileError( + "stack_bytes must be a multiple of the ABI's stack alignment, so that every slot's " ++ + "stack top is aligned without waste", + ); + } + return struct { + stacks: [task_count][stack_bytes]u8 align(context.stack_align) = undefined, + slots: [task_count]Task = undefined, + runtime: Runtime = undefined, + + /// Total `.bss` this declaration costs. + pub const size = @sizeOf(@This()); + + pub fn init(self: *@This(), opts: Options) *Runtime { + for (&self.slots, &self.stacks) |*slot, *stack| slot.* = .{ .stack = stack }; + self.runtime.init(&self.slots, opts); + return &self.runtime; + } + }; +} + +// -------------------------------------------------------------------- tests +// +// These run on the host, against `host.zig`'s virtual clock and `std.Io.fiber`'s x86-64 context +// switch, which is the same scheduler with a different two files underneath it. What they cannot +// test is the riscv32 asm in `context.zig` - that only runs on the die. + +const testing = std.testing; + +/// Deliberately file-scope: 8 KiB stacks in a test function's frame is not what a stack is for. +/// Unreferenced outside tests, so it is not analysed - let alone emitted - in a firmware build. +var test_pool: Static(4, 8 * 1024) = .{}; + +fn testRuntime() *Runtime { + return test_pool.init(.{}); +} + +const Trace = struct { + buf: [64]u8 = undefined, + len: usize = 0, + + fn put(t: *Trace, c: u8) void { + t.buf[t.len] = c; + t.len += 1; + } + fn seen(t: *const Trace) []const u8 { + return t.buf[0..t.len]; + } +}; + +/// The numbers quoted in this file's header. Not a tuning knob - a regression alarm: `Task` growing +/// silently is how a 35 KiB budget becomes 40. The chip's figures are the 32-bit column, and they +/// were read off a real riscv32 build; the host runs the 64-bit column so that this test is not +/// vacuous where it can actually run. +pub const footprint = struct { + pub const task_bytes: usize = if (@sizeOf(usize) == 4) 80 else 128; + pub const runtime_bytes: usize = if (@sizeOf(usize) == 4) 152 else 216; + pub const context_bytes: usize = 3 * @sizeOf(usize); +}; + +test footprint { + try testing.expectEqual(footprint.task_bytes, @sizeOf(Task)); + try testing.expectEqual(footprint.runtime_bytes, @sizeOf(Runtime)); + try testing.expectEqual(footprint.context_bytes, @sizeOf(Context)); +} + +fn tick(io: Io, trace: *Trace, mark: u8, rounds: usize) void { + for (0..rounds) |_| { + trace.put(mark); + io.sleep(.zero, .awake) catch return; + } +} + +test "three tasks round-robin through the scheduler" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + var a = io.async(tick, .{ io, &trace, 'a', 3 }); + var b = io.async(tick, .{ io, &trace, 'b', 3 }); + var c = io.async(tick, .{ io, &trace, 'c', 3 }); + a.await(io); + b.await(io); + c.await(io); + + // Spawn order is queue order, and each task yields after every mark, so the interleaving is + // exact. `main` awaits `a` first and so is not in the rotation. + try testing.expectEqualStrings("abcabcabc", trace.seen()); +} + +fn locker(io: Io, m: *Io.Mutex, trace: *Trace, mark: u8) void { + m.lock(io) catch return; + defer m.unlock(io); + trace.put(mark); + // Hold the lock across a yield, so the other task must actually block on the futex rather than + // finding it free. + io.sleep(.zero, .awake) catch {}; + trace.put(std.ascii.toUpper(mark)); +} + +test "std.Io.Mutex serialises two tasks through the futex" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + var m: Io.Mutex = .init; + + var a = io.async(locker, .{ io, &m, &trace, 'a' }); + var b = io.async(locker, .{ io, &m, &trace, 'b' }); + a.await(io); + b.await(io); + + // Interleaved would be "abAB"; serialised is each task's pair adjacent. + try testing.expectEqualStrings("aAbB", trace.seen()); + try testing.expect(m.tryLock()); +} + +fn producer(io: Io, q: *Io.Queue(u32), n: u32) void { + var i: u32 = 0; + while (i < n) : (i += 1) q.putOne(io, i) catch return; + q.close(io); +} + +fn consumer(io: Io, q: *Io.Queue(u32), sum: *u32) void { + while (true) { + const v = q.getOne(io) catch return; + sum.* += v; + } +} + +test "std.Io.Queue passes items between two tasks" { + const rt = testRuntime(); + const io = rt.io(); + // Capacity 2 against 10 items, so both directions block and both directions get woken. + var storage: [2]u32 = undefined; + var q: Io.Queue(u32) = .init(&storage); + var sum: u32 = 0; + + var p = io.async(producer, .{ io, &q, 10 }); + var c = io.async(consumer, .{ io, &q, &sum }); + p.await(io); + c.await(io); + + try testing.expectEqual(@as(u32, 45), sum); +} + +fn napper(io: Io, trace: *Trace, mark: u8, ms: u64) void { + io.sleep(.fromMilliseconds(@intCast(ms)), .awake) catch return; + trace.put(mark); +} + +test "sleep orders three tasks by deadline, not by spawn order" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + const t0 = Io.Timestamp.now(io, .awake); + var a = io.async(napper, .{ io, &trace, 'a', @as(u64, 30) }); + var b = io.async(napper, .{ io, &trace, 'b', @as(u64, 10) }); + var c = io.async(napper, .{ io, &trace, 'c', @as(u64, 20) }); + a.await(io); + b.await(io); + c.await(io); + + try testing.expectEqualStrings("bca", trace.seen()); + // And the clock really moved, by at least the longest sleep. + const elapsed = t0.durationTo(Io.Timestamp.now(io, .awake)); + try testing.expect(elapsed.toMilliseconds() >= 30); +} + +fn napAndCatch(io: Io, trace: *Trace) void { + trace.put('s'); + io.sleep(.fromMilliseconds(1000), .awake) catch |err| switch (err) { + error.Canceled => { + trace.put('x'); + return; + }, + }; + trace.put('e'); // must not be reached +} + +test "a canceled task does not run to completion" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + var f = io.async(napAndCatch, .{ io, &trace }); + // The body has not run yet - `async` only assigned a slot - so let it reach its sleep first. + rt.yield(); + try testing.expectEqualStrings("s", trace.seen()); + + f.cancel(io); + try testing.expectEqualStrings("sx", trace.seen()); +} + +test "cancel before the body ever runs still runs it, and its first cancelation point reports" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + var f = io.async(napAndCatch, .{ io, &trace }); + f.cancel(io); + // Same as `Threaded`: the function always runs; cancelation is delivered at the first + // cancelation point inside it. + try testing.expectEqualStrings("sx", trace.seen()); +} + +fn protected(io: Io, trace: *Trace) void { + const old = io.swapCancelProtection(.blocked); + io.sleep(.fromMilliseconds(5), .awake) catch unreachable; // protection is on: cannot fail + trace.put('p'); + _ = io.swapCancelProtection(old); + io.checkCancel() catch { + trace.put('x'); + return; + }; + trace.put('e'); +} + +test "cancel protection defers delivery to the next unprotected point" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + var f = io.async(protected, .{ io, &trace }); + f.cancel(io); + try testing.expectEqualStrings("px", trace.seen()); +} + +fn waiter(io: Io, word: *std.atomic.Value(u32), trace: *Trace) void { + while (word.load(.acquire) == 0) { + io.futexWait(u32, &word.raw, 0) catch return; + } + trace.put('w'); +} + +fn poster(io: Io, word: *std.atomic.Value(u32), trace: *Trace) void { + trace.put('p'); + word.store(1, .release); + io.futexWake(u32, &word.raw, 1); +} + +test "futexWait parks and futexWake releases exactly one waiter" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + var word: std.atomic.Value(u32) = .init(0); + + var w = io.async(waiter, .{ io, &word, &trace }); + var p = io.async(poster, .{ io, &word, &trace }); + w.await(io); + p.await(io); + + try testing.expectEqualStrings("pw", trace.seen()); +} + +test "futexWait returns immediately when the value already differs" { + const rt = testRuntime(); + const io = rt.io(); + var word: std.atomic.Value(u32) = .init(7); + // Would deadlock if it parked: nobody is going to wake it. + try io.futexWait(u32, &word.raw, 0); +} + +test "futexWaitTimeout unblocks on its deadline with nothing else runnable" { + const rt = testRuntime(); + const io = rt.io(); + var word: std.atomic.Value(u32) = .init(0); + + // Nobody is going to wake this, so the deadline has to - which also drives the scheduler's + // "nothing runnable, one deadline pending" idle path from the main task, the same path the + // deadlock watchdog must *not* fire on. + const t0 = Io.Timestamp.now(io, .awake); + try io.futexWaitTimeout(u32, &word.raw, 0, .{ .duration = .{ + .raw = .fromMilliseconds(25), + .clock = .awake, + } }); + const elapsed = t0.durationTo(Io.Timestamp.now(io, .awake)); + try testing.expect(elapsed.toMilliseconds() >= 25); + try testing.expectEqual(@as(u32, 0), word.load(.monotonic)); +} + +fn add(a: u32, b: u32) u32 { + return a + b; +} + +test "async falls back to running eagerly when every slot is taken" { + const rt = testRuntime(); + const io = rt.io(); + + // Fill all four slots with tasks that will not finish until woken. + var word: std.atomic.Value(u32) = .init(0); + var trace: Trace = .{}; + var held: [4]Io.Future(void) = undefined; + for (&held) |*f| f.* = io.async(waiter, .{ io, &word, &trace }); + + var eager = io.async(add, .{ 20, 22 }); + try testing.expectEqual(@as(?*Io.AnyFuture, null), eager.any_future); + try testing.expectEqual(@as(u32, 42), eager.await(io)); + + word.store(1, .release); + io.futexWake(u32, &word.raw, 4); + for (&held) |*f| f.await(io); + try testing.expectEqualStrings("wwww", trace.seen()); +} + +test "concurrent reports ConcurrencyUnavailable instead of running inline" { + const rt = testRuntime(); + const io = rt.io(); + + var word: std.atomic.Value(u32) = .init(0); + var trace: Trace = .{}; + var held: [4]Io.Future(void) = undefined; + for (&held) |*f| f.* = io.async(waiter, .{ io, &word, &trace }); + + try testing.expectError(error.ConcurrencyUnavailable, io.concurrent(add, .{ 1, 2 })); + + word.store(1, .release); + io.futexWake(u32, &word.raw, 4); + for (&held) |*f| f.await(io); +} + +test "now is monotonic and converts ticks exactly" { + const rt = testRuntime(); + const io = rt.io(); + + const a = Io.Timestamp.now(io, .awake); + try io.sleep(.fromMilliseconds(7), .awake); + const b = Io.Timestamp.now(io, .awake); + try testing.expect(b.nanoseconds >= a.nanoseconds); + try testing.expect(a.durationTo(b).toMilliseconds() >= 7); + + // 62.5 ns a tick, kept exact: one tick is 62 ns and two are 125, not 124. + try testing.expectEqual(@as(i96, 62), nsFromTicks(1)); + try testing.expectEqual(@as(i96, 125), nsFromTicks(2)); + try testing.expectEqual(@as(i96, 1_000_000_000), nsFromTicks(machine.ticks_hz)); + // And back, rounding up so a deadline is never early. + try testing.expectEqual(@as(u64, 1), ticksFromNs(1)); + try testing.expectEqual(@as(u64, 1), ticksFromNs(62)); + try testing.expectEqual(@as(u64, 2), ticksFromNs(63)); + try testing.expectEqual(@as(u64, machine.ticks_hz), ticksFromNs(1_000_000_000)); + try testing.expectEqual(@as(u64, std.math.maxInt(u64)), ticksFromNs(Io.Duration.max.nanoseconds)); + + // `real` is the same counter plus an offset the caller sets; `cpu_*` report resolution 0 to say + // they are not really implemented. + try testing.expectEqual(Io.Duration{ .nanoseconds = 62 }, try io.vtable.clockResolution(io.userdata, .awake)); + try testing.expectEqual(Io.Duration.zero, try io.vtable.clockResolution(io.userdata, .cpu_thread)); + rt.real_epoch_offset_ns = 1_700_000_000 * std.time.ns_per_s; + try testing.expect(Io.Timestamp.now(io, .real).toSeconds() > 1_600_000_000); + rt.real_epoch_offset_ns = 0; +} + +test "sleep(.none) is not a wait and not a cancelation point" { + const rt = testRuntime(); + const io = rt.io(); + rt.current.cancel_pending = true; + try io.vtable.sleep(io.userdata, .none); + try testing.expect(rt.current.cancel_pending); + rt.current.cancel_pending = false; +} + +test "random fills, is not all zero, and does not repeat itself" { + const rt = testRuntime(); + const io = rt.io(); + var a: [32]u8 = @splat(0); + var b: [32]u8 = @splat(0); + io.random(&a); + io.random(&b); + try testing.expect(!std.mem.allEqual(u8, &a, 0)); + try testing.expect(!std.mem.eql(u8, &a, &b)); + // The one thing this port will not pretend about. + try testing.expectError(error.EntropyUnavailable, io.randomSecure(&a)); +} + +test "a task's stack high-water mark is measurable" { + const rt = testRuntime(); + const io = rt.io(); + var f = io.async(add, .{ 1, 2 }); + _ = f.await(io); + // Slot 1 ran `add` through the trampoline; something was written, and nowhere near 8 KiB. + const used = rt.stackUsed(&rt.slots[0]); + try testing.expect(used > 0); + try testing.expect(used < 8 * 1024); + try testing.expectEqual(@as(usize, 0), rt.stackUsed(&rt.main)); +} + +fn childAcks(io: Io, trace: *Trace) void { + io.sleep(.fromMilliseconds(1000), .awake) catch { + trace.put('c'); + return; + }; + trace.put('C'); +} + +fn childNeverAcks(io: Io, trace: *Trace) void { + // Blocks, so the parent really has to wait for it, but never observes the cancelation - which + // is the case where the parent has to take its own request back. + const old = io.swapCancelProtection(.blocked); + io.sleep(.fromMilliseconds(5), .awake) catch unreachable; + _ = io.swapCancelProtection(old); + trace.put('C'); +} + +fn parentAwaits(io: Io, trace: *Trace, acks: bool) void { + var f = if (acks) io.async(childAcks, .{ io, trace }) else io.async(childNeverAcks, .{ io, trace }); + f.await(io); + trace.put('r'); + io.checkCancel() catch { + trace.put('x'); + return; + }; + trace.put('e'); +} + +test "a cancel that lands during await is forwarded to the child" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + var f = io.async(parentAwaits, .{ io, &trace, true }); + f.cancel(io); + + // 'c': the child's sleep reported `error.Canceled`, so the child consumed the request. + // 'r': the parent's `await` returned. 'e': and the parent's own next cancelation point is + // *clean*, because the cancelation was delivered into the child rather than to the parent. + try testing.expectEqualStrings("cre", trace.seen()); +} + +test "a cancel the child never acknowledges is re-armed on the parent" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + + var f = io.async(parentAwaits, .{ io, &trace, false }); + f.cancel(io); + + // 'C': the child ran to completion under cancel protection. 'r': await returned. 'x': and the + // request the parent gave away is back, so the parent's next cancelation point reports it - + // `Threaded.await` does the same thing through `recancelInner` (`Threaded.zig:2470`). + try testing.expectEqualStrings("Crx", trace.seen()); +} + +fn condWaiter(io: Io, m: *Io.Mutex, c: *Io.Condition, flag: *bool, trace: *Trace) void { + m.lock(io) catch return; + defer m.unlock(io); + while (!flag.*) c.wait(io, m) catch return; + trace.put('w'); +} + +fn condSignaler(io: Io, m: *Io.Mutex, c: *Io.Condition, flag: *bool, trace: *Trace) void { + m.lock(io) catch return; + flag.* = true; + m.unlock(io); + trace.put('s'); + c.signal(io); +} + +test "std.Io.Condition signals across tasks" { + const rt = testRuntime(); + const io = rt.io(); + var trace: Trace = .{}; + var m: Io.Mutex = .init; + var c: Io.Condition = .init; + var flag = false; + + // `Condition` is the most futex-dependent thing in std: an epoch word, a packed + // waiters/signals state, and a mutex handed back and forth across the wait. + var w = io.async(condWaiter, .{ io, &m, &c, &flag, &trace }); + var s = io.async(condSignaler, .{ io, &m, &c, &flag, &trace }); + w.await(io); + s.await(io); + + try testing.expectEqualStrings("sw", trace.seen()); + try testing.expect(m.tryLock()); +} diff --git a/src/io/unimplemented.zig b/src/io/unimplemented.zig new file mode 100644 index 0000000..566afac --- /dev/null +++ b/src/io/unimplemented.zig @@ -0,0 +1,490 @@ +//! Every `std.Io.VTable` entry this port does not implement, as a function that panics with its +//! own name. +//! +//! `std.Io.VTable` has 109 entries (`/usr/lib/zig/std/Io.zig:51-255`). A port of it to a chip with +//! no filesystem, no sockets and no processes implements sixteen of them, and the honest way to +//! express the other 93 is not a partial struct - the struct has no optional fields - but a complete +//! one whose unimplemented entries announce themselves the moment they are reached. So this file is +//! the floor: `p4.zig` starts from `unimplemented.vtable` and overwrites the entries it really has, +//! and anything it forgot fails with the name of the entry rather than a jump to address zero. +//! +//! A stub for an entry `p4.zig` does implement is dead the moment the assignment overwrites the +//! field, so it costs nothing: a riscv32 `-OReleaseSmall` build emits exactly the 93 that survive. +//! +//! The signatures are transcribed from that VTable declaration verbatim, which is why they carry +//! precision about types nothing here will ever construct: the compiler checks each one against +//! std, so a Zig release that changes a signature - or adds an entry - is a build error in this +//! file instead of a silent gap. The parameter names are dropped to `_` because a stub that reads +//! its arguments would be a lie. +//! +//! Note the five `fileMemoryMap*` entries. Their signatures name `Io.File.MemoryMap`, and merely +//! *defining* a function with that in its type forces the struct to be laid out, which on +//! riscv32-freestanding needs `std.options.page_size_min` - see `p4.zig`'s "Integrating this". + +const std = @import("std"); +const Io = std.Io; + +const Alignment = std.mem.Alignment; +const AnyFuture = Io.AnyFuture; +const Batch = Io.Batch; +const CancelProtection = Io.CancelProtection; +const Cancelable = Io.Cancelable; +const Clock = Io.Clock; +const ConcurrentError = Io.ConcurrentError; +const Dir = Io.Dir; +const Duration = Io.Duration; +const File = Io.File; +const Group = Io.Group; +const LockedStderr = Io.LockedStderr; +const Operation = Io.Operation; +const Queue = Io.Queue; +const RandomSecureError = Io.RandomSecureError; +const Terminal = Io.Terminal; +const Timeout = Io.Timeout; +const Timestamp = Io.Timestamp; +const net = Io.net; + +/// A `std.Io.VTable` in which nothing is implemented. Every entry panics with its own name. +pub const vtable: Io.VTable = .{ + .crashHandler = panics.crashHandler, + .async = panics.async, + .concurrent = panics.concurrent, + .await = panics.await, + .cancel = panics.cancel, + .groupAsync = panics.groupAsync, + .groupConcurrent = panics.groupConcurrent, + .groupAwait = panics.groupAwait, + .groupCancel = panics.groupCancel, + .recancel = panics.recancel, + .swapCancelProtection = panics.swapCancelProtection, + .checkCancel = panics.checkCancel, + .futexWait = panics.futexWait, + .futexWaitUncancelable = panics.futexWaitUncancelable, + .futexWake = panics.futexWake, + .operate = panics.operate, + .batchAwaitAsync = panics.batchAwaitAsync, + .batchAwaitConcurrent = panics.batchAwaitConcurrent, + .batchCancel = panics.batchCancel, + .dirCreateDir = panics.dirCreateDir, + .dirCreateDirPath = panics.dirCreateDirPath, + .dirCreateDirPathOpen = panics.dirCreateDirPathOpen, + .dirOpenDir = panics.dirOpenDir, + .dirStat = panics.dirStat, + .dirStatFile = panics.dirStatFile, + .dirAccess = panics.dirAccess, + .dirCreateFile = panics.dirCreateFile, + .dirCreateFileAtomic = panics.dirCreateFileAtomic, + .dirOpenFile = panics.dirOpenFile, + .dirClose = panics.dirClose, + .dirRead = panics.dirRead, + .dirRealPath = panics.dirRealPath, + .dirRealPathFile = panics.dirRealPathFile, + .dirDeleteFile = panics.dirDeleteFile, + .dirDeleteDir = panics.dirDeleteDir, + .dirRename = panics.dirRename, + .dirRenamePreserve = panics.dirRenamePreserve, + .dirSymLink = panics.dirSymLink, + .dirReadLink = panics.dirReadLink, + .dirSetOwner = panics.dirSetOwner, + .dirSetFileOwner = panics.dirSetFileOwner, + .dirSetPermissions = panics.dirSetPermissions, + .dirSetFilePermissions = panics.dirSetFilePermissions, + .dirSetTimestamps = panics.dirSetTimestamps, + .dirHardLink = panics.dirHardLink, + .fileStat = panics.fileStat, + .fileLength = panics.fileLength, + .fileClose = panics.fileClose, + .fileWritePositional = panics.fileWritePositional, + .fileWriteFileStreaming = panics.fileWriteFileStreaming, + .fileWriteFilePositional = panics.fileWriteFilePositional, + .fileReadPositional = panics.fileReadPositional, + .fileSeekBy = panics.fileSeekBy, + .fileSeekTo = panics.fileSeekTo, + .fileSync = panics.fileSync, + .fileIsTty = panics.fileIsTty, + .fileEnableAnsiEscapeCodes = panics.fileEnableAnsiEscapeCodes, + .fileSupportsAnsiEscapeCodes = panics.fileSupportsAnsiEscapeCodes, + .fileSetLength = panics.fileSetLength, + .fileSetOwner = panics.fileSetOwner, + .fileSetPermissions = panics.fileSetPermissions, + .fileSetTimestamps = panics.fileSetTimestamps, + .fileLock = panics.fileLock, + .fileTryLock = panics.fileTryLock, + .fileUnlock = panics.fileUnlock, + .fileDowngradeLock = panics.fileDowngradeLock, + .fileRealPath = panics.fileRealPath, + .fileHardLink = panics.fileHardLink, + .fileMemoryMapCreate = panics.fileMemoryMapCreate, + .fileMemoryMapDestroy = panics.fileMemoryMapDestroy, + .fileMemoryMapSetLength = panics.fileMemoryMapSetLength, + .fileMemoryMapRead = panics.fileMemoryMapRead, + .fileMemoryMapWrite = panics.fileMemoryMapWrite, + .processExecutableOpen = panics.processExecutableOpen, + .processExecutablePath = panics.processExecutablePath, + .lockStderr = panics.lockStderr, + .tryLockStderr = panics.tryLockStderr, + .unlockStderr = panics.unlockStderr, + .processCurrentPath = panics.processCurrentPath, + .processSetCurrentDir = panics.processSetCurrentDir, + .processSetCurrentPath = panics.processSetCurrentPath, + .processReplace = panics.processReplace, + .processReplacePath = panics.processReplacePath, + .processSpawn = panics.processSpawn, + .processSpawnPath = panics.processSpawnPath, + .childWait = panics.childWait, + .childKill = panics.childKill, + .progressParentFile = panics.progressParentFile, + .now = panics.now, + .clockResolution = panics.clockResolution, + .sleep = panics.sleep, + .random = panics.random, + .randomSecure = panics.randomSecure, + .netListenIp = panics.netListenIp, + .netAccept = panics.netAccept, + .netBindIp = panics.netBindIp, + .netConnectIp = panics.netConnectIp, + .netListenUnix = panics.netListenUnix, + .netConnectUnix = panics.netConnectUnix, + .netSocketCreatePair = panics.netSocketCreatePair, + .netSend = panics.netSend, + .netRead = panics.netRead, + .netWrite = panics.netWrite, + .netWriteFile = panics.netWriteFile, + .netClose = panics.netClose, + .netShutdown = panics.netShutdown, + .netInterfaceNameResolve = panics.netInterfaceNameResolve, + .netInterfaceName = panics.netInterfaceName, + .netLookup = panics.netLookup, +}; + +/// One function per entry. Grouped in a namespace so the panic message and the vtable field +/// cannot drift apart: `panics.dirOpenFile` is the entry named `dirOpenFile`. +const panics = struct { + fn crashHandler(_: ?*anyopaque) void { + @panic("io.p4 unimplemented: crashHandler"); + } + fn async(_: ?*anyopaque, _: []u8, _: std.mem.Alignment, _: []const u8, _: std.mem.Alignment, _: *const fn (context: *const anyopaque, result: *anyopaque) void) ?*AnyFuture { + @panic("io.p4 unimplemented: async"); + } + fn concurrent(_: ?*anyopaque, _: usize, _: std.mem.Alignment, _: []const u8, _: std.mem.Alignment, _: *const fn (context: *const anyopaque, result: *anyopaque) void) ConcurrentError!*AnyFuture { + @panic("io.p4 unimplemented: concurrent"); + } + fn await(_: ?*anyopaque, _: *AnyFuture, _: []u8, _: std.mem.Alignment) void { + @panic("io.p4 unimplemented: await"); + } + fn cancel(_: ?*anyopaque, _: *AnyFuture, _: []u8, _: std.mem.Alignment) void { + @panic("io.p4 unimplemented: cancel"); + } + fn groupAsync(_: ?*anyopaque, _: *Group, _: []const u8, _: std.mem.Alignment, _: *const fn (context: *const anyopaque) void) void { + @panic("io.p4 unimplemented: groupAsync"); + } + fn groupConcurrent(_: ?*anyopaque, _: *Group, _: []const u8, _: std.mem.Alignment, _: *const fn (context: *const anyopaque) void) ConcurrentError!void { + @panic("io.p4 unimplemented: groupConcurrent"); + } + fn groupAwait(_: ?*anyopaque, _: *Group, _: *anyopaque) Cancelable!void { + @panic("io.p4 unimplemented: groupAwait"); + } + fn groupCancel(_: ?*anyopaque, _: *Group, _: *anyopaque) void { + @panic("io.p4 unimplemented: groupCancel"); + } + fn recancel(_: ?*anyopaque) void { + @panic("io.p4 unimplemented: recancel"); + } + fn swapCancelProtection(_: ?*anyopaque, _: CancelProtection) CancelProtection { + @panic("io.p4 unimplemented: swapCancelProtection"); + } + fn checkCancel(_: ?*anyopaque) Cancelable!void { + @panic("io.p4 unimplemented: checkCancel"); + } + fn futexWait(_: ?*anyopaque, _: *const u32, _: u32, _: Timeout) Cancelable!void { + @panic("io.p4 unimplemented: futexWait"); + } + fn futexWaitUncancelable(_: ?*anyopaque, _: *const u32, _: u32) void { + @panic("io.p4 unimplemented: futexWaitUncancelable"); + } + fn futexWake(_: ?*anyopaque, _: *const u32, _: u32) void { + @panic("io.p4 unimplemented: futexWake"); + } + fn operate(_: ?*anyopaque, _: Operation) Cancelable!Operation.Result { + @panic("io.p4 unimplemented: operate"); + } + fn batchAwaitAsync(_: ?*anyopaque, _: *Batch) Cancelable!void { + @panic("io.p4 unimplemented: batchAwaitAsync"); + } + fn batchAwaitConcurrent(_: ?*anyopaque, _: *Batch, _: Timeout) Batch.AwaitConcurrentError!void { + @panic("io.p4 unimplemented: batchAwaitConcurrent"); + } + fn batchCancel(_: ?*anyopaque, _: *Batch) void { + @panic("io.p4 unimplemented: batchCancel"); + } + fn dirCreateDir(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.Permissions) Dir.CreateDirError!void { + @panic("io.p4 unimplemented: dirCreateDir"); + } + fn dirCreateDirPath(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.Permissions) Dir.CreateDirPathError!Dir.CreatePathStatus { + @panic("io.p4 unimplemented: dirCreateDirPath"); + } + fn dirCreateDirPathOpen(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.Permissions, _: Dir.OpenOptions) Dir.CreateDirPathOpenError!Dir { + @panic("io.p4 unimplemented: dirCreateDirPathOpen"); + } + fn dirOpenDir(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.OpenOptions) Dir.OpenError!Dir { + @panic("io.p4 unimplemented: dirOpenDir"); + } + fn dirStat(_: ?*anyopaque, _: Dir) Dir.StatError!Dir.Stat { + @panic("io.p4 unimplemented: dirStat"); + } + fn dirStatFile(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.StatFileOptions) Dir.StatFileError!File.Stat { + @panic("io.p4 unimplemented: dirStatFile"); + } + fn dirAccess(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.AccessOptions) Dir.AccessError!void { + @panic("io.p4 unimplemented: dirAccess"); + } + fn dirCreateFile(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.CreateFileOptions) File.OpenError!File { + @panic("io.p4 unimplemented: dirCreateFile"); + } + fn dirCreateFileAtomic(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.CreateFileAtomicOptions) Dir.CreateFileAtomicError!File.Atomic { + @panic("io.p4 unimplemented: dirCreateFileAtomic"); + } + fn dirOpenFile(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.OpenFileOptions) File.OpenError!File { + @panic("io.p4 unimplemented: dirOpenFile"); + } + fn dirClose(_: ?*anyopaque, _: []const Dir) void { + @panic("io.p4 unimplemented: dirClose"); + } + fn dirRead(_: ?*anyopaque, _: *Dir.Reader, _: []Dir.Entry) Dir.Reader.Error!usize { + @panic("io.p4 unimplemented: dirRead"); + } + fn dirRealPath(_: ?*anyopaque, _: Dir, _: []u8) Dir.RealPathError!usize { + @panic("io.p4 unimplemented: dirRealPath"); + } + fn dirRealPathFile(_: ?*anyopaque, _: Dir, _: []const u8, _: []u8) Dir.RealPathFileError!usize { + @panic("io.p4 unimplemented: dirRealPathFile"); + } + fn dirDeleteFile(_: ?*anyopaque, _: Dir, _: []const u8) Dir.DeleteFileError!void { + @panic("io.p4 unimplemented: dirDeleteFile"); + } + fn dirDeleteDir(_: ?*anyopaque, _: Dir, _: []const u8) Dir.DeleteDirError!void { + @panic("io.p4 unimplemented: dirDeleteDir"); + } + fn dirRename(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir, _: []const u8) Dir.RenameError!void { + @panic("io.p4 unimplemented: dirRename"); + } + fn dirRenamePreserve(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir, _: []const u8) Dir.RenamePreserveError!void { + @panic("io.p4 unimplemented: dirRenamePreserve"); + } + fn dirSymLink(_: ?*anyopaque, _: Dir, _: []const u8, _: []const u8, _: Dir.SymLinkFlags) Dir.SymLinkError!void { + @panic("io.p4 unimplemented: dirSymLink"); + } + fn dirReadLink(_: ?*anyopaque, _: Dir, _: []const u8, _: []u8) Dir.ReadLinkError!usize { + @panic("io.p4 unimplemented: dirReadLink"); + } + fn dirSetOwner(_: ?*anyopaque, _: Dir, _: ?File.Uid, _: ?File.Gid) Dir.SetOwnerError!void { + @panic("io.p4 unimplemented: dirSetOwner"); + } + fn dirSetFileOwner(_: ?*anyopaque, _: Dir, _: []const u8, _: ?File.Uid, _: ?File.Gid, _: Dir.SetFileOwnerOptions) Dir.SetFileOwnerError!void { + @panic("io.p4 unimplemented: dirSetFileOwner"); + } + fn dirSetPermissions(_: ?*anyopaque, _: Dir, _: Dir.Permissions) Dir.SetPermissionsError!void { + @panic("io.p4 unimplemented: dirSetPermissions"); + } + fn dirSetFilePermissions(_: ?*anyopaque, _: Dir, _: []const u8, _: File.Permissions, _: Dir.SetFilePermissionsOptions) Dir.SetFilePermissionsError!void { + @panic("io.p4 unimplemented: dirSetFilePermissions"); + } + fn dirSetTimestamps(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir.SetTimestampsOptions) Dir.SetTimestampsError!void { + @panic("io.p4 unimplemented: dirSetTimestamps"); + } + fn dirHardLink(_: ?*anyopaque, _: Dir, _: []const u8, _: Dir, _: []const u8, _: Dir.HardLinkOptions) Dir.HardLinkError!void { + @panic("io.p4 unimplemented: dirHardLink"); + } + fn fileStat(_: ?*anyopaque, _: File) File.StatError!File.Stat { + @panic("io.p4 unimplemented: fileStat"); + } + fn fileLength(_: ?*anyopaque, _: File) File.LengthError!u64 { + @panic("io.p4 unimplemented: fileLength"); + } + fn fileClose(_: ?*anyopaque, _: []const File) void { + @panic("io.p4 unimplemented: fileClose"); + } + fn fileWritePositional(_: ?*anyopaque, _: File, _: []const u8, _: []const []const u8, _: usize, _: u64) File.WritePositionalError!usize { + @panic("io.p4 unimplemented: fileWritePositional"); + } + fn fileWriteFileStreaming(_: ?*anyopaque, _: File, _: []const u8, _: *Io.File.Reader, _: Io.Limit) File.Writer.WriteFileError!usize { + @panic("io.p4 unimplemented: fileWriteFileStreaming"); + } + fn fileWriteFilePositional(_: ?*anyopaque, _: File, _: []const u8, _: *Io.File.Reader, _: Io.Limit, _: u64) File.WriteFilePositionalError!usize { + @panic("io.p4 unimplemented: fileWriteFilePositional"); + } + fn fileReadPositional(_: ?*anyopaque, _: File, _: []const []u8, _: u64) File.ReadPositionalError!usize { + @panic("io.p4 unimplemented: fileReadPositional"); + } + fn fileSeekBy(_: ?*anyopaque, _: File, _: i64) File.SeekError!void { + @panic("io.p4 unimplemented: fileSeekBy"); + } + fn fileSeekTo(_: ?*anyopaque, _: File, _: u64) File.SeekError!void { + @panic("io.p4 unimplemented: fileSeekTo"); + } + fn fileSync(_: ?*anyopaque, _: File) File.SyncError!void { + @panic("io.p4 unimplemented: fileSync"); + } + fn fileIsTty(_: ?*anyopaque, _: File) Cancelable!bool { + @panic("io.p4 unimplemented: fileIsTty"); + } + fn fileEnableAnsiEscapeCodes(_: ?*anyopaque, _: File) File.EnableAnsiEscapeCodesError!void { + @panic("io.p4 unimplemented: fileEnableAnsiEscapeCodes"); + } + fn fileSupportsAnsiEscapeCodes(_: ?*anyopaque, _: File) Cancelable!bool { + @panic("io.p4 unimplemented: fileSupportsAnsiEscapeCodes"); + } + fn fileSetLength(_: ?*anyopaque, _: File, _: u64) File.SetLengthError!void { + @panic("io.p4 unimplemented: fileSetLength"); + } + fn fileSetOwner(_: ?*anyopaque, _: File, _: ?File.Uid, _: ?File.Gid) File.SetOwnerError!void { + @panic("io.p4 unimplemented: fileSetOwner"); + } + fn fileSetPermissions(_: ?*anyopaque, _: File, _: File.Permissions) File.SetPermissionsError!void { + @panic("io.p4 unimplemented: fileSetPermissions"); + } + fn fileSetTimestamps(_: ?*anyopaque, _: File, _: File.SetTimestampsOptions) File.SetTimestampsError!void { + @panic("io.p4 unimplemented: fileSetTimestamps"); + } + fn fileLock(_: ?*anyopaque, _: File, _: File.Lock) File.LockError!void { + @panic("io.p4 unimplemented: fileLock"); + } + fn fileTryLock(_: ?*anyopaque, _: File, _: File.Lock) File.LockError!bool { + @panic("io.p4 unimplemented: fileTryLock"); + } + fn fileUnlock(_: ?*anyopaque, _: File) void { + @panic("io.p4 unimplemented: fileUnlock"); + } + fn fileDowngradeLock(_: ?*anyopaque, _: File) File.DowngradeLockError!void { + @panic("io.p4 unimplemented: fileDowngradeLock"); + } + fn fileRealPath(_: ?*anyopaque, _: File, _: []u8) File.RealPathError!usize { + @panic("io.p4 unimplemented: fileRealPath"); + } + fn fileHardLink(_: ?*anyopaque, _: File, _: Dir, _: []const u8, _: File.HardLinkOptions) File.HardLinkError!void { + @panic("io.p4 unimplemented: fileHardLink"); + } + fn fileMemoryMapCreate(_: ?*anyopaque, _: File, _: File.MemoryMap.CreateOptions) File.MemoryMap.CreateError!File.MemoryMap { + @panic("io.p4 unimplemented: fileMemoryMapCreate"); + } + fn fileMemoryMapDestroy(_: ?*anyopaque, _: *File.MemoryMap) void { + @panic("io.p4 unimplemented: fileMemoryMapDestroy"); + } + fn fileMemoryMapSetLength(_: ?*anyopaque, _: *File.MemoryMap, _: usize) File.MemoryMap.SetLengthError!void { + @panic("io.p4 unimplemented: fileMemoryMapSetLength"); + } + fn fileMemoryMapRead(_: ?*anyopaque, _: *File.MemoryMap) File.ReadPositionalError!void { + @panic("io.p4 unimplemented: fileMemoryMapRead"); + } + fn fileMemoryMapWrite(_: ?*anyopaque, _: *File.MemoryMap) File.WritePositionalError!void { + @panic("io.p4 unimplemented: fileMemoryMapWrite"); + } + fn processExecutableOpen(_: ?*anyopaque, _: Dir.OpenFileOptions) std.process.OpenExecutableError!File { + @panic("io.p4 unimplemented: processExecutableOpen"); + } + fn processExecutablePath(_: ?*anyopaque, _: []u8) std.process.ExecutablePathError!usize { + @panic("io.p4 unimplemented: processExecutablePath"); + } + fn lockStderr(_: ?*anyopaque, _: ?Terminal.Mode) Cancelable!LockedStderr { + @panic("io.p4 unimplemented: lockStderr"); + } + fn tryLockStderr(_: ?*anyopaque, _: ?Terminal.Mode) Cancelable!?LockedStderr { + @panic("io.p4 unimplemented: tryLockStderr"); + } + fn unlockStderr(_: ?*anyopaque) void { + @panic("io.p4 unimplemented: unlockStderr"); + } + fn processCurrentPath(_: ?*anyopaque, _: []u8) std.process.CurrentPathError!usize { + @panic("io.p4 unimplemented: processCurrentPath"); + } + fn processSetCurrentDir(_: ?*anyopaque, _: Dir) std.process.SetCurrentDirError!void { + @panic("io.p4 unimplemented: processSetCurrentDir"); + } + fn processSetCurrentPath(_: ?*anyopaque, _: []const u8) std.process.SetCurrentPathError!void { + @panic("io.p4 unimplemented: processSetCurrentPath"); + } + fn processReplace(_: ?*anyopaque, _: std.process.ReplaceOptions) std.process.ReplaceError { + @panic("io.p4 unimplemented: processReplace"); + } + fn processReplacePath(_: ?*anyopaque, _: Dir, _: std.process.ReplaceOptions) std.process.ReplaceError { + @panic("io.p4 unimplemented: processReplacePath"); + } + fn processSpawn(_: ?*anyopaque, _: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { + @panic("io.p4 unimplemented: processSpawn"); + } + fn processSpawnPath(_: ?*anyopaque, _: Dir, _: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { + @panic("io.p4 unimplemented: processSpawnPath"); + } + fn childWait(_: ?*anyopaque, _: *std.process.Child) std.process.Child.WaitError!std.process.Child.Term { + @panic("io.p4 unimplemented: childWait"); + } + fn childKill(_: ?*anyopaque, _: *std.process.Child) void { + @panic("io.p4 unimplemented: childKill"); + } + fn progressParentFile(_: ?*anyopaque) std.Progress.ParentFileError!File { + @panic("io.p4 unimplemented: progressParentFile"); + } + fn now(_: ?*anyopaque, _: Clock) Timestamp { + @panic("io.p4 unimplemented: now"); + } + fn clockResolution(_: ?*anyopaque, _: Clock) Clock.ResolutionError!Duration { + @panic("io.p4 unimplemented: clockResolution"); + } + fn sleep(_: ?*anyopaque, _: Timeout) Cancelable!void { + @panic("io.p4 unimplemented: sleep"); + } + fn random(_: ?*anyopaque, _: []u8) void { + @panic("io.p4 unimplemented: random"); + } + fn randomSecure(_: ?*anyopaque, _: []u8) RandomSecureError!void { + @panic("io.p4 unimplemented: randomSecure"); + } + fn netListenIp(_: ?*anyopaque, _: *const net.IpAddress, _: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Socket { + @panic("io.p4 unimplemented: netListenIp"); + } + fn netAccept(_: ?*anyopaque, _: net.Socket.Handle, _: net.Server.AcceptOptions) net.Server.AcceptError!net.Socket { + @panic("io.p4 unimplemented: netAccept"); + } + fn netBindIp(_: ?*anyopaque, _: *const net.IpAddress, _: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket { + @panic("io.p4 unimplemented: netBindIp"); + } + fn netConnectIp(_: ?*anyopaque, _: *const net.IpAddress, _: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Socket { + @panic("io.p4 unimplemented: netConnectIp"); + } + fn netListenUnix(_: ?*anyopaque, _: *const net.UnixAddress, _: net.UnixAddress.ListenOptions) net.UnixAddress.ListenError!net.Socket.Handle { + @panic("io.p4 unimplemented: netListenUnix"); + } + fn netConnectUnix(_: ?*anyopaque, _: *const net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle { + @panic("io.p4 unimplemented: netConnectUnix"); + } + fn netSocketCreatePair(_: ?*anyopaque, _: net.Socket.CreatePairOptions) net.Socket.CreatePairError![2]net.Socket { + @panic("io.p4 unimplemented: netSocketCreatePair"); + } + fn netSend(_: ?*anyopaque, _: net.Socket.Handle, _: []net.OutgoingMessage, _: net.SendFlags) struct { ?net.Socket.SendError, usize } { + @panic("io.p4 unimplemented: netSend"); + } + fn netRead(_: ?*anyopaque, _: net.Socket.Handle, _: [][]u8) net.Stream.Reader.Error!usize { + @panic("io.p4 unimplemented: netRead"); + } + fn netWrite(_: ?*anyopaque, _: net.Socket.Handle, _: []const u8, _: []const []const u8, _: usize) net.Stream.Writer.Error!usize { + @panic("io.p4 unimplemented: netWrite"); + } + fn netWriteFile(_: ?*anyopaque, _: net.Socket.Handle, _: []const u8, _: *Io.File.Reader, _: Io.Limit) net.Stream.Writer.WriteFileError!usize { + @panic("io.p4 unimplemented: netWriteFile"); + } + fn netClose(_: ?*anyopaque, _: []const net.Socket.Handle) void { + @panic("io.p4 unimplemented: netClose"); + } + fn netShutdown(_: ?*anyopaque, _: net.Socket.Handle, _: net.ShutdownHow) net.ShutdownError!void { + @panic("io.p4 unimplemented: netShutdown"); + } + fn netInterfaceNameResolve(_: ?*anyopaque, _: *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface { + @panic("io.p4 unimplemented: netInterfaceNameResolve"); + } + fn netInterfaceName(_: ?*anyopaque, _: net.Interface) net.Interface.NameError!net.Interface.Name { + @panic("io.p4 unimplemented: netInterfaceName"); + } + fn netLookup(_: ?*anyopaque, _: net.HostName, _: *Queue(net.HostName.LookupResult), _: net.HostName.LookupOptions) net.HostName.LookupError!void { + @panic("io.p4 unimplemented: netLookup"); + } +}; |
