lib/mprompt/src/prompt.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const sys = @import("sys");
   3 const assert = std.debug.assert;
   4 const signal = @import("signals.zig");
   5 const stack_memory = @import("stack.zig");
   6 const context = sys.context;
   7 const JmpBuf = context.JmpBuf;
   8 const UnwindFrame = context.UnwindFrame;
   9 
  10 const page_size = std.heap.page_size_min;
  11 const stack_grows_down = true;
  12 
  13 comptime {
  14     if (@bitSizeOf(usize) != 64) {
  15         @compileError("lib/mprompt requires a 64-bit target because stacklets reserve virtual address space");
  16     }
  17 }
  18 
  19 /// Function type a new prompt runs first, with the C calling convention, so a caller passes one to
  20 /// `prompt` or `promptEnter` to run it on a prompt's stacklet. The function receives the prompt and
  21 /// the argument given to `prompt` or `promptEnter`. Its return value becomes the result of the call
  22 /// that entered or last resumed the prompt.
  23 pub const StartFn = *const fn (*Prompt, ?*anyopaque) callconv(.c) ?*anyopaque;
  24 /// Function type `yieldPrompt` runs after the suspend, with the C calling convention, so a caller
  25 /// writes one to receive the resumption when code suspends. The function runs on the stack of the
  26 /// code that entered or last resumed the prompt. The function receives the new resumption and the
  27 /// argument given to `yieldPrompt`. Its return value becomes the result of the call that entered or
  28 /// last resumed the prompt. The function may resume the resumption, drop it, or return it for later
  29 /// use.
  30 pub const YieldFn = *const fn (*Resume, ?*anyopaque) callconv(.c) ?*anyopaque;
  31 
  32 /// Holds the runtime's tuning values with a C layout, and every field has a default. A program that
  33 /// needs larger stacks, another pool size, or fully committed stacks passes one to `init` before it
  34 /// creates any prompt. The runtime reads it once, at initialization, and later changes have no
  35 /// effect. Sizes are in bytes and are rounded up to whole pages. A size of zero or less selects the
  36 /// field's default. `configDefault` returns one with every default.
  37 pub const Config = extern struct {
  38     /// When true, each stacklet comes from shared reserved regions cut into equal stacklet-sized
  39     /// blocks (stack pool). When false, each stacklet reserves its own range of address space. The
  40     /// default is true, and overcommit mode turns it off. Every thread of the process takes
  41     /// stacklets from the pool under one lock.
  42     gpool_enable: bool = true,
  43     /// When true, the runtime's fault handler commits extra pages past the faulting page each time
  44     /// a write faults in the uncommitted part of a stacklet. The extra space is twice the stack in
  45     /// use so far, at most 1 MiB, and at most the space left. When false, the runtime commits pages
  46     /// up to the faulting page only. The runtime commits a page by making it readable and writable.
  47     /// The default is true, and overcommit mode turns it off.
  48     stack_grow_fast: bool = true,
  49     /// When true, the runtime commits every page of a stacklet when it takes the stacklet, and it
  50     /// installs no fault handler and no alternate signal stack. Turning it on also turns off the
  51     /// stack pool and fast growth. The default is false.
  52     stack_use_overcommit: bool = false,
  53     /// When true, the runtime makes every page of a stacklet inaccessible when it places the
  54     /// stacklet in a per-thread list of freed stacklets and prompt records for reuse (stack cache),
  55     /// and it also decommits the used pages of a pooled stacklet it frees. When false, a cached
  56     /// stacklet keeps its pages, and a pooled stacklet that is freed has its used pages discarded,
  57     /// so their contents are dropped. The default is false.
  58     stack_reset_decommits: bool = false,
  59     /// The size in bytes of each region of the stack pool. A region holds as many stacklets as fit
  60     /// in this size, at least 1 and at most 32,000. The process reserves another region when every
  61     /// block of the existing ones is in use. The default is 256 GiB of address space.
  62     gpool_max_size: isize = 256 * gib,
  63     /// The reserved size of each stacklet in bytes, the two gaps included, so the usable stack is
  64     /// this size minus twice `stack_gap_size`. The default is 8 MiB. Initialization stops the
  65     /// program with a panic when the size leaves one page or less of usable stack. A write past the
  66     /// usable stack lands in a gap, which the runtime never commits, so the fault goes to the
  67     /// program's earlier fault handler, or the process aborts when there was none.
  68     stack_max_size: isize = 8 * mib,
  69     /// The runtime reads nothing from this field, so its value has no effect. The default is 32
  70     /// KiB.
  71     stack_exn_guaranteed: isize = 32 * kib,
  72     /// The number of bytes of a new stacklet committed when the runtime takes it, rounded up to
  73     /// whole pages. The default of 0 means one page. The runtime caps it at the stacklet's usable
  74     /// size.
  75     stack_initial_commit: isize = 0,
  76     /// The size in bytes of the unmapped gap at each end of a stacklet, rounded up to whole pages.
  77     /// The default is 64 KiB.
  78     stack_gap_size: isize = 64 * kib,
  79     /// The number of freed stacklets, and separately the number of freed prompt records, that each
  80     /// thread keeps for reuse. The default is 4. A negative value becomes 0, which turns the reuse
  81     /// off.
  82     stack_cache_count: isize = 4,
  83 };
  84 
  85 /// The runtime's record for one prompt: its stacklet, its links to other prompts, its reference
  86 /// count, and the registers saved for its entry and its last suspend. A prompt marks the point
  87 /// where code entered its stacklet, and that code can later suspend back to it. A body receives a
  88 /// `*Prompt` for the stacklet it runs on, and `yieldPrompt` takes that pointer as the point to
  89 /// suspend back to. The runtime reads and writes every field, and callers pass the pointer through.
  90 /// A prompt is entered once, and `promptEnter` asserts that it is neither a prompt whose code or
  91 /// nested code runs on this thread (active prompt) nor a suspended one. The runtime frees the
  92 /// record, or keeps it for reuse, when its reference count reaches zero, so a `*Prompt` stays valid
  93 /// only while its code runs or a resumption for it is held.
  94 pub const Prompt = extern struct {
  95     /// While the prompt is active, this field holds the next active prompt outward, or null for the
  96     /// outermost. While the prompt is suspended, this field holds null in the prompt the code
  97     /// suspended to. In each prompt captured inside that one, this field holds the next prompt
  98     /// outward in the captured chain. While the record waits in the stack cache, this field holds
  99     /// the next cached record.
 100     parent: ?*Prompt,
 101     /// Holds null while the prompt is active. While the prompt is suspended, this field points to
 102     /// the innermost prompt of the chain captured with it. A new prompt points to itself.
 103     top: ?*Prompt,
 104     /// The number of owners of the prompt, starting at 1. Each saved stack copy adds one owner, as
 105     /// does each resume of a reference-counted handle resumable more than once (multi-shot handle).
 106     /// The runtime frees the prompt's stacklets when the count reaches zero.
 107     refcount: isize,
 108     /// The stacklet the prompt's code runs on. The runtime reserves each stacklet as a fixed-size
 109     /// range of address space with an unmapped gap at each end, and commits its pages as the stack
 110     /// grows into them. The code's type for a stacklet is `GStack`, and the runtime's fatal
 111     /// messages, such as "unable to reserve gstack virtual memory", call it a gstack.
 112     gstack: *GStack,
 113     /// The registers saved by the call that last entered or resumed the prompt. A suspend or a
 114     /// return from the prompt's code jumps there.
 115     return_point: ?*ReturnPoint,
 116     /// Registers saved at the suspend while the prompt is suspended, where a resume jumps. This
 117     /// field is null for a new prompt and after its code returns.
 118     resume_point: ?*ResumePoint,
 119     /// The saved stack pointer for the inactive side of the last switch, XORed with a random
 120     /// per-process value (guard cookie). Before each jump the runtime checks the target against it,
 121     /// and a mismatch stops the program with a "potential stack corruption detected" panic.
 122     sp: ?*anyopaque,
 123     /// The frame record, holding an instruction pointer, that the stack-entry code passes to the
 124     /// prompt's first function. The runtime stores it and reads it nowhere.
 125     unwind_frame: ?*UnwindFrame,
 126 };
 127 
 128 /// An opaque handle to a suspended prompt, so a yield function receives one and resumes or drops
 129 /// it, and code that suspends to its caller hands it back as the caller's result. The prompt's own
 130 /// address serves as a handle resumable at most once, continuing in place with no copy (one-shot
 131 /// handle). A multi-shot handle is the address of a reference-counted record with bit 2 set, and
 132 /// the runtime tells the two apart by that bit. Each handle, and each reference of a multi-shot
 133 /// handle, is resumed or dropped exactly once. A handle that is neither keeps its stacklets
 134 /// allocated.
 135 pub const Resume = opaque {};
 136 
 137 const kib: isize = 1024;
 138 const mib: isize = kib * kib;
 139 const gib: isize = 1024 * mib;
 140 
 141 const ReturnKind = enum(c_int) {
 142     normal_return,
 143     exception,
 144     yielded,
 145 };
 146 
 147 const ResumePoint = extern struct {
 148     jmp: JmpBuf,
 149     result: ?*anyopaque,
 150 };
 151 
 152 const ReturnPoint = extern struct {
 153     jmp: JmpBuf,
 154     kind: ReturnKind,
 155     fun: ?YieldFn,
 156     arg: ?*anyopaque,
 157 };
 158 
 159 const EntryEnv = extern struct {
 160     prompt: *Prompt,
 161     fun: StartFn,
 162     arg: ?*anyopaque,
 163 };
 164 
 165 const GStack = struct {
 166     next: ?*GStack,
 167     memory: stack_memory.Allocation,
 168     stack: []align(page_size) u8,
 169     stack_size: usize,
 170     committed: usize,
 171 
 172     fn alloc() *GStack {
 173         if (!initialized) ensureInitialized(null);
 174         if (!config.stack_use_overcommit and !signal_stack_ready) ensureThreadSignalStack();
 175 
 176         var stack_probe: u8 = 0;
 177         const parent_sp = @intFromPtr(&stack_probe);
 178         if (takeCached(parent_sp)) |cached| {
 179             cached.next = null;
 180             if (config.stack_use_overcommit) {
 181                 cached.commitAll();
 182             } else {
 183                 cached.commitInitial();
 184             }
 185             return cached;
 186         }
 187 
 188         const allocator = processAllocator();
 189         const g = allocator.create(GStack) catch fatal("unable to allocate gstack metadata", .{});
 190         errdefer allocator.destroy(g);
 191 
 192         const memory = stack_memory.alloc(allocator, .{
 193             .full_size = config.stack_max_size,
 194             .gap_size = config.stack_gap_size,
 195             .pool_enable = config.gpool_enable,
 196             .pool_max_size = config.gpool_max_size,
 197         }) catch fatal("unable to reserve gstack virtual memory", .{});
 198 
 199         g.* = .{
 200             .next = null,
 201             .memory = memory,
 202             .stack = memory.stack,
 203             .stack_size = memory.stack.len,
 204             .committed = 0,
 205         };
 206         if (config.stack_use_overcommit) {
 207             g.commitAll();
 208         } else {
 209             g.commitInitial();
 210         }
 211         return g;
 212     }
 213 
 214     fn takeCached(parent_sp: usize) ?*GStack {
 215         var previous: ?*GStack = null;
 216         var current = gstack_cache;
 217         while (current) |candidate| {
 218             const next = candidate.next;
 219             if (candidate.isBelow(parent_sp)) {
 220                 if (previous) |prev| {
 221                     prev.next = next;
 222                 } else {
 223                     gstack_cache = next;
 224                 }
 225                 gstack_cache_count -= 1;
 226                 return candidate;
 227             }
 228 
 229             previous = candidate;
 230             current = next;
 231         }
 232         return null;
 233     }
 234 
 235     fn isBelow(g: *const GStack, parent_sp: usize) bool {
 236         const stack_start = @intFromPtr(g.stack.ptr);
 237         if (stack_grows_down) return stack_start < parent_sp;
 238         return stack_start + g.stack_size > parent_sp;
 239     }
 240 
 241     fn free(g: *GStack) void {
 242         if (gstack_cache_count < config.stack_cache_count) {
 243             if (config.stack_reset_decommits) g.decommit();
 244             g.next = gstack_cache;
 245             gstack_cache = g;
 246             gstack_cache_count += 1;
 247             return;
 248         }
 249 
 250         stack_memory.freeForReuse(g.memory, g.committed, stack_grows_down, config.stack_reset_decommits) catch
 251             fatal("unable to release gstack virtual memory", .{});
 252         processAllocator().destroy(g);
 253     }
 254 
 255     fn commitAll(g: *GStack) void {
 256         if (g.committed == g.stack_size) return;
 257         protect(g.stack, .{ .read = true, .write = true }) catch fatal(
 258             "unable to recommit cached gstack stack range",
 259             .{},
 260         );
 261         g.committed = g.stack_size;
 262     }
 263 
 264     fn decommit(g: *GStack) void {
 265         if (g.committed == 0) return;
 266         protect(g.stack, .{}) catch fatal(
 267             "unable to decommit cached gstack stack range",
 268             .{},
 269         );
 270         g.committed = 0;
 271     }
 272 
 273     fn commitInitial(g: *GStack) void {
 274         const initial_commit = initialCommitSize(g.stack_size);
 275         if (g.committed >= initial_commit) return;
 276         g.commitTo(initial_commit, "unable to commit initial gstack stack range");
 277     }
 278 
 279     fn commitTo(g: *GStack, new_committed: usize, comptime message: []const u8) void {
 280         assert(new_committed <= g.stack_size);
 281         assert(new_committed >= g.committed);
 282         if (new_committed == g.committed) return;
 283 
 284         const memory = g.commitRange(new_committed);
 285         protect(memory, .{ .read = true, .write = true }) catch fatal(message, .{});
 286         g.committed = new_committed;
 287     }
 288 
 289     fn commitRange(g: *const GStack, new_committed: usize) []align(page_size) u8 {
 290         assert(stack_grows_down);
 291         const high = g.stack.ptr + g.stack_size;
 292         const start: [*]align(page_size) u8 = @alignCast(high - new_committed);
 293         return start[0 .. new_committed - g.committed];
 294     }
 295 
 296     fn commitFaultPage(g: *GStack, fault_addr: usize) bool {
 297         assert(stack_grows_down);
 298 
 299         const low = @intFromPtr(g.stack.ptr);
 300         const high = low + g.stack_size;
 301         const fault_page = alignDown(fault_addr, page_size);
 302         if (fault_page < low or fault_page >= high) return false;
 303 
 304         const used = high - fault_page;
 305         if (used == 0 or used > g.stack_size or used <= g.committed) return false;
 306 
 307         var new_committed = used;
 308         if (config.stack_grow_fast) {
 309             const available = g.stack_size - used;
 310             var extra = used *| 2;
 311             extra = @min(extra, 1 * @as(usize, @intCast(mib)));
 312             extra = @min(extra, available);
 313             new_committed += alignDown(extra, page_size);
 314         }
 315         new_committed = alignUp(new_committed, page_size);
 316         new_committed = @min(new_committed, g.stack_size);
 317         if (new_committed <= g.committed) return false;
 318 
 319         const memory = g.commitRange(new_committed);
 320         sys.memory.protect(memory, .{ .read = true, .write = true }) catch return false;
 321         g.committed = new_committed;
 322         return true;
 323     }
 324 
 325     fn base(g: *const GStack) [*]u8 {
 326         return if (stack_grows_down) g.stack.ptr + g.stack_size else g.stack.ptr;
 327     }
 328 
 329     fn push(_: *const GStack, sp: [*]u8, size: usize) [*]u8 {
 330         return if (stack_grows_down) sp - size else sp + size;
 331     }
 332 
 333     fn contains(g: *const GStack, sp: [*]const u8) bool {
 334         const addr = @intFromPtr(sp);
 335         const low = @intFromPtr(g.stack.ptr);
 336         const high = low + g.stack_size;
 337         return addr >= low and addr < high;
 338     }
 339 
 340     fn usedFrom(g: *const GStack, sp: [*]const u8) usize {
 341         const addr = @intFromPtr(sp);
 342         const low = @intFromPtr(g.stack.ptr);
 343         const high = low + g.stack_size;
 344         return if (stack_grows_down) high - addr else addr - low;
 345     }
 346 
 347     fn enter(
 348         g: *GStack,
 349         return_jmp: **JmpBuf,
 350         fun: context.StartFn,
 351         arg: ?*anyopaque,
 352     ) noreturn {
 353         const base_sp = g.base();
 354         const commit_limit = g.push(base_sp, g.committed);
 355         const stack_limit = g.push(base_sp, g.stack_size);
 356         _ = context.mp_stack_enter(base_sp, commit_limit, stack_limit, return_jmp, fun, arg);
 357         unreachable;
 358     }
 359 
 360     fn save(g: *GStack, sp: [*]u8) *GSave {
 361         assert(g.contains(sp));
 362 
 363         const used = g.usedFrom(sp);
 364         const data = processAllocator().alloc(u8, used) catch fatal("unable to save gstack", .{});
 365         const source = if (stack_grows_down) sp[0..used] else g.stack.ptr[0..used];
 366         @memcpy(data, source);
 367 
 368         const gs = processAllocator().create(GSave) catch fatal("unable to allocate gstack save", .{});
 369         gs.* = .{
 370             .stack = if (stack_grows_down) sp else g.stack.ptr,
 371             .data = data,
 372         };
 373         return gs;
 374     }
 375 };
 376 
 377 const GSave = struct {
 378     stack: [*]u8,
 379     data: []u8,
 380 
 381     fn restore(gs: *const GSave) void {
 382         @memcpy(gs.stack[0..gs.data.len], gs.data);
 383     }
 384 
 385     fn free(gs: *GSave) void {
 386         const allocator = processAllocator();
 387         allocator.free(gs.data);
 388         allocator.destroy(gs);
 389     }
 390 };
 391 
 392 const PromptSave = struct {
 393     next: ?*PromptSave,
 394     prompt: *Prompt,
 395     prompt_snapshot: Prompt,
 396     gsave: *GSave,
 397 
 398     fn free(save: *PromptSave) void {
 399         const allocator = processAllocator();
 400         save.gsave.free();
 401         promptDrop(save.prompt);
 402         allocator.destroy(save);
 403     }
 404 };
 405 
 406 const MResume = struct {
 407     refcount: isize,
 408     resume_count: c_long,
 409     prompt: *Prompt,
 410     save: ?*PromptSave,
 411     tail_return_point: ?*ReturnPoint,
 412 };
 413 
 414 threadlocal var prompt_top: ?*Prompt = null;
 415 threadlocal var prompt_cache: ?*Prompt = null;
 416 threadlocal var prompt_cache_count: usize = 0;
 417 threadlocal var gstack_cache: ?*GStack = null;
 418 threadlocal var gstack_cache_count: usize = 0;
 419 threadlocal var signal_stack: ?[]u8 = null;
 420 threadlocal var signal_stack_ready = false;
 421 
 422 var initialized = false;
 423 var growth_handler_installed = false;
 424 var previous_sigsegv: sys.signal.SignalAction = undefined;
 425 var have_previous_sigsegv = false;
 426 var previous_sigbus: sys.signal.SignalAction = undefined;
 427 var have_previous_sigbus = false;
 428 var guard_cookie: usize = 0x00002B992DDFA232;
 429 var return_label: ?*anyopaque = null;
 430 var resume_label: ?*anyopaque = null;
 431 var config: RuntimeConfig = .{};
 432 
 433 const RuntimeConfig = struct {
 434     stack_max_size: usize = 8 * @as(usize, @intCast(mib)),
 435     stack_gap_size: usize = 64 * @as(usize, @intCast(kib)),
 436     stack_cache_count: usize = 4,
 437     stack_initial_commit: usize = 0,
 438     stack_grow_fast: bool = true,
 439     stack_use_overcommit: bool = false,
 440     stack_reset_decommits: bool = false,
 441     gpool_enable: bool = true,
 442     gpool_max_size: usize = 256 * @as(usize, @intCast(gib)),
 443 };
 444 
 445 /// Initializes the runtime from `cfg`, or from the defaults when `cfg` is null, so a program calls
 446 /// it once before creating any prompt to replace the default configuration. Only the first
 447 /// initialization in a process takes effect: later calls return at once, and the first
 448 /// `promptCreate` initializes with the defaults when `init` has not run. Initialization draws the
 449 /// guard cookie from the clock and an address. Unless overcommit mode is on, initialization
 450 /// installs a fault handler for stack growth and an alternate signal stack of 64 KiB for the
 451 /// calling thread. The fault handler takes segmentation faults, and bus faults as well on platforms
 452 /// other than Linux. The runtime keeps the fault handler that was installed before, passes it every
 453 /// fault it cannot use for stack growth, and aborts the process when there was none. Initialization
 454 /// uses plain process globals and no lock, so two threads that initialize at the same time race.
 455 /// The program stops with a panic when the configuration leaves no usable stack or when a handler
 456 /// or signal stack cannot be installed.
 457 pub fn init(cfg: ?*const Config) void {
 458     ensureInitialized(cfg);
 459 }
 460 
 461 /// Returns a `Config` with every field at its default, so a caller starts from the defaults and
 462 /// changes only the fields it needs.
 463 pub fn configDefault() Config {
 464     return .{};
 465 }
 466 
 467 /// Creates a prompt and enters it with `fun` and `arg`, which is `promptCreate` followed by
 468 /// `promptEnter`, so a caller runs a C-convention function on a new stacklet in one call. The call
 469 /// returns what `fun` returns when it finishes, or what the yield function returns when `fun`
 470 /// suspends to this prompt. The stack switch exists for x86_64 and aarch64 targets other than
 471 /// Windows, and the package compiles only for 64-bit targets.
 472 pub fn prompt(fun: StartFn, arg: ?*anyopaque) ?*anyopaque {
 473     const p = promptCreate();
 474     return promptEnter(p, fun, arg);
 475 }
 476 
 477 /// Returns a new prompt with one owner, its own stacklet, and no saved registers, so a caller
 478 /// creates a prompt ahead of the call that enters it. The record comes from the calling thread's
 479 /// stack cache or from the process allocator. The stacklet comes from the stack cache, when a
 480 /// cached one lies below the caller's stack pointer, or from a new reservation. The first call in a
 481 /// process initializes the runtime with the defaults when `init` has not run. The program stops
 482 /// with a panic when memory for the record or the stacklet cannot be had. The caller enters the
 483 /// prompt with `promptEnter`, and a prompt that is never entered keeps its stacklet.
 484 pub fn promptCreate() *Prompt {
 485     if (!initialized) ensureInitialized(null);
 486 
 487     const p = promptAlloc();
 488     p.* = .{
 489         .parent = null,
 490         .top = p,
 491         .refcount = 1,
 492         .gstack = GStack.alloc(),
 493         .return_point = null,
 494         .resume_point = null,
 495         .sp = null,
 496         .unwind_frame = null,
 497     };
 498     return p;
 499 }
 500 
 501 fn promptAlloc() *Prompt {
 502     if (prompt_cache) |cached| {
 503         prompt_cache = cached.parent;
 504         prompt_cache_count -= 1;
 505         cached.parent = null;
 506         return cached;
 507     }
 508 
 509     return processAllocator().create(Prompt) catch fatal("unable to allocate prompt", .{});
 510 }
 511 
 512 /// Switches to `p`'s stacklet and calls `fun(p, arg)` there, so a caller runs `fun` on a prompt
 513 /// created earlier with `promptCreate`. While `fun` runs, `p` is the calling thread's innermost
 514 /// active prompt. The call returns what `fun` returns when it finishes, and the runtime then
 515 /// releases `p`. The call returns what the yield function returns when the code suspends to `p`.
 516 /// `p` must be new: an active or suspended prompt fails an assertion in safe builds.
 517 pub fn promptEnter(p: *Prompt, fun: StartFn, arg: ?*anyopaque) ?*anyopaque {
 518     assert(!promptIsActive(p));
 519     assert(p.resume_point == null);
 520 
 521     var env: EntryEnv = .{
 522         .prompt = p,
 523         .fun = fun,
 524         .arg = arg,
 525     };
 526     return promptResume(p, &env);
 527 }
 528 
 529 /// Returns the calling thread's innermost active prompt, or null when no prompt is active, so a
 530 /// caller finds the innermost prompt its code runs under without being handed it.
 531 pub fn promptTop() ?*Prompt {
 532     return prompt_top;
 533 }
 534 
 535 /// Returns the next active prompt outward from `p`, or the innermost active prompt when `p` is
 536 /// null, so a caller walks the thread's active prompts outward one step per call, as the runtime
 537 /// does to check that a suspend targets an active prompt. For a suspended `p`, the call returns
 538 /// null or the next prompt outward inside the captured chain.
 539 pub fn promptParent(p: ?*Prompt) ?*Prompt {
 540     return if (p) |prompt_ptr| prompt_ptr.parent else prompt_top;
 541 }
 542 
 543 /// Suspends the calling code together with `p` and each nested active prompt, so code running under
 544 /// a prompt suspends back to the point that entered the prompt and hands a resumption to a function
 545 /// there, as a worker does while it waits for its request. The call jumps to where `p` was entered
 546 /// or last resumed and calls `fun(resumption, arg)` there. The return value of `fun` becomes the
 547 /// result of the call that entered or last resumed `p`. The call returns the value passed when the
 548 /// resumption is resumed. The call saves the registers at its own point, where a resume jumps, and
 549 /// the frames on the suspended stacklets stay as they were. `p` must be an active prompt of the
 550 /// calling thread, and assertions check this in safe builds. Before each jump the runtime checks
 551 /// the saved stack pointer and jump target against the guard cookie, and a mismatch stops the
 552 /// program with a "potential stack corruption detected" panic.
 553 pub noinline fn yieldPrompt(p: *Prompt, fun: YieldFn, arg: ?*anyopaque) ?*anyopaque {
 554     assert(promptIsAncestor(p));
 555     assert(promptIsActive(p));
 556 
 557     var res: ResumePoint = undefined;
 558     if (context.mp_setjmp(&res.jmp) != 0) {
 559         assert(promptIsActive(p));
 560         assert(promptIsAncestor(p));
 561         return res.result;
 562     }
 563 
 564     if (resume_label == null) resume_label = guard(res.jmp.reg_ip);
 565 
 566     var sp: ?*anyopaque = null;
 567     const ret = promptUnlink(p, &res, &sp);
 568     ret.fun = fun;
 569     ret.arg = arg;
 570     ret.kind = .yielded;
 571     checkedLongjmp(return_label, sp, &ret.jmp);
 572 }
 573 
 574 /// Switches back into the suspended code so that its `yieldPrompt` call returns `arg`, for a yield
 575 /// function or code that kept a resumption to continue the suspended code with a value. The call
 576 /// returns when that code suspends again, with the yield function's result, or when it finishes,
 577 /// with its result. For a one-shot handle, the code continues in place and the handle is used up. A
 578 /// one-shot handle with more than one owner, or with nothing suspended, fails an assertion in safe
 579 /// builds. For a multi-shot handle, the call gives up one reference and counts one resume. When
 580 /// other references remain, the runtime first copies the suspended stack segments to the heap, and
 581 /// a later resume copies them back.
 582 pub fn resumePrompt(resume_ptr: *Resume, arg: ?*anyopaque) ?*anyopaque {
 583     if (resumeIsOnce(resume_ptr)) |p| {
 584         @branchHint(.likely);
 585         assert(p.refcount == 1);
 586         assert(p.resume_point != null);
 587         return promptResume(p, arg);
 588     } else {
 589         @branchHint(.unlikely);
 590         return mresume(resumeIsMulti(resume_ptr).?, arg);
 591     }
 592 }
 593 
 594 /// Resumes like `resumePrompt`, but the resumed code's next suspend or return jumps to the point
 595 /// that called the yield function, so this call does not return to its caller. A yield function
 596 /// that ends by resuming calls it to discard its own frame under the resumed code, as a generator
 597 /// does for each value. The call must be made from inside the yield function that received this
 598 /// resumption, as that function's last action. For a multi-shot handle, the first tail resume after
 599 /// `resumeMulti` works this way, and later ones act as `resumePrompt` does.
 600 pub fn resumeTailPrompt(resume_ptr: *Resume, arg: ?*anyopaque) ?*anyopaque {
 601     if (resumeIsOnce(resume_ptr)) |p| {
 602         @branchHint(.likely);
 603         return promptResumeTail(p, arg, p.return_point.?);
 604     } else {
 605         @branchHint(.unlikely);
 606         return mresumeTail(resumeIsMulti(resume_ptr).?, arg);
 607     }
 608 }
 609 
 610 /// Gives up the handle without resuming, for a caller that abandons suspended code it will not
 611 /// continue. For a one-shot handle, or the last reference of a multi-shot handle, the call frees
 612 /// the stacklets captured with it, and their frames never run again, `defer` statements included.
 613 pub fn resumeDrop(resume_ptr: *Resume) void {
 614     if (resumeIsOnce(resume_ptr)) |p| {
 615         @branchHint(.likely);
 616         promptDrop(p);
 617         return;
 618     } else {
 619         @branchHint(.unlikely);
 620         mresumeDrop(resumeIsMulti(resume_ptr).?);
 621     }
 622 }
 623 
 624 /// Returns a multi-shot handle for the same suspend, so a yield function converts its handle before
 625 /// resuming the same suspend more than once. The caller uses the returned handle in place of
 626 /// `resume_ptr`. Converting a one-shot handle allocates a record from the process allocator, and
 627 /// the record starts with one reference. The program stops with a panic if that allocation fails. A
 628 /// handle that is already multi-shot comes back unchanged.
 629 pub fn resumeMulti(resume_ptr: *Resume) *Resume {
 630     if (resumeIsMulti(resume_ptr)) |_| {
 631         @branchHint(.unlikely);
 632         return resume_ptr;
 633     }
 634 
 635     const p = resumeIsOnce(resume_ptr).?;
 636     const mr = processAllocator().create(MResume) catch fatal("unable to allocate multi-shot resume", .{});
 637     mr.* = .{
 638         .refcount = 1,
 639         .resume_count = 0,
 640         .prompt = p,
 641         .save = null,
 642         .tail_return_point = p.return_point,
 643     };
 644     return resumeAsMulti(mr);
 645 }
 646 
 647 /// Adds one reference to a multi-shot handle and returns the same handle, or returns null for a
 648 /// one-shot handle, so a caller takes one extra reference for each extra resume. The call copies no
 649 /// stack memory.
 650 pub fn resumeDup(resume_ptr: *Resume) ?*Resume {
 651     const mr = resumeIsMulti(resume_ptr) orelse {
 652         @branchHint(.unlikely);
 653         return null;
 654     };
 655     _ = mresumeDup(mr);
 656     return resume_ptr;
 657 }
 658 
 659 /// Returns the number of resumes of a multi-shot handle, or 0 for a one-shot handle, so a caller
 660 /// reads how many times a multi-shot handle has been resumed so far.
 661 pub fn resumeResumeCount(resume_ptr: *Resume) c_long {
 662     const mr = resumeIsMulti(resume_ptr) orelse return 0;
 663     return mr.resume_count;
 664 }
 665 
 666 /// Returns 1 for a multi-shot handle that holds the last reference and has never been resumed, and
 667 /// 0 otherwise, one-shot handles included, for the effect layer to choose between unwinding the
 668 /// suspended body and dropping one reference before releasing a resumption.
 669 pub fn resumeShouldUnwind(resume_ptr: *Resume) c_int {
 670     const mr = resumeIsMulti(resume_ptr) orelse return 0;
 671     return if (mr.refcount == 1 and mr.resume_count == 0) 1 else 0;
 672 }
 673 
 674 /// Fills `buffer` with the return addresses of the calling thread's current stack and returns how
 675 /// many it wrote, so a caller records the return addresses of its current stack for a diagnostic.
 676 /// The call returns 0 for an empty buffer or on a platform without stack capture.
 677 pub fn captureBacktrace(buffer: []?*anyopaque) usize {
 678     return sys.backtrace.capture(buffer);
 679 }
 680 
 681 fn promptStackEntry(penv: ?*anyopaque, unwind_frame: ?*UnwindFrame) callconv(.c) void {
 682     const env: *EntryEnv = @ptrCast(@alignCast(penv.?));
 683     const p = env.prompt;
 684     p.unwind_frame = unwind_frame;
 685 
 686     const result = env.fun(p, env.arg);
 687 
 688     var sp: ?*anyopaque = null;
 689     const ret = promptUnlink(p, null, &sp);
 690     ret.arg = result;
 691     ret.fun = null;
 692     ret.kind = .normal_return;
 693     checkedLongjmp(return_label, sp, &ret.jmp);
 694 }
 695 
 696 noinline fn promptResume(p: *Prompt, arg: ?*anyopaque) ?*anyopaque {
 697     var ret: ReturnPoint = undefined;
 698     if (context.mp_setjmp(&ret.jmp) != 0) {
 699         return promptExecReturnPoint(&ret, p);
 700     }
 701 
 702     if (return_label == null) return_label = guard(ret.jmp.reg_ip);
 703     assert(p.parent == null);
 704 
 705     var sp: ?*anyopaque = null;
 706     const res = promptLink(p, &ret, &sp);
 707     if (res) |resume_point| {
 708         resume_point.result = arg;
 709         checkedLongjmp(resume_label, sp, &resume_point.jmp);
 710     }
 711 
 712     p.gstack.enter(@ptrCast(&p.return_point), promptStackEntry, arg);
 713 }
 714 
 715 noinline fn promptExecReturnPoint(ret: *ReturnPoint, p: *Prompt) ?*anyopaque {
 716     assert(!promptIsActive(p));
 717     if (ret.kind == .yielded) return ret.fun.?(resumeAsOnce(p), ret.arg);
 718     if (ret.kind == .normal_return) {
 719         const result = ret.arg;
 720         promptDrop(p);
 721         return result;
 722     } else {
 723         @branchHint(.cold);
 724         fatal("unexpected prompt exception return", .{});
 725     }
 726 }
 727 
 728 fn promptResumeTail(p: *Prompt, arg: ?*anyopaque, ret: *ReturnPoint) ?*anyopaque {
 729     assert(p.refcount == 1);
 730     assert(!promptIsActive(p));
 731     assert(p.resume_point != null);
 732 
 733     var sp: ?*anyopaque = null;
 734     const res = promptLink(p, ret, &sp).?;
 735     res.result = arg;
 736     checkedLongjmp(resume_label, sp, &res.jmp);
 737 }
 738 
 739 fn promptLink(p: *Prompt, ret: *ReturnPoint, sp: *?*anyopaque) ?*ResumePoint {
 740     assert(!promptIsActive(p));
 741 
 742     sp.* = p.sp;
 743     p.parent = prompt_top;
 744     prompt_top = p.top;
 745     p.top = null;
 746     p.return_point = ret;
 747     p.sp = guard(ret.jmp.reg_sp);
 748     return p.resume_point;
 749 }
 750 
 751 fn promptUnlink(p: *Prompt, res: ?*ResumePoint, sp: *?*anyopaque) *ReturnPoint {
 752     assert(promptIsActive(p));
 753     assert(promptIsAncestor(p));
 754 
 755     sp.* = p.sp;
 756     p.top = prompt_top;
 757     prompt_top = p.parent;
 758     p.parent = null;
 759     p.resume_point = res;
 760     if (res) |resume_point| p.sp = guard(resume_point.jmp.reg_sp);
 761     return p.return_point.?;
 762 }
 763 
 764 fn promptDrop(p: *Prompt) void {
 765     promptDropInternal(p);
 766 }
 767 
 768 fn promptDropInternal(p: *Prompt) void {
 769     const old = p.refcount;
 770     p.refcount -= 1;
 771     if (old <= 1) {
 772         @branchHint(.likely);
 773         promptFree(p);
 774     }
 775 }
 776 
 777 fn promptFree(start: *Prompt) void {
 778     assert(!promptIsActive(start));
 779 
 780     var current: ?*Prompt = start.top;
 781     while (current) |p| {
 782         assert(p.refcount == 0);
 783         const parent = p.parent;
 784         p.gstack.free();
 785         promptRelease(p);
 786         if (parent) |q| {
 787             assert(q.refcount == 1);
 788             q.refcount -= 1;
 789         }
 790         current = parent;
 791     }
 792 }
 793 
 794 fn promptRelease(p: *Prompt) void {
 795     if (prompt_cache_count < config.stack_cache_count) {
 796         p.parent = prompt_cache;
 797         prompt_cache = p;
 798         prompt_cache_count += 1;
 799         return;
 800     }
 801 
 802     processAllocator().destroy(p);
 803 }
 804 
 805 fn promptDup(p: *Prompt) *Prompt {
 806     p.refcount += 1;
 807     return p;
 808 }
 809 
 810 fn promptIsActive(p: *Prompt) bool {
 811     return p.top == null;
 812 }
 813 
 814 fn promptIsAncestor(p: *Prompt) bool {
 815     var q = promptParent(null);
 816     while (q) |candidate| {
 817         if (candidate == p) return true;
 818         q = promptParent(candidate);
 819     }
 820     return false;
 821 }
 822 
 823 fn mresumeDup(mr: *MResume) *MResume {
 824     mr.refcount += 1;
 825     return mr;
 826 }
 827 
 828 fn mresumeDrop(mr: *MResume) void {
 829     const old = mr.refcount;
 830     mr.refcount -= 1;
 831     if (old > 1) return;
 832 
 833     var save = mr.save;
 834     while (save) |s| {
 835         const next = s.next;
 836         s.free();
 837         save = next;
 838     }
 839     promptDrop(mr.prompt);
 840     processAllocator().destroy(mr);
 841 }
 842 
 843 fn mresume(mr: *MResume, arg: ?*anyopaque) ?*anyopaque {
 844     mr.resume_count += 1;
 845     const p = resumeGetPrompt(mr);
 846     return promptResume(p, arg);
 847 }
 848 
 849 fn mresumeTail(mr: *MResume, arg: ?*anyopaque) ?*anyopaque {
 850     const ret = mr.tail_return_point orelse return mresume(mr, arg);
 851     mr.tail_return_point = null;
 852     mr.resume_count += 1;
 853     const p = resumeGetPrompt(mr);
 854     return promptResumeTail(p, arg, ret);
 855 }
 856 
 857 fn resumeGetPrompt(mr: *MResume) *Prompt {
 858     const p = mr.prompt;
 859     if (mr.save) |save| {
 860         promptRestore(p, save);
 861     } else if (mr.refcount > 1 or p.refcount > 1) {
 862         mr.save = promptSave(p);
 863     }
 864     _ = promptDup(p);
 865     mresumeDrop(mr);
 866     return p;
 867 }
 868 
 869 fn promptSave(p: *Prompt) *PromptSave {
 870     assert(!promptIsActive(p));
 871     const resume_point = p.resume_point.?;
 872 
 873     var save_head: ?*PromptSave = null;
 874     var sp: [*]u8 = @ptrCast(resume_point.jmp.reg_sp.?);
 875     var current: ?*Prompt = p.top;
 876     while (current) |q| {
 877         const save = processAllocator().create(PromptSave) catch fatal("unable to allocate prompt save", .{});
 878         _ = promptDup(q);
 879         save.* = .{
 880             .next = save_head,
 881             .prompt = q,
 882             .prompt_snapshot = q.*,
 883             .gsave = q.gstack.save(sp),
 884         };
 885         save_head = save;
 886 
 887         sp = if (q.parent != null)
 888             @ptrCast(q.return_point.?.jmp.reg_sp.?)
 889         else
 890             undefined;
 891         current = q.parent;
 892     }
 893 
 894     return save_head.?;
 895 }
 896 
 897 fn promptRestore(p: *Prompt, save_head: *PromptSave) void {
 898     assert(!promptIsActive(p));
 899     assert(p == save_head.prompt);
 900     var save: ?*PromptSave = save_head;
 901     while (save) |s| {
 902         s.prompt.* = s.prompt_snapshot;
 903         s.gsave.restore();
 904         save = s.next;
 905     }
 906 }
 907 
 908 fn resumeIsOnce(resume_ptr: *Resume) ?*Prompt {
 909     const raw = @intFromPtr(resume_ptr);
 910     return if ((raw & 4) == 0) @ptrFromInt(raw) else null;
 911 }
 912 
 913 fn resumeIsMulti(resume_ptr: *Resume) ?*MResume {
 914     const raw = @intFromPtr(resume_ptr);
 915     return if ((raw & 4) == 0) null else @ptrFromInt(raw ^ 4);
 916 }
 917 
 918 fn resumeAsOnce(p: *Prompt) *Resume {
 919     return @ptrCast(p);
 920 }
 921 
 922 fn resumeAsMulti(mr: *MResume) *Resume {
 923     return @ptrFromInt(@intFromPtr(mr) | 4);
 924 }
 925 
 926 fn checkedLongjmp(label: ?*anyopaque, sp: ?*anyopaque, jmp: *JmpBuf) noreturn {
 927     const expected_ip = unguard(label.?);
 928     const expected_sp = unguard(sp.?);
 929     if (expected_ip != jmp.reg_ip or expected_sp != jmp.reg_sp) {
 930         fatal(
 931             "potential stack corruption detected: expected ip/sp {any}/{any}, found {any}/{any}",
 932             .{ expected_ip, expected_sp, jmp.reg_ip, jmp.reg_sp },
 933         );
 934     }
 935     context.mp_longjmp(jmp);
 936 }
 937 
 938 fn guard(p: ?*anyopaque) ?*anyopaque {
 939     return @ptrFromInt(@intFromPtr(p.?) ^ guard_cookie);
 940 }
 941 
 942 fn unguard(p: *anyopaque) ?*anyopaque {
 943     return @ptrFromInt(@intFromPtr(p) ^ guard_cookie);
 944 }
 945 
 946 fn ensureInitialized(cfg: ?*const Config) void {
 947     if (initialized) return;
 948 
 949     var seed: usize = @intCast(sys.time.realNanoTimestamp());
 950     seed ^= @intFromPtr(&seed);
 951     if (seed == 0) seed = 0x9E3779B97F4A7C15;
 952     guard_cookie = seed;
 953 
 954     const defaults = configDefault();
 955     const source = cfg orelse &defaults;
 956     config = .{
 957         .stack_max_size = sanitizeSize(source.stack_max_size, 8 * @as(usize, @intCast(mib))),
 958         .stack_gap_size = sanitizeSize(source.stack_gap_size, 64 * @as(usize, @intCast(kib))),
 959         .stack_cache_count = sanitizeCount(source.stack_cache_count),
 960         .stack_initial_commit = sanitizeSize(source.stack_initial_commit, 0),
 961         .stack_grow_fast = !source.stack_use_overcommit and source.stack_grow_fast,
 962         .stack_use_overcommit = source.stack_use_overcommit,
 963         .stack_reset_decommits = source.stack_reset_decommits,
 964         .gpool_enable = !source.stack_use_overcommit and source.gpool_enable,
 965         .gpool_max_size = sanitizeSize(source.gpool_max_size, 256 * @as(usize, @intCast(gib))),
 966     };
 967 
 968     config.stack_max_size = alignUp(config.stack_max_size, page_size);
 969     config.stack_gap_size = alignUp(config.stack_gap_size, page_size);
 970     config.stack_initial_commit = alignUp(config.stack_initial_commit, page_size);
 971     config.gpool_max_size = alignUp(config.gpool_max_size, page_size);
 972     if (config.stack_max_size <= 2 * config.stack_gap_size + page_size) {
 973         fatal("mprompt stack_max_size must leave room for a usable stack", .{});
 974     }
 975 
 976     if (!config.stack_use_overcommit) {
 977         installStackGrowthHandler();
 978         ensureThreadSignalStack();
 979     }
 980 
 981     initialized = true;
 982 }
 983 
 984 fn sanitizeSize(value: isize, default_value: usize) usize {
 985     if (value <= 0) return default_value;
 986     return @intCast(value);
 987 }
 988 
 989 fn sanitizeCount(value: isize) usize {
 990     if (value < 0) return 0;
 991     return @intCast(value);
 992 }
 993 
 994 fn alignUp(value: usize, alignment: usize) usize {
 995     return std.mem.alignForward(usize, value, alignment);
 996 }
 997 
 998 fn alignDown(value: usize, alignment: usize) usize {
 999     return std.mem.alignBackward(usize, value, alignment);
1000 }
1001 
1002 fn initialCommitSize(stack_size: usize) usize {
1003     if (config.stack_use_overcommit) return stack_size;
1004     const requested = if (config.stack_initial_commit == 0) page_size else config.stack_initial_commit;
1005     return @min(requested, stack_size);
1006 }
1007 
1008 fn currentGStack() ?*GStack {
1009     return if (prompt_top) |top| top.gstack else null;
1010 }
1011 
1012 fn stackGrowthSignalHandler(sig: sys.signal.RawSignal, info: *const sys.signal.SignalInfo, ctx: ?*anyopaque) callconv(.c) void {
1013     if (signal.faultAddress(sig, info)) |addr| {
1014         if (currentGStack()) |g| {
1015             if (g.commitFaultPage(addr)) return;
1016         }
1017     }
1018     forwardSignal(sig, info, ctx);
1019 }
1020 
1021 fn forwardSignal(sig: sys.signal.RawSignal, info: *const sys.signal.SignalInfo, ctx: ?*anyopaque) noreturn {
1022     if (previousSignal(sig)) |previous| {
1023         sys.signal.dispatchPreviousAction(previous, sig, info, ctx);
1024     }
1025     sys.process.abort();
1026 }
1027 
1028 fn previousSignal(sig: sys.signal.RawSignal) ?sys.signal.SignalAction {
1029     if (sys.signal.isBusFaultSignal(sig)) {
1030         return if (have_previous_sigbus) previous_sigbus else null;
1031     }
1032     return if (have_previous_sigsegv) previous_sigsegv else null;
1033 }
1034 
1035 fn installStackGrowthHandler() void {
1036     if (growth_handler_installed) return;
1037 
1038     const act = sys.signal.stackFaultAction(stackGrowthSignalHandler);
1039     sys.signal.installAction(sys.signal.segmentationFaultSignal(), &act, &previous_sigsegv) catch fatal(
1040         "unable to install stack growth signal handler",
1041         .{},
1042     );
1043     have_previous_sigsegv = true;
1044 
1045     if (sys.signal.installBusFaultStackGrowthHandler()) {
1046         sys.signal.installAction(sys.signal.busFaultSignal(), &act, &previous_sigbus) catch fatal(
1047             "unable to install stack growth signal handler",
1048             .{},
1049         );
1050         have_previous_sigbus = true;
1051     }
1052 
1053     growth_handler_installed = true;
1054 }
1055 
1056 fn ensureThreadSignalStack() void {
1057     if (signal_stack_ready) return;
1058 
1059     const old_stack = sys.signal.readAlternateStack() catch fatal("unable to read alternate signal stack", .{});
1060     if (sys.signal.alternateStackIsInstalled(old_stack)) {
1061         signal_stack_ready = true;
1062         return;
1063     }
1064 
1065     const stack = processAllocator().alloc(u8, signal.stack_size) catch fatal(
1066         "unable to allocate alternate signal stack",
1067         .{},
1068     );
1069 
1070     const ss = sys.signal.alternateStack(stack.ptr, stack.len);
1071     sys.signal.installAlternateStack(&ss) catch {
1072         processAllocator().free(stack);
1073         fatal("unable to install alternate signal stack", .{});
1074     };
1075     signal_stack = stack;
1076     signal_stack_ready = true;
1077 }
1078 
1079 fn protect(memory: []align(page_size) u8, protection: sys.memory.Protection) !void {
1080     return sys.memory.protect(memory, protection) catch |err| switch (err) {
1081         error.AccessDenied, error.PermissionDenied => error.AccessDenied,
1082         error.OutOfMemory => error.OutOfMemory,
1083         error.InvalidMapping, error.UnsupportedPlatform, error.ProtectFailed => error.MProtectFailed,
1084     };
1085 }
1086 
1087 /// Returns the process-wide allocator of the `sys` package, so the effect layer allocates its
1088 /// resumption records from the same allocator as the runtime. The runtime allocates from it the
1089 /// prompt records, the stacklet records, the saved stack copies, and the records behind multi-shot
1090 /// handles. A program can replace this allocator through the `sys` package's override.
1091 pub fn processAllocator() std.mem.Allocator {
1092     return sys.allocator.processAllocator();
1093 }
1094 
1095 fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
1096     std.debug.panic("lib/mprompt: " ++ fmt, args);
1097 }
1098 
1099 fn returnArg(_: *Prompt, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1100     return arg;
1101 }
1102 
1103 fn intToPtr(value: usize) ?*anyopaque {
1104     if (value == 0) return null;
1105     return @ptrFromInt(value);
1106 }
1107 
1108 fn ptrToInt(value: ?*anyopaque) usize {
1109     return if (value) |ptr| @intFromPtr(ptr) else 0;
1110 }
1111 
1112 test "prompt returns action result" {
1113     try std.testing.expectEqual(@as(usize, 42), ptrToInt(prompt(returnArg, intToPtr(42))));
1114 }
1115 
1116 fn resumeWithArg(continuation: *Resume, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1117     return resumePrompt(continuation, arg);
1118 }
1119 
1120 fn addOneAfterYield(prompt_instance: *Prompt, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1121     const yielded = yieldPrompt(prompt_instance, resumeWithArg, intToPtr(41));
1122     return intToPtr(ptrToInt(yielded) + 1);
1123 }
1124 
1125 test "yield captures and resumes the prompt context" {
1126     try std.testing.expectEqual(@as(usize, 42), ptrToInt(prompt(addOneAfterYield, null)));
1127 }
1128 
1129 fn awaitResult(continuation: *Resume, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1130     return @ptrCast(continuation);
1131 }
1132 
1133 fn asyncWorker(prompt_instance: *Prompt, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1134     const arg = yieldPrompt(prompt_instance, awaitResult, null);
1135     return intToPtr(ptrToInt(arg) + 1);
1136 }
1137 
1138 test "suspended prompt can be resumed later" {
1139     const suspended_any = prompt(asyncWorker, null).?;
1140     const suspended: *Resume = @ptrCast(@alignCast(suspended_any));
1141     try std.testing.expectEqual(@as(usize, 8), ptrToInt(resumePrompt(suspended, intToPtr(7))));
1142 }
1143 
1144 const GeneratorEnv = struct {
1145     values: [16]usize = @splat(0),
1146     count: usize = 0,
1147     current: usize = 0,
1148     limit: usize,
1149 };
1150 
1151 fn collectYield(continuation: *Resume, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1152     const env: *GeneratorEnv = @ptrCast(@alignCast(arg.?));
1153     env.values[env.count] = env.current;
1154     env.count += 1;
1155     return resumeTailPrompt(continuation, null);
1156 }
1157 
1158 fn generator(prompt_instance: *Prompt, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1159     const env: *GeneratorEnv = @ptrCast(@alignCast(arg.?));
1160     var i: usize = 0;
1161     while (i < env.limit) : (i += 1) {
1162         env.current = i;
1163         _ = yieldPrompt(prompt_instance, collectYield, env);
1164     }
1165     return null;
1166 }
1167 
1168 test "generator-style repeated tail resumes preserve state" {
1169     var env: GeneratorEnv = .{ .limit = 10 };
1170     _ = prompt(generator, &env);
1171 
1172     try std.testing.expectEqual(@as(usize, 10), env.count);
1173     for (env.values[0..env.count], 0..) |value, index| {
1174         try std.testing.expectEqual(index, value);
1175     }
1176 }
1177 
1178 fn addTenAfterYield(prompt_instance: *Prompt, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1179     const value = yieldPrompt(prompt_instance, resumeTwice, null);
1180     return intToPtr(ptrToInt(value) + 10);
1181 }
1182 
1183 fn resumeTwice(continuation: *Resume, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1184     const multi = resumeMulti(continuation);
1185     const first_resume = resumeDup(multi).?;
1186     const first = ptrToInt(resumePrompt(first_resume, intToPtr(1)));
1187     const second = ptrToInt(resumePrompt(multi, intToPtr(2)));
1188     return intToPtr(first + second);
1189 }
1190 
1191 test "multi-shot resumption restores the saved prompt stack" {
1192     try std.testing.expectEqual(@as(usize, 23), ptrToInt(prompt(addTenAfterYield, null)));
1193 }
1194 
1195 test "completed prompts reuse cached prompt metadata" {
1196     const first = promptCreate();
1197     const first_addr = @intFromPtr(first);
1198     try std.testing.expectEqual(@as(?*anyopaque, @ptrFromInt(17)), promptEnter(first, returnArg, @ptrFromInt(17)));
1199 
1200     const second = promptCreate();
1201     const second_addr = @intFromPtr(second);
1202     try std.testing.expectEqual(first_addr, second_addr);
1203     try std.testing.expectEqual(@as(?*anyopaque, @ptrFromInt(19)), promptEnter(second, returnArg, @ptrFromInt(19)));
1204 }