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 }