lib/mprompt/src/effect.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 //! Code in a body makes a named request, and the nearest enclosing code registered for that request
   2 //! answers it and decides whether the body continues, how often, and with what value: algebraic
   3 //! effect handlers, built on the package's stack-switching runtime. Two layers provide this
   4 //! control: C-convention functions over tables of raw pointers, and a typed layer that builds those
   5 //! tables at compile time from a Zig description of the requests.
   6 //!
   7 //! A body wants to ask for things such as a value to read, a state to update, a choice among
   8 //! branches, or an early exit, without knowing which enclosing code answers, so the same body can
   9 //! run under different answers. The answering code needs a range of powers: answer and let the body
  10 //! go on at once, keep the body waiting and continue it later, continue it several times to explore
  11 //! each branch of a choice, or end it early.
  12 //!
  13 //! Finding the answering code happens at run time, by walking outward from the request through the
  14 //! code that encloses it, because answering code nests and one body can run under different
  15 //! answering code. Keeping the body waiting means switching stacks and saving registers, and an
  16 //! answer that lets the body go on at once, such as reading a value or updating a counter, needs
  17 //! neither. Continuing a body more than once needs a copy of its suspended stack, and ending it
  18 //! early has to discard that stack without running the rest of the body.
  19 //!
  20 //! Daan Leijen's paper [Implementing Algebraic Effects in
  21 //! C](https://www.microsoft.com/en-us/research/publication/implementing-algebraic-effects-c/) and
  22 //! his [libmprompt](https://github.com/koka-lang/libmprompt) library implement algebraic effect
  23 //! handlers on top of multi-prompt delimited continuations. The package keeps libmprompt's way of
  24 //! moving control through handlers: each handler runs its body under a prompt of its own, and an
  25 //! operation that suspends the body moves control to its handler's prompt. The package's reference
  26 //! benchmark reruns libmprompt's effect workloads, among them a reader, a state counter, a choice
  27 //! search, n-queens and a triples search, at the sizes meant for optimized builds.
  28 //!
  29 //! The package identifies each effect by one table of strings: the effect's name, then one name per
  30 //! operation. The runtime finds an operation's handler by comparing that table's address. A handler
  31 //! wraps its body in a frame on a new stack, and the runtime walks the frames outward from the call
  32 //! that performs an operation to the innermost handler for its effect. A handler answers each of
  33 //! its operations with a function written for that operation (*clause*). Each clause carries a kind
  34 //! fixed when the handler is written, so a clause that lets the body go on at once runs in place on
  35 //! the body's stack as an ordinary call, and only the kinds that may keep the body waiting switch
  36 //! stacks. One of those kinds allows at most one resume, made while its clause runs, so it keeps
  37 //! its handle in the clause's own stack frame and allocates nothing. The others allocate their
  38 //! handle from the process allocator. The handler search keeps the last handler it found and reuses
  39 //! it for the same effect until a frame is pushed or popped. The typed layer declares an effect
  40 //! once with `EffectDefinition`, builds its tables at compile time, allows at most eight
  41 //! operations, and rejects a handler that leaves an operation without a clause at compile time.
  42 //!
  43 //! - *local state*: a pointer the handler keeps and receives back with each resume
  44 //! - *result function*: a handler's optional function that replaces the body's result
  45 //! - *operation tag*: the record naming one operation by its effect and index
  46 //! - *mask*: a frame that makes operations of one effect skip their innermost handler
  47 //! - *release*: end a suspended body without running the rest of it
  48 //! - *tail resume*: a resume made as a function's last act that never returns to it
  49 //! - *one-shot handle*: a handle resumable at most once, continuing in place with no copy
  50 //! - *multi-shot handle*: a reference-counted handle resumable more than once
  51 const std = @import("std");
  52 const pretty = @import("pretty");
  53 const mp = @import("root.zig");
  54 const effect_mod = @This();
  55 const assert = std.debug.assert;
  56 
  57 /// The function type for the body of `handle`, `handleRaw`, `mask` and `finally`. A caller passes
  58 /// one as the code to run under a handler or a frame. The function uses the C calling convention,
  59 /// takes one pointer argument and returns one pointer result.
  60 pub const ActionFn = *const fn (?*anyopaque) callconv(.c) ?*anyopaque;
  61 /// The function type of a handler's result function. A handler that turns the body's final result
  62 /// into something else supplies one, as a counting handler turns each finished branch into 1. The
  63 /// function receives the handler's local state and the body's result, and its return value becomes
  64 /// the result of `handle`.
  65 pub const ResultFn = *const fn (?*anyopaque, ?*anyopaque) callconv(.c) ?*anyopaque;
  66 /// The function type that `finally` calls after its body returns. The function receives the local
  67 /// pointer given to `finally`.
  68 pub const ReleaseFn = *const fn (?*anyopaque) callconv(.c) void;
  69 /// The function type of a clause. A handler author writes one per operation. The function receives
  70 /// the resumption, the handler's local state and the operation's argument, and it returns the
  71 /// clause's result. Resuming the resumption makes the body's `perform` call return the value passed
  72 /// in. The resumption is null for `abort` and `never` clauses. For `tail_noop` and `tail` clauses,
  73 /// the return value becomes the result of `perform`. For the other kinds, the return value becomes
  74 /// the result of the `handle` call, or of the resume that last continued the body.
  75 pub const OpFn = *const fn (?*Resume, ?*anyopaque, ?*anyopaque) callconv(.c) ?*anyopaque;
  76 
  77 /// A pointer to a null-terminated array of C strings: the effect's name, then one name per
  78 /// operation in index order. A raw caller defines one per set of operations and uses it in tags and
  79 /// handler tables. The runtime matches a handler to an operation's effect by this pointer's
  80 /// address, so each effect needs exactly one array, and two arrays that hold the same strings are
  81 /// two different effects. The typed layer builds the array from the effect's name and one
  82 /// `name/operation` string per operation.
  83 pub const Effect = [*:null]const ?[*:0]const u8;
  84 
  85 /// Names one operation by its effect and its index. A raw caller defines one constant per operation
  86 /// and performs with its address. The `perform` call takes a pointer to one, and the tag's index
  87 /// picks the handler's clause.
  88 pub const Optag = extern struct {
  89     /// The effect the operation belongs to, which the handler search matches by address. The
  90     /// `optagName` function prints `<null>` for a null effect.
  91     effect: ?Effect,
  92     /// The operation's zero-based index. The index picks the handler's clause and the name at
  93     /// position index plus one in the effect's array. An index of 8 or more fails the bounds check
  94     /// in safe builds, and nothing checks that the clause at the index answers this operation.
  95     opidx: c_long,
  96 };
  97 
  98 /// Lists the kinds of clause a handler table holds, as C `int` values in declaration order. A
  99 /// handler author picks one per operation to say how its clause may continue the body, and the
 100 /// runtime takes the cheapest way to run the clause from it. The kind decides whether the clause
 101 /// runs on the body's stack or after a stack switch, and whether its resumption lives on a stack or
 102 /// is allocated.
 103 pub const OperationKind = enum(c_int) {
 104     /// Marks an empty slot, and handler tables are padded with it. Performing an operation whose
 105     /// slot holds it runs nothing and returns null. The `on` helper rejects it at compile time.
 106     null_op,
 107     /// The clause passes the operation to the next enclosing handler of the same effect: it
 108     /// performs the operation again inside a mask, and the mask covers this handler's effect. The
 109     /// slot holds no function. The typed layer builds this clause with `forward()`, and `on`
 110     /// rejects this kind.
 111     forward,
 112     /// The clause ends the body: control goes back to the handler, the body's stacklets are dropped
 113     /// without running the rest of the body, and the clause runs with a null resumption. The
 114     /// clause's result becomes the result of `handle`, and the handler's result function does not
 115     /// run. A `finally` call inside the dropped body never calls its `finally_fun` function. A
 116     /// typed clause of this kind returns the result type of `handle`.
 117     abort,
 118     /// Runs the same code path as `abort`.
 119     never,
 120     /// The clause runs in place on the body's stack as an ordinary call, with no stack switch. The
 121     /// resumption lives on that stack, and a raw clause continues the body by returning
 122     /// `resumeTail(...)`. A typed clause of this kind returns the operation's result. An operation
 123     /// the clause performs itself starts its search at the body's innermost frame, so it can reach
 124     /// this same handler.
 125     tail_noop,
 126     /// The clause runs in place on the body's stack, as with `tail_noop`. While the clause runs, an
 127     /// extra frame makes the operations it performs skip this handler and every frame inside it, so
 128     /// they reach the handlers outside.
 129     tail,
 130     /// The body suspends to the handler's prompt, and the clause runs on the handler's side with a
 131     /// resumption kept in its own stack frame, so nothing is allocated. The clause resumes the body
 132     /// at most once, before it returns, and it cannot release the resumption. A clause that returns
 133     /// without resuming leaves the body's stacklet allocated.
 134     scoped_once,
 135     /// Runs the same code path as `multi`: the clause gets an allocated resumption that can resume
 136     /// more than once. The tests use it for clauses that resume the body several times before they
 137     /// return.
 138     scoped,
 139     /// The body suspends, and the clause gets an allocated resumption that it may keep after it
 140     /// returns. The clause resumes it at most once, with `resumeFinal` or `resumeTail`, and
 141     /// `resumeEffect` on it fails an assertion in safe builds. A kept resumption can be resumed
 142     /// later under a different handler.
 143     once,
 144     /// The body suspends, and the clause gets an allocated resumption that can resume more than
 145     /// once. The `resumeEffect` call keeps the resumption for another resume, and `resumeFinal`,
 146     /// `resumeTail` or `resumeRelease` ends it.
 147     multi,
 148 };
 149 
 150 /// One slot of a handler table: the operation's tag, its clause, and the kind of that clause. A raw
 151 /// handler table holds one per operation.
 152 pub const Operation = extern struct {
 153     /// The kind of the slot's clause, which decides how `perform` runs the clause.
 154     opkind: OperationKind,
 155     /// The tag of the operation this slot answers. A `forward` slot performs the operation again
 156     /// with it. The `perform` call selects the slot by the performed tag's index and does not
 157     /// compare this field with that tag.
 158     optag: ?*const Optag,
 159     /// The slot's clause, null for `forward` and `null_op` slots.
 160     opfun: ?OpFn,
 161 };
 162 
 163 /// The number of slots in every handler table, and so the most operations one effect can have. A
 164 /// caller sizes handler tables with it, and `EffectDefinition` checks each effect against it.
 165 pub const max_operations = 8;
 166 
 167 /// Describes one handler: its effect, an optional result function, and a table of eight slots that
 168 /// hold its clauses. A raw caller builds one per handler, usually as a constant, and passes its
 169 /// address to `handle`. The `handle` function keeps a pointer to it while the body runs, so it must
 170 /// outlive the call.
 171 pub const HandlerDef = extern struct {
 172     /// The effect this handler answers. The runtime picks this handler for an operation when this
 173     /// pointer equals the operation's effect.
 174     effect: ?Effect,
 175     /// An optional function applied to the body's result, with the handler's current local state,
 176     /// when the body returns. A null value leaves the result unchanged. The function does not run
 177     /// when an `abort` or `never` clause ends the body.
 178     resultfun: ?ResultFn,
 179     /// The eight slots that hold the handler's clauses: slot i answers the operation whose index is
 180     /// i. Unused slots hold `null_op`.
 181     operations: [max_operations]Operation,
 182 };
 183 
 184 /// Records an operation's argument type and result type. A caller writes one with `operation` for
 185 /// each entry of an `EffectDefinition` spec.
 186 pub const OperationSignature = struct {
 187     /// The type `perform` passes to the clause, and `void` means the operation takes no argument.
 188     Arg: type,
 189     /// The type the clause gives back to `perform`.
 190     Result: type,
 191 };
 192 
 193 /// Returns the signature of one operation with argument type `Arg` and result type `Result`, so a
 194 /// caller writes one per field of an `EffectDefinition` spec's `.operations`.
 195 pub fn operation(comptime Arg: type, comptime Result: type) OperationSignature {
 196     return .{ .Arg = Arg, .Result = Result };
 197 }
 198 
 199 /// Returns a clause that answers an operation with `handler` under clause kind `kind`, so a caller
 200 /// writes one per operation in the clauses passed to a typed `handle`. A `kind` of `.forward` is a
 201 /// compile error that points to `forward()`, and `.null_op` is a compile error. For `tail_noop`,
 202 /// `tail`, `abort` and `never`, the handler takes `()`, `(arg)` or `(context, arg)`, and the empty
 203 /// form needs a `void` argument. For `scoped_once`, `scoped`, `once` and `multi`, the handler takes
 204 /// `(continuation)`, `(continuation, arg)` or `(continuation, context, arg)`, and the one-parameter
 205 /// form needs a `void` argument. The `tail_noop` and `tail` handlers return the operation's result,
 206 /// and the other kinds return the result type of `handle`. The handler must be a function with a
 207 /// declared return type, and a generic handler is a compile error.
 208 pub fn on(comptime kind: OperationKind, comptime handler: anytype) HandlerClause(@TypeOf(handler)) {
 209     if (kind == .forward) {
 210         @compileError("use forward() for forwarded operations");
 211     }
 212     if (kind == .null_op) {
 213         @compileError("null operations are only used to terminate raw handler tables");
 214     }
 215     return .{ .kind = kind, .handler = handler };
 216 }
 217 
 218 /// Returns a clause that passes the operation to the next enclosing handler of the same effect, so
 219 /// an inner handler that answers some operations of an effect lets an outer handler answer the
 220 /// rest.
 221 pub fn forward() ForwardClause {
 222     return .{};
 223 }
 224 
 225 fn HandlerClause(comptime Handler: type) type {
 226     return struct {
 227         kind: OperationKind,
 228         handler: Handler,
 229     };
 230 }
 231 
 232 const ForwardClause = struct {
 233     kind: OperationKind = .forward,
 234 };
 235 
 236 const ContinueKind = enum {
 237     regular,
 238     final,
 239     tail,
 240 };
 241 
 242 /// Returns a type for one effect so a caller declares an effect once in Zig, then performs and
 243 /// handles its operations with checked types and no pointer casts. The type is built from a spec
 244 /// with a `.name` string and an `.operations` struct literal whose fields are
 245 /// `operation(Arg, Result)` values. A spec with more than eight operations is a compile error, and
 246 /// so is an `.operations` value of any type other than a struct literal. The type builds the
 247 /// effect's name table and one operation tag per operation at compile time, with operation names of
 248 /// the form `name/operation`.
 249 pub fn EffectDefinition(comptime spec: anytype) type {
 250     const Operations = @TypeOf(spec.operations);
 251     const operations_info = switch (@typeInfo(Operations)) {
 252         .@"struct" => |info| info,
 253         else => @compileError("effect operations must be a struct literal"),
 254     };
 255     const operation_names = operations_info.field_names;
 256     if (operation_names.len > max_operations) {
 257         @compileError("effect definitions support at most eight operations");
 258     }
 259 
 260     return struct {
 261         /// An enum with one tag per field of `.operations`, in declaration order. `optag`,
 262         /// `Signature`, `Continuation`, `perform` and `performWithoutValue` take one to pick the
 263         /// operation.
 264         pub const Op: type = std.meta.FieldEnum(Operations);
 265         /// The number of operations in the spec.
 266         pub const operation_count = operation_names.len;
 267 
 268         const Self = @This();
 269 
 270         const names = blk: {
 271             var result: [operation_count + 2:null]?[*:0]const u8 = undefined;
 272             result[0] = spec.name;
 273             for (operation_names, 0..) |operation_name, index| {
 274                 result[index + 1] = std.fmt.comptimePrint("{s}/{s}", .{ spec.name, operation_name });
 275             }
 276             result[operation_count + 1] = null;
 277             break :blk result;
 278         };
 279 
 280         const tags = blk: {
 281             var result: [operation_count]Optag = undefined;
 282             for (operation_names, 0..) |_, index| {
 283                 result[index] = .{ .effect = rawEffect(), .opidx = index };
 284             }
 285             break :blk result;
 286         };
 287 
 288         /// Returns the effect's name table so raw calls that name this effect take it.
 289         pub fn rawEffect() Effect {
 290             return @ptrCast(&names);
 291         }
 292 
 293         /// Returns a pointer to `op_id`'s operation tag so raw calls that perform or handle one
 294         /// operation take its tag. The tag's index is the operation's position in `.operations`.
 295         pub fn optag(comptime op_id: Op) *const Optag {
 296             return &tags[operationIndex(op_id)];
 297         }
 298 
 299         /// Returns the argument type and result type of `op_id` as the spec declares them.
 300         pub fn Signature(comptime op_id: Op) OperationSignature {
 301             return @field(spec.operations, @tagName(op_id));
 302         }
 303 
 304         /// Returns the continuation type that a clause of kind `kind` for `op_id` receives, so a
 305         /// continuation clause names its parameter type with it. A resume of that continuation
 306         /// passes a value of `op_id`'s result type into the body and returns `HandlerResult`.
 307         /// `HandlerResult` must be the `Result` of the `handle` call, and the compiler rejects a
 308         /// mismatch at the clause call site.
 309         pub fn Continuation(comptime op_id: Op, comptime kind: OperationKind, comptime HandlerResult: type) type {
 310             const sig = Signature(op_id);
 311             return EffectContinuation(sig.Result, HandlerResult, kind);
 312         }
 313 
 314         /// Performs `op_id` with `arg` and returns the result the clause gives, so code under a
 315         /// handler asks for an operation with a typed argument and gets a typed result. The
 316         /// argument and the result travel in a small record on the performer's stack, and the
 317         /// clause reaches that record by address. Whether the call suspends the body depends on the
 318         /// clause's kind. With no enclosing handler for the effect, the runtime prints
 319         /// `lib/mpeff: unhandled operation:` and the operation's name to standard error. An
 320         /// operation with a non-`void` result then unwraps a null pointer, which panics in safe
 321         /// builds, and an operation with a `void` result returns normally.
 322         pub fn perform(comptime op_id: Op, arg: Signature(op_id).Arg) Signature(op_id).Result {
 323             const sig = Signature(op_id);
 324             const OpFrame = PerformFrame(sig.Arg, sig.Result);
 325             var frame: OpFrame = .{};
 326             writeSlot(sig.Arg, &frame.arg, arg);
 327             return readSlot(sig.Result, performRaw(optag(op_id), &frame));
 328         }
 329 
 330         /// Performs `op_id` with no argument, for an operation whose `Arg` is `void`. Any other
 331         /// operation is a compile error.
 332         pub fn performWithoutValue(comptime op_id: Op) Signature(op_id).Result {
 333             const sig = Signature(op_id);
 334             if (sig.Arg != void) {
 335                 @compileError("performWithoutValue requires an operation with a void argument");
 336             }
 337             return Self.perform(op_id, {});
 338         }
 339 
 340         /// Runs `body(context)` on a new stacklet under a handler for this effect and returns a
 341         /// `Result`, so a caller runs a body under this effect's typed clauses and gets back the
 342         /// body's result, or the result a clause chose. The `context` parameter must be a pointer,
 343         /// and every clause that asks for it receives it. The `clauses` argument is a struct
 344         /// literal with one field per operation, each made by `on` or `forward()`, and a missing
 345         /// field is a compile error. The handler table is built at compile time. The result is the
 346         /// body's return value, the value an `abort` or `never` clause returns, or the value a
 347         /// continuation clause returns. An error union passes through as `Result`.
 348         pub fn handle(
 349             comptime Result: type,
 350             context: anytype,
 351             comptime body: *const fn (@TypeOf(context)) Result,
 352             comptime clauses: anytype,
 353         ) Result {
 354             const Context = @TypeOf(context);
 355             requirePointer(Context, "effect handler context");
 356             const Clauses = @TypeOf(clauses);
 357 
 358             const Runner = struct {
 359                 const Env = struct {
 360                     context: Context,
 361                     result: Slot(Result) = .{},
 362                 };
 363 
 364                 fn start(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
 365                     const env: *Env = @ptrCast(@alignCast(arg.?));
 366                     writeSlot(Result, &env.result, body(env.context));
 367                     return slotPtr(Result, &env.result);
 368                 }
 369 
 370                 fn table() [max_operations]Operation {
 371                     var entries = @as([max_operations]Operation, @splat(.{ .opkind = .null_op, .optag = null, .opfun = null }));
 372                     inline for (operation_names) |operation_name| {
 373                         if (!hasStructField(Clauses, operation_name)) {
 374                             @compileError("missing handler clause for operation '" ++ operation_name ++ "'");
 375                         }
 376                         const op_id = std.meta.stringToEnum(Op, operation_name).?;
 377                         entries[operationIndex(op_id)] = operationEntry(op_id);
 378                     }
 379                     return entries;
 380                 }
 381 
 382                 fn operationEntry(comptime op_id: Op) Operation {
 383                     const clause = @field(clauses, @tagName(op_id));
 384                     return .{
 385                         .opkind = clause.kind,
 386                         .optag = optag(op_id),
 387                         .opfun = switch (clause.kind) {
 388                             .forward => null,
 389                             .null_op => null,
 390                             else => thunk(op_id),
 391                         },
 392                     };
 393                 }
 394 
 395                 fn thunk(comptime op_id: Op) OpFn {
 396                     return struct {
 397                         fn call(raw_resume: ?*Resume, local: ?*anyopaque, raw_arg: ?*anyopaque) callconv(.c) ?*anyopaque {
 398                             const env: *Env = @ptrCast(@alignCast(local.?));
 399                             const sig = Signature(op_id);
 400                             const OpFrame = PerformFrame(sig.Arg, sig.Result);
 401                             const frame: *OpFrame = @ptrCast(@alignCast(raw_arg.?));
 402                             const op_arg = readSlotValue(sig.Arg, &frame.arg);
 403                             const clause = @field(clauses, @tagName(op_id));
 404 
 405                             switch (clause.kind) {
 406                                 .tail_noop, .tail => {
 407                                     const op_result = callTailHandler(clause.handler, env.context, op_arg);
 408                                     writeSlot(sig.Result, &frame.result, op_result);
 409                                     return slotPtr(sig.Result, &frame.result);
 410                                 },
 411                                 .abort, .never => {
 412                                     const result = callTailHandler(clause.handler, env.context, op_arg);
 413                                     writeSlot(Result, &env.result, result);
 414                                     return slotPtr(Result, &env.result);
 415                                 },
 416                                 .scoped_once, .scoped, .once, .multi => {
 417                                     const continuation: EffectContinuation(sig.Result, Result, clause.kind) = .{
 418                                         .raw_resume = raw_resume.?,
 419                                         .local = local,
 420                                     };
 421                                     const result = callContinuationHandler(clause.handler, continuation, env.context, op_arg);
 422                                     writeSlot(Result, &env.result, result);
 423                                     return slotPtr(Result, &env.result);
 424                                 },
 425                                 .forward, .null_op => unreachable,
 426                             }
 427                         }
 428                     }.call;
 429                 }
 430 
 431                 const hdef = HandlerDef{
 432                     .effect = rawEffect(),
 433                     .resultfun = null,
 434                     .operations = table(),
 435                 };
 436             };
 437 
 438             var env: Runner.Env = .{ .context = context };
 439             return readSlot(Result, handleRaw(&Runner.hdef, &env, Runner.start, &env));
 440         }
 441 
 442         /// Runs a body that takes no arguments, the way `handle` does, so a caller whose body and
 443         /// clauses need no context skips the pointer. A clause of the `(context, arg)` form
 444         /// receives a pointer to an empty struct.
 445         pub fn handleWithoutContext(
 446             comptime Result: type,
 447             comptime body: *const fn () Result,
 448             comptime clauses: anytype,
 449         ) Result {
 450             const Context = struct {};
 451             const Runner = struct {
 452                 fn start(_: *Context) Result {
 453                     return body();
 454                 }
 455             };
 456 
 457             var context: Context = .{};
 458             return Self.handle(Result, &context, Runner.start, clauses);
 459         }
 460 
 461         fn operationIndex(comptime op_id: Op) usize {
 462             inline for (operation_names, 0..) |operation_name, index| {
 463                 if (std.mem.eql(u8, operation_name, @tagName(op_id))) return index;
 464             }
 465             unreachable;
 466         }
 467     };
 468 }
 469 
 470 /// Returns the continuation type for clause kinds `scoped_once`, `scoped`, `once` and `multi`, so a
 471 /// typed clause that may keep the body waiting receives one and resumes the body with the
 472 /// operation's result. Any other kind is a compile error. `ResumeValue` is the operation's result
 473 /// type, which `perform` returns in the body. `HandlerResult` is the result type of the `handle`
 474 /// call. A resume returns when the resumed body finishes, with the body's result, or when a later
 475 /// clause suspends the body and returns, with that clause's result. A `scoped_once` or `once`
 476 /// continuation is resumed at most once, and a `scoped` or `multi` continuation is resumed any
 477 /// number of times, with its last resume final. A `scoped`, `once` or `multi` continuation that
 478 /// gets no final resume and no release stays allocated.
 479 pub fn EffectContinuation(comptime ResumeValue: type, comptime HandlerResult: type, comptime kind: OperationKind) type {
 480     comptime {
 481         switch (kind) {
 482             .scoped_once, .scoped, .once, .multi => {},
 483             else => @compileError("effect continuations are only available for scoped_once, scoped, once, and multi operations"),
 484         }
 485     }
 486 
 487     return struct {
 488         /// The raw resumption for the suspended body.
 489         raw_resume: *Resume,
 490         /// The handler's local state, passed back with each resume. The typed `handle` sets it to
 491         /// the address of its own environment, which holds the context and the result.
 492         local: ?*anyopaque,
 493 
 494         const Self = @This();
 495 
 496         /// Resumes the body with `value` as the result of its `perform` and returns the handler's
 497         /// result, for every resume but the last in a clause that resumes the body more than once,
 498         /// as a search over both branches of a choice does. A `scoped` or `multi` continuation
 499         /// stays usable for another resume. This call is the one resume for a `scoped_once`
 500         /// continuation. A `once` continuation is a compile error here, and it uses
 501         /// `continueFinalWith` or `continueTailWith`.
 502         pub fn continueWith(self: Self, value: ResumeValue) HandlerResult {
 503             if (kind == .once) {
 504                 @compileError("once continuations must use continueFinalWith or continueTailWith");
 505             }
 506             return self.continueInternal(.regular, value);
 507         }
 508 
 509         /// Resumes the body with `value` as the result of its `perform` and returns the handler's
 510         /// result, so a clause makes its last resume of the body. This call is the continuation's
 511         /// last resume: it frees an allocated continuation, which no caller uses again.
 512         pub fn continueFinalWith(self: Self, value: ResumeValue) HandlerResult {
 513             return self.continueInternal(.final, value);
 514         }
 515 
 516         /// Resumes the body with `value` as a tail resume and frees an allocated continuation, for
 517         /// a clause whose last act is to resume the body, so no clause frame stays under the
 518         /// resumed body. For every kind this type allows, control does not come back to the clause,
 519         /// and the result goes to the call that entered or last resumed the handled body. The call
 520         /// must be the clause's last action.
 521         pub fn continueTailWith(self: Self, value: ResumeValue) HandlerResult {
 522             return self.continueInternal(.tail, value);
 523         }
 524 
 525         /// Resumes the body with no value, for a `ResumeValue` of `void`. Any other `ResumeValue`
 526         /// is a compile error.
 527         pub fn continueWithoutValue(self: Self) HandlerResult {
 528             if (ResumeValue != void) {
 529                 @compileError("continueWithoutValue requires ResumeValue to be void");
 530             }
 531             return self.continueWith({});
 532         }
 533 
 534         /// Makes the final resume with no value, for a `ResumeValue` of `void`. Any other
 535         /// `ResumeValue` is a compile error.
 536         pub fn continueFinalWithoutValue(self: Self) HandlerResult {
 537             if (ResumeValue != void) {
 538                 @compileError("continueFinalWithoutValue requires ResumeValue to be void");
 539             }
 540             return self.continueFinalWith({});
 541         }
 542 
 543         /// Makes a tail resume with no value, for a `ResumeValue` of `void`. Any other
 544         /// `ResumeValue` is a compile error.
 545         pub fn continueTailWithoutValue(self: Self) HandlerResult {
 546             if (ResumeValue != void) {
 547                 @compileError("continueTailWithoutValue requires ResumeValue to be void");
 548             }
 549             return self.continueTailWith({});
 550         }
 551 
 552         /// Ends the suspended body without running its rest, for a clause that will not continue
 553         /// the body, such as a failing branch of a search. The body is resumed only to jump back to
 554         /// its handler, and its stacklet is freed. For a `multi` or `scoped` continuation that
 555         /// other references share, or that has resumed before, the call gives up one reference and
 556         /// resumes nothing. The call is valid for `scoped`, `once` and `multi` continuations, and
 557         /// any other kind is a compile error. A `finally` call inside the ended body never calls
 558         /// its `finally_fun` function.
 559         pub fn release(self: Self) void {
 560             switch (kind) {
 561                 .scoped, .once, .multi => {},
 562                 else => @compileError("release is only valid for scoped, once, and multi continuations"),
 563             }
 564             resumeRelease(self.raw_resume);
 565         }
 566 
 567         fn continueInternal(self: Self, comptime continue_kind: ContinueKind, value: ResumeValue) HandlerResult {
 568             var slot: Slot(ResumeValue) = .{};
 569             writeSlot(ResumeValue, &slot, value);
 570             const result = switch (continue_kind) {
 571                 .regular => resumeEffect(self.raw_resume, self.local, slotPtr(ResumeValue, &slot)),
 572                 .final => resumeFinal(self.raw_resume, self.local, slotPtr(ResumeValue, &slot)),
 573                 .tail => resumeTail(self.raw_resume, self.local, slotPtr(ResumeValue, &slot)),
 574             };
 575             return readSlot(HandlerResult, result);
 576         }
 577     };
 578 }
 579 
 580 /// Builds a handler description from an effect, an optional result function, and up to eight
 581 /// operations, so a raw caller builds its handler constants. The operations are copied in slice
 582 /// order, and the remaining slots hold `null_op`. Each operation's position in the slice must equal
 583 /// its tag's index, because `perform` picks the clause by that index. The function runs at compile
 584 /// time when its arguments are constants.
 585 pub fn handlerDef(effect: ?Effect, resultfun: ?ResultFn, operations: []const Operation) HandlerDef {
 586     return .{
 587         .effect = effect,
 588         .resultfun = resultfun,
 589         .operations = operationTable(operations),
 590     };
 591 }
 592 
 593 /// Returns an eight-slot table with `entries` in order and `null_op` in the remaining slots.
 594 /// `handlerDef` calls it to fill the table of each handler description it builds. More than eight
 595 /// entries fails an assertion in safe builds.
 596 pub fn operationTable(entries: []const Operation) [max_operations]Operation {
 597     assert(entries.len <= max_operations);
 598     var table = @as([max_operations]Operation, @splat(.{ .opkind = .null_op, .optag = null, .opfun = null }));
 599     for (entries, 0..) |entry, index| table[index] = entry;
 600     return table;
 601 }
 602 
 603 fn PerformFrame(comptime Arg: type, comptime Result: type) type {
 604     return struct {
 605         arg: Slot(Arg) = .{},
 606         result: Slot(Result) = .{},
 607     };
 608 }
 609 
 610 fn Slot(comptime T: type) type {
 611     return if (T == void) struct {} else struct {
 612         value: T = undefined,
 613     };
 614 }
 615 
 616 fn writeSlot(comptime T: type, slot: *Slot(T), value: T) void {
 617     if (comptime T != void) {
 618         slot.value = value;
 619     }
 620 }
 621 
 622 fn readSlotValue(comptime T: type, slot: *Slot(T)) T {
 623     if (comptime T != void) {
 624         return slot.value;
 625     }
 626     return {};
 627 }
 628 
 629 fn slotPtr(comptime T: type, slot: *Slot(T)) ?*anyopaque {
 630     if (comptime T != void) {
 631         return @ptrCast(slot);
 632     }
 633     return null;
 634 }
 635 
 636 fn readSlot(comptime T: type, ptr: ?*anyopaque) T {
 637     if (comptime T != void) {
 638         const slot: *Slot(T) = @ptrCast(@alignCast(ptr.?));
 639         return slot.value;
 640     }
 641     return {};
 642 }
 643 
 644 fn callTailHandler(handler: anytype, context: anytype, arg: anytype) handlerReturnType(@TypeOf(handler)) {
 645     const Arg = @TypeOf(arg);
 646     const count = comptime handlerParamCount(@TypeOf(handler));
 647     if (count == 0) {
 648         if (Arg != void) @compileError("handler without parameters requires a void operation argument");
 649         return handler();
 650     }
 651     if (count == 1) return handler(arg);
 652     if (count == 2) return handler(context, arg);
 653     @compileError("tail handlers must accept (), (arg), or (context, arg)");
 654 }
 655 
 656 fn callContinuationHandler(
 657     handler: anytype,
 658     continuation: anytype,
 659     context: anytype,
 660     arg: anytype,
 661 ) handlerReturnType(@TypeOf(handler)) {
 662     const Arg = @TypeOf(arg);
 663     const count = comptime handlerParamCount(@TypeOf(handler));
 664     if (count == 1) {
 665         if (Arg != void) @compileError("continuation-only handlers require a void operation argument");
 666         return handler(continuation);
 667     }
 668     if (count == 2) return handler(continuation, arg);
 669     if (count == 3) return handler(continuation, context, arg);
 670     @compileError("continuation handlers must accept (continuation), (continuation, arg), or (continuation, context, arg)");
 671 }
 672 
 673 fn handlerReturnType(comptime Handler: type) type {
 674     return handlerFnInfo(Handler).return_type orelse @compileError("generic effect handlers are not supported");
 675 }
 676 
 677 fn handlerParamCount(comptime Handler: type) usize {
 678     return handlerFnInfo(Handler).param_types.len;
 679 }
 680 
 681 fn handlerFnInfo(comptime Handler: type) std.builtin.Type.Fn {
 682     const Fn = switch (@typeInfo(Handler)) {
 683         .pointer => |ptr| ptr.child,
 684         .@"fn" => Handler,
 685         else => @compileError("effect handlers must be functions"),
 686     };
 687     return switch (@typeInfo(Fn)) {
 688         .@"fn" => |info| info,
 689         else => @compileError("effect handlers must be functions"),
 690     };
 691 }
 692 
 693 fn requirePointer(comptime T: type, comptime name: []const u8) void {
 694     switch (@typeInfo(T)) {
 695         .pointer => {},
 696         else => @compileError(name ++ " must be a pointer"),
 697     }
 698 }
 699 
 700 fn hasStructField(comptime T: type, comptime name: []const u8) bool {
 701     const info = switch (@typeInfo(T)) {
 702         .@"struct" => |struct_info| struct_info,
 703         else => return false,
 704     };
 705     inline for (info.field_names) |field_name| {
 706         if (std.mem.eql(u8, field_name, name)) return true;
 707     }
 708     return false;
 709 }
 710 
 711 const Frame = extern struct {
 712     effect: ?Effect,
 713     parent: ?*Frame,
 714 };
 715 
 716 const HandleFrame = struct {
 717     frame: Frame,
 718     prompt: *mp.Prompt,
 719     hdef: *const HandlerDef,
 720     local: ?*anyopaque,
 721 };
 722 
 723 const UnderFrame = struct {
 724     frame: Frame,
 725     under: ?Effect,
 726 };
 727 
 728 const MaskFrame = struct {
 729     frame: Frame,
 730     mask: ?Effect,
 731     from: usize,
 732 };
 733 
 734 const FinallyFrame = struct {
 735     frame: Frame,
 736     fun: ReleaseFn,
 737     local: ?*anyopaque,
 738 };
 739 
 740 /// Lists the four kinds of effect resumption as C `int` values. The runtime records in each
 741 /// resumption where it lives and how it resumes, so one set of resume functions serves every kind
 742 /// of clause.
 743 pub const ResumptionKind = enum(c_int) {
 744     /// The clause runs on the body's stack, and the resumption points at the handler's local state.
 745     /// Resuming it stores the new local state and returns the value. `tail_noop` and `tail` clauses
 746     /// get this kind.
 747     inplace,
 748     /// The resumption lives in a stack frame of the clause and wraps the suspended prompt's
 749     /// one-shot handle.
 750     scoped_once,
 751     /// The resumption is allocated and wraps the suspended prompt's one-shot handle. Its final
 752     /// resume frees it.
 753     once,
 754     /// The resumption is allocated and wraps the suspended prompt's multi-shot handle. Each resume
 755     /// before the final resume adds a reference first.
 756     multi,
 757 };
 758 
 759 const ResumePayload = extern union {
 760     plocal: *?*anyopaque,
 761     continuation: *mp.Resume,
 762 };
 763 
 764 /// Holds an effect resumption as a kind and a payload. A raw clause receives a pointer to one and
 765 /// passes it to `resumeEffect`, `resumeFinal`, `resumeTail` or `resumeRelease`.
 766 pub const Resume = extern struct {
 767     /// Records which of the four kinds this resumption is.
 768     kind: ResumptionKind,
 769     /// Holds a pointer to the handler's local state for an `inplace` resumption. For the other
 770     /// kinds, the payload holds the prompt resumption for the suspended body.
 771     payload: ResumePayload,
 772 };
 773 
 774 const PerformEnv = extern struct {
 775     rkind: ResumptionKind,
 776     opfun: OpFn,
 777     local: ?*anyopaque,
 778     oparg: ?*anyopaque,
 779 };
 780 
 781 const ResumeEnv = extern struct {
 782     local: ?*anyopaque,
 783     result: ?*anyopaque,
 784     unwind: bool,
 785 };
 786 
 787 const HandleStartEnv = extern struct {
 788     hdef: *const HandlerDef,
 789     local: ?*anyopaque,
 790     body: ActionFn,
 791     arg: ?*anyopaque,
 792 };
 793 
 794 const under_names = [_:null]?[*:0]const u8{ "mpe_frame_under", null };
 795 const mask_names = [_:null]?[*:0]const u8{ "mpe_frame_mask", null };
 796 const finally_names = [_:null]?[*:0]const u8{ "mpe_frame_finally", null };
 797 const unwind_names = [_:null]?[*:0]const u8{ "mpe_unwind", "mpe_unwind/mpe_unwind", null };
 798 
 799 const under_effect: Effect = @ptrCast(&under_names);
 800 const mask_effect: Effect = @ptrCast(&mask_names);
 801 const finally_effect: Effect = @ptrCast(&finally_names);
 802 const unwind_effect: Effect = @ptrCast(&unwind_names);
 803 const unwind_optag: Optag = .{ .effect = unwind_effect, .opidx = 0 };
 804 
 805 threadlocal var frame_top: ?*Frame = null;
 806 threadlocal var find_cache: ?*HandleFrame = null;
 807 
 808 /// Returns the effect's name, the first string of its name table, for a diagnostic to print the
 809 /// name of an effect. The call returns `<null>` for a null effect or a null first entry.
 810 pub fn effectName(effect: ?Effect) [*:0]const u8 {
 811     const names = effect orelse return "<null>";
 812     return names[0] orelse "<null>";
 813 }
 814 
 815 /// Returns the string at position index plus one in the operation's effect table, for a diagnostic
 816 /// to print the name of an operation as the unhandled-operation message does. The call returns
 817 /// `<null>` for a null tag, a null effect or a null entry.
 818 pub fn optagName(optag: ?*const Optag) [*:0]const u8 {
 819     const tag = optag orelse return "<null>";
 820     const names = tag.effect orelse return "<null>";
 821     const index: usize = @intCast(tag.opidx + 1);
 822     return names[index] orelse "<null>";
 823 }
 824 
 825 /// The typed `handle` and raw callers run a body under a handler table with this function. The call
 826 /// creates a prompt with a new stacklet, pushes a handler frame for `hdef` with local state
 827 /// `local`, and runs `body(arg)` there. The call returns the body's result, passed through the
 828 /// handler's result function when it has one. When an `abort` or `never` clause ends the body, the
 829 /// call returns that clause's result. `hdef` must stay valid until the call returns. Handler frames
 830 /// live in a per-thread list, so a handler answers operations performed on its own thread.
 831 pub fn handleRaw(hdef: *const HandlerDef, local: ?*anyopaque, body: ActionFn, arg: ?*anyopaque) ?*anyopaque {
 832     var env: HandleStartEnv = .{
 833         .hdef = hdef,
 834         .local = local,
 835         .body = body,
 836         .arg = arg,
 837     };
 838     return mp.prompt(handleStart, &env);
 839 }
 840 
 841 /// Raw callers use this name. The function does exactly what `handleRaw` does.
 842 pub fn handle(hdef: *const HandlerDef, local: ?*anyopaque, body: ActionFn, arg: ?*anyopaque) ?*anyopaque {
 843     return handleRaw(hdef, local, body, arg);
 844 }
 845 
 846 /// The typed `perform` and raw code call this function to perform an operation. The call finds the
 847 /// innermost handler for the tag's effect and runs its clause at the tag's index, in the way the
 848 /// clause's kind requires. The search walks the thread's frames outward and passes over one more
 849 /// handler of the operation's effect for each mask of that effect that applies. Inside a `tail`
 850 /// clause, the search jumps from the clause's frame to the frames outside the clause's own handler.
 851 /// The last handler found is kept and reused for the same effect until a frame is pushed or popped.
 852 /// The call returns the clause's result for kinds that run in place, and the value a resume passed
 853 /// for kinds that suspend the body. With no handler for the effect, the call prints
 854 /// `lib/mpeff: unhandled operation:` and the operation's name to standard error and returns null.
 855 pub fn performRaw(optag: *const Optag, arg: ?*anyopaque) ?*anyopaque {
 856     const h = find(optag) orelse {
 857         @branchHint(.unlikely);
 858         return unhandledOperation(optag);
 859     };
 860     const op_index: usize = @intCast(optag.opidx);
 861     const op = &h.hdef.operations[op_index];
 862     return performAt(h, op, arg);
 863 }
 864 
 865 /// Raw callers use this name. The function does exactly what `performRaw` does.
 866 pub fn perform(optag: *const Optag, arg: ?*anyopaque) ?*anyopaque {
 867     return performRaw(optag, arg);
 868 }
 869 
 870 /// Resumes the body: its `perform` returns `arg`, and `local` becomes the handler's new local
 871 /// state. A clause that resumes the body more than once, as one that tries each branch of a choice
 872 /// does, calls it for every resume but the last. When the body finishes and the handler has no
 873 /// result function, the call returns the body's result. When the body finishes and the handler has
 874 /// a result function, the call returns the value that the function puts in place of the body's
 875 /// result. When a later clause suspends the body and returns, the call returns that clause's
 876 /// result. For a `multi` resumption, the call adds a reference first, so the resumption stays
 877 /// usable. For a `scoped_once` resumption, this call is the one resume. An `inplace` or `once`
 878 /// resumption fails an assertion in safe builds.
 879 pub fn resumeEffect(resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque) ?*anyopaque {
 880     return resumeInternal(false, resume_ptr, local, arg, false);
 881 }
 882 
 883 /// Resumes the body the way `resumeEffect` does, as the resumption's last resume. A clause ends
 884 /// every allocated resumption with this function, `resumeTail` or `resumeRelease`, and makes its
 885 /// last resume of a `multi` resumption with it. For `once` and `multi` resumptions, the call frees
 886 /// the allocated record first, and the caller never uses the resumption again.
 887 pub fn resumeFinal(resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque) ?*anyopaque {
 888     return resumeInternal(true, resume_ptr, local, arg, false);
 889 }
 890 
 891 /// Resumes the body for a clause whose last act is to resume, such as a state or reader clause, so
 892 /// no clause frame stays under the resumed body. For an `inplace` resumption, the call stores
 893 /// `local` as the handler's local state and returns `arg`, which the clause returns to `perform`.
 894 /// For the other kinds, the call resumes the body as a tail resume, so control does not come back
 895 /// to the clause. The call frees an allocated resumption first. The call must be the clause's last
 896 /// action, with its value returned.
 897 pub fn resumeTail(resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque) ?*anyopaque {
 898     if (resume_ptr.kind == .inplace) {
 899         @branchHint(.likely);
 900         resume_ptr.payload.plocal.* = local;
 901         return arg;
 902     }
 903 
 904     var renv: ResumeEnv = .{
 905         .local = local,
 906         .result = arg,
 907         .unwind = false,
 908     };
 909 
 910     if (resume_ptr.kind == .scoped_once) {
 911         @branchHint(.likely);
 912         return mp.resumeTailPrompt(resume_ptr.payload.continuation, &renv);
 913     }
 914 
 915     const mpr = resume_ptr.payload.continuation;
 916     mp.processAllocator().destroy(resume_ptr);
 917     return mp.resumeTailPrompt(mpr, &renv);
 918 }
 919 
 920 /// Ends an effect resumption for a clause that will not continue the body, such as a failing branch
 921 /// of a choice. The call does nothing for null, which an `abort` clause receives. For a `once`
 922 /// resumption, and for a `multi` resumption that holds the last reference and never resumed, the
 923 /// call resumes the body with an unwind flag. The unwind makes the body's `perform` jump back to
 924 /// the handler, and the runtime drops the body's stacklet without running the rest of the body. For
 925 /// a `multi` resumption with other references or earlier resumes, the call frees the record and
 926 /// gives up one reference. A `scoped_once` or `inplace` resumption fails an assertion in safe
 927 /// builds. A `finally` call inside the dropped body never calls its `finally_fun` function.
 928 pub fn resumeRelease(resume_ptr: ?*Resume) void {
 929     const r = resume_ptr orelse return;
 930     if (r.kind == .once) {
 931         resumeUnwind(r);
 932         return;
 933     }
 934 
 935     assert(r.kind == .multi);
 936     const mpr = r.payload.continuation;
 937     if (mp.resumeShouldUnwind(mpr) != 0) {
 938         resumeUnwind(r);
 939     } else {
 940         mp.processAllocator().destroy(r);
 941         mp.resumeDrop(mpr);
 942     }
 943 }
 944 
 945 /// Runs `fun(arg)` under a frame for `effect`, for code that must reach an outer handler of an
 946 /// effect past the innermost one. While `fun` runs, a search for a handler of `effect` passes over
 947 /// one more handler of that effect. The frame applies only when the search has at least `from`
 948 /// handlers of that effect still to pass over when it reaches the frame, so a `from` of 0 always
 949 /// applies. The call returns what `fun` returns, and pops the frame on return.
 950 pub fn mask(effect: ?Effect, from: usize, fun: ActionFn, arg: ?*anyopaque) ?*anyopaque {
 951     var f: MaskFrame = .{
 952         .frame = .{ .effect = mask_effect, .parent = null },
 953         .mask = effect,
 954         .from = from,
 955     };
 956     pushFrame(&f.frame);
 957     defer popFrame(&f.frame);
 958     return fun(arg);
 959 }
 960 
 961 /// Runs `fun(arg)`, then calls `finally_fun(local)`, and returns `fun`'s result, so code can run a
 962 /// second function after its body returns. `finally_fun` runs only when `fun` returns normally, so
 963 /// a body ended by an `abort` or `never` clause, or by `resumeRelease`, skips it.
 964 pub fn finally(local: ?*anyopaque, finally_fun: ReleaseFn, fun: ActionFn, arg: ?*anyopaque) ?*anyopaque {
 965     var f: FinallyFrame = .{
 966         .frame = .{ .effect = finally_effect, .parent = null },
 967         .fun = finally_fun,
 968         .local = local,
 969     };
 970     pushFrame(&f.frame);
 971     const result = fun(arg);
 972     popFrame(&f.frame);
 973     f.fun(f.local);
 974     return result;
 975 }
 976 
 977 fn pushFrame(f: *Frame) void {
 978     f.parent = frame_top;
 979     assert(f.parent != f);
 980     frame_top = f;
 981     clearFindCache();
 982 }
 983 
 984 fn popFrame(f: *Frame) void {
 985     assert(frame_top == f);
 986     frame_top = f.parent;
 987     clearFindCache();
 988 }
 989 
 990 fn clearFindCache() void {
 991     find_cache = null;
 992 }
 993 
 994 fn handleStart(prompt: *mp.Prompt, earg: ?*anyopaque) callconv(.c) ?*anyopaque {
 995     const env: *HandleStartEnv = @ptrCast(@alignCast(earg.?));
 996     var h: HandleFrame = .{
 997         .frame = .{ .effect = env.hdef.effect, .parent = null },
 998         .prompt = prompt,
 999         .hdef = env.hdef,
1000         .local = env.local,
1001     };
1002 
1003     pushFrame(&h.frame);
1004     var result = env.body(env.arg);
1005     popFrame(&h.frame);
1006 
1007     if (h.hdef.resultfun) |resultfun| {
1008         result = resultfun(h.local, result);
1009     }
1010     return result;
1011 }
1012 
1013 fn find(optag: *const Optag) ?*HandleFrame {
1014     var f = frame_top;
1015     const operation_effect = optag.effect;
1016     var mask_level: usize = 0;
1017 
1018     if (find_cache) |cached| {
1019         if (cached.frame.effect == operation_effect) {
1020             @branchHint(.likely);
1021             return cached;
1022         }
1023     }
1024 
1025     while (f) |frame| {
1026         @branchHint(.likely);
1027         const eff = frame.effect;
1028         if (eff == operation_effect) {
1029             @branchHint(.likely);
1030             if (mask_level == 0) return cacheFind(@fieldParentPtr("frame", frame));
1031             mask_level -= 1;
1032         } else if (eff == under_effect) {
1033             @branchHint(.unlikely);
1034             const under: *UnderFrame = @fieldParentPtr("frame", frame);
1035             var cursor = frame.parent;
1036             while (cursor) |candidate| {
1037                 if (candidate.effect == under.under) break;
1038                 cursor = candidate.parent;
1039             }
1040             f = cursor orelse return null;
1041         } else if (eff == mask_effect) {
1042             @branchHint(.unlikely);
1043             const masked: *MaskFrame = @fieldParentPtr("frame", frame);
1044             if (masked.mask == operation_effect and masked.from <= mask_level) {
1045                 mask_level += 1;
1046             }
1047         }
1048         f = f.?.parent;
1049     }
1050 
1051     return null;
1052 }
1053 
1054 fn cacheFind(h: *HandleFrame) *HandleFrame {
1055     find_cache = h;
1056     return h;
1057 }
1058 
1059 fn performAt(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque {
1060     if (op.opkind == .tail_noop) {
1061         @branchHint(.likely);
1062         var r: Resume = .{
1063             .kind = .inplace,
1064             .payload = .{ .plocal = &h.local },
1065         };
1066         return op.opfun.?(&r, h.local, arg);
1067     }
1068 
1069     if (op.opkind == .tail) {
1070         @branchHint(.likely);
1071         return performUnder(h, op, arg);
1072     }
1073 
1074     if (op.opkind == .scoped_once) return performYieldTo(.scoped_once, h, op, arg);
1075     if (op.opkind == .once) return performYieldTo(.once, h, op, arg);
1076     if (op.opkind == .never) return unwindTo(h, op, arg);
1077     if (op.opkind == .abort) return performYieldToAbort(h, op, arg);
1078     if (op.opkind == .forward) return performForward(h, op, arg);
1079     if (op.opkind == .null_op) return null;
1080     return performYieldTo(.multi, h, op, arg);
1081 }
1082 
1083 fn performForward(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque {
1084     const optag = op.optag orelse return null;
1085     var f: MaskFrame = .{
1086         .frame = .{ .effect = mask_effect, .parent = null },
1087         .mask = h.frame.effect,
1088         .from = 0,
1089     };
1090     pushFrame(&f.frame);
1091     defer popFrame(&f.frame);
1092     return perform(optag, arg);
1093 }
1094 
1095 fn performUnder(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque {
1096     const saved_find_cache = find_cache;
1097     var f: UnderFrame = .{
1098         .frame = .{ .effect = under_effect, .parent = null },
1099         .under = h.frame.effect,
1100     };
1101     f.frame.parent = frame_top;
1102     assert(f.frame.parent != &f.frame);
1103     frame_top = &f.frame;
1104     clearFindCache();
1105     defer {
1106         assert(frame_top == &f.frame);
1107         frame_top = f.frame.parent;
1108         find_cache = saved_find_cache;
1109     }
1110 
1111     var r: Resume = .{
1112         .kind = .inplace,
1113         .payload = .{ .plocal = &h.local },
1114     };
1115     return op.opfun.?(&r, h.local, arg);
1116 }
1117 
1118 fn performYieldTo(rkind: ResumptionKind, h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque {
1119     const resume_top = frame_top;
1120     frame_top = h.frame.parent;
1121     clearFindCache();
1122     var penv: PerformEnv = .{
1123         .rkind = rkind,
1124         .opfun = op.opfun.?,
1125         .local = h.local,
1126         .oparg = arg,
1127     };
1128 
1129     const result = mp.yieldPrompt(h.prompt, performOpClause, &penv);
1130     const renv: *ResumeEnv = @ptrCast(@alignCast(result.?));
1131     h.local = renv.local;
1132     assert(frame_top != &h.frame);
1133     h.frame.parent = frame_top;
1134     frame_top = resume_top;
1135     clearFindCache();
1136 
1137     if (renv.unwind) return unwindTo(h, &unwind_operation, renv.result);
1138     return renv.result;
1139 }
1140 
1141 fn performOpClause(mpr: *mp.Resume, earg: ?*anyopaque) callconv(.c) ?*anyopaque {
1142     const env: *PerformEnv = @ptrCast(@alignCast(earg.?));
1143     var stack_resume: Resume = undefined;
1144     const r = if (env.rkind == .scoped_once) blk: {
1145         @branchHint(.likely);
1146         break :blk &stack_resume;
1147     } else allocateResume();
1148 
1149     r.kind = env.rkind;
1150     r.payload.continuation = if (env.rkind == .multi) mp.resumeMulti(mpr) else mpr;
1151     return env.opfun(r, env.local, env.oparg);
1152 }
1153 
1154 fn performYieldToAbort(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque {
1155     frame_top = h.frame.parent;
1156     clearFindCache();
1157     var env: PerformEnv = .{
1158         .rkind = .scoped_once,
1159         .opfun = op.opfun.?,
1160         .local = h.local,
1161         .oparg = arg,
1162     };
1163     return mp.yieldPrompt(h.prompt, performOpClauseAbort, &env);
1164 }
1165 
1166 fn performOpClauseAbort(mpr: *mp.Resume, earg: ?*anyopaque) callconv(.c) ?*anyopaque {
1167     const env: PerformEnv = (@as(*PerformEnv, @ptrCast(@alignCast(earg.?)))).*;
1168     mp.resumeDrop(mpr);
1169     return env.opfun(null, env.local, env.oparg);
1170 }
1171 
1172 fn unwindTo(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque {
1173     return performYieldToAbort(h, op, arg);
1174 }
1175 
1176 fn handleOpUnwind(_: ?*Resume, _: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1177     return arg;
1178 }
1179 
1180 const unwind_operation: Operation = .{
1181     .opkind = .abort,
1182     .optag = &unwind_optag,
1183     .opfun = handleOpUnwind,
1184 };
1185 
1186 fn resumeInternal(final: bool, resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque, unwind: bool) ?*anyopaque {
1187     assert(@backingInt(resume_ptr.kind) >= @backingInt(ResumptionKind.scoped_once));
1188     var renv: ResumeEnv = .{
1189         .local = local,
1190         .result = arg,
1191         .unwind = unwind,
1192     };
1193 
1194     switch (resume_ptr.kind) {
1195         .scoped_once => {
1196             @branchHint(.likely);
1197             return mp.resumePrompt(resume_ptr.payload.continuation, &renv);
1198         },
1199         .once => {
1200             assert(final);
1201             const mpr = resume_ptr.payload.continuation;
1202             mp.processAllocator().destroy(resume_ptr);
1203             return mp.resumePrompt(mpr, &renv);
1204         },
1205         .multi => {
1206             const mpr = resume_ptr.payload.continuation;
1207             if (final) {
1208                 mp.processAllocator().destroy(resume_ptr);
1209             } else {
1210                 _ = mp.resumeDup(mpr);
1211             }
1212             return mp.resumePrompt(mpr, &renv);
1213         },
1214         .inplace => unreachable,
1215     }
1216 }
1217 
1218 fn resumeUnwind(resume_ptr: *Resume) void {
1219     _ = resumeInternal(true, resume_ptr, null, null, true);
1220 }
1221 
1222 fn allocateResume() *Resume {
1223     return mp.processAllocator().create(Resume) catch std.debug.panic("lib/mpeff: out of memory", .{});
1224 }
1225 
1226 fn unhandledOperation(optag: *const Optag) ?*anyopaque {
1227     @branchHint(.cold);
1228     pretty.diagnostic.writeStderrText("lib/mpeff: unhandled operation: {s}\n", .{optagName(optag)});
1229     return null;
1230 }
1231 
1232 fn intToPtr(value: isize) ?*anyopaque {
1233     if (value == 0) return null;
1234     return @ptrFromInt(@as(usize, @intCast(value)));
1235 }
1236 
1237 fn ptrToInt(value: ?*anyopaque) isize {
1238     return if (value) |ptr| @intCast(@intFromPtr(ptr)) else 0;
1239 }
1240 
1241 const reader_names = [_:null]?[*:0]const u8{ "reader", "reader/ask", null };
1242 const reader_effect: effect_mod.Effect = @ptrCast(&reader_names);
1243 const reader_ask_tag: effect_mod.Optag = .{ .effect = reader_effect, .opidx = 0 };
1244 
1245 fn readerAsk() isize {
1246     return ptrToInt(effect_mod.perform(&reader_ask_tag, null));
1247 }
1248 
1249 fn stackUse(kb: usize) isize {
1250     if (kb <= 4) return readerAsk();
1251 
1252     var page: [4096]u8 = undefined;
1253     page[4095] = @truncate(kb);
1254     std.mem.doNotOptimizeAway(&page);
1255     return stackUse(kb - 4);
1256 }
1257 
1258 fn handleReaderAsk(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1259     return effect_mod.resumeTail(continuation.?, local, local);
1260 }
1261 
1262 fn handleGeneralReaderAsk(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1263     _ = local;
1264     return effect_mod.resumeTail(continuation.?, intToPtr(42), intToPtr(42));
1265 }
1266 
1267 const reader_def = effect_mod.handlerDef(reader_effect, null, &.{
1268     .{ .opkind = .tail_noop, .optag = &reader_ask_tag, .opfun = handleReaderAsk },
1269 });
1270 
1271 const general_reader_def = effect_mod.handlerDef(reader_effect, null, &.{
1272     .{ .opkind = .scoped_once, .optag = &reader_ask_tag, .opfun = handleGeneralReaderAsk },
1273 });
1274 
1275 const forward_reader_def = effect_mod.handlerDef(reader_effect, null, &.{
1276     .{ .opkind = .forward, .optag = &reader_ask_tag, .opfun = null },
1277 });
1278 
1279 fn handleUnderReaderAsk(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1280     return effect_mod.resumeTail(continuation.?, local, intToPtr(readerAsk() + 1));
1281 }
1282 
1283 const under_reader_def = effect_mod.handlerDef(reader_effect, null, &.{
1284     .{ .opkind = .tail, .optag = &reader_ask_tag, .opfun = handleUnderReaderAsk },
1285 });
1286 
1287 fn readerHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque {
1288     return effect_mod.handle(&reader_def, intToPtr(init), action, arg);
1289 }
1290 
1291 fn generalReaderHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque {
1292     return effect_mod.handle(&general_reader_def, intToPtr(init), action, arg);
1293 }
1294 
1295 fn forwardingReaderHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque {
1296     return effect_mod.handle(&forward_reader_def, intToPtr(init), action, arg);
1297 }
1298 
1299 fn underReaderHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque {
1300     return effect_mod.handle(&under_reader_def, intToPtr(init), action, arg);
1301 }
1302 
1303 fn readerAction(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1304     return intToPtr(stackUse(64) + readerAsk());
1305 }
1306 
1307 test "reader handles tail and scoped-once ask operations" {
1308     try std.testing.expectEqual(@as(isize, 84), ptrToInt(readerHandle(readerAction, 42, null)));
1309     try std.testing.expectEqual(@as(isize, 84), ptrToInt(generalReaderHandle(readerAction, 99, null)));
1310 }
1311 
1312 fn askOnce(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1313     return intToPtr(readerAsk());
1314 }
1315 
1316 fn innerForwardingReader(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1317     return forwardingReaderHandle(askOnce, 99, arg);
1318 }
1319 
1320 test "forward operation skips the current handler and reaches the enclosing handler" {
1321     try std.testing.expectEqual(@as(isize, 7), ptrToInt(readerHandle(innerForwardingReader, 7, null)));
1322 }
1323 
1324 fn innerUnderReader(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1325     return underReaderHandle(askOnce, 99, arg);
1326 }
1327 
1328 test "tail operation runs under-frame effects below the current handler" {
1329     try std.testing.expectEqual(@as(isize, 8), ptrToInt(readerHandle(innerUnderReader, 7, null)));
1330 }
1331 
1332 const state_names = [_:null]?[*:0]const u8{ "state", "state/get", "state/set", null };
1333 const state_effect: effect_mod.Effect = @ptrCast(&state_names);
1334 const state_get_tag: effect_mod.Optag = .{ .effect = state_effect, .opidx = 0 };
1335 const state_set_tag: effect_mod.Optag = .{ .effect = state_effect, .opidx = 1 };
1336 
1337 fn rawStateGet() isize {
1338     return ptrToInt(effect_mod.perform(&state_get_tag, null));
1339 }
1340 
1341 fn rawStateSet(value: isize) void {
1342     _ = effect_mod.perform(&state_set_tag, intToPtr(value));
1343 }
1344 
1345 fn handleStateGet(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1346     return effect_mod.resumeTail(continuation.?, local, local);
1347 }
1348 
1349 fn handleStateSet(continuation: ?*effect_mod.Resume, _: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1350     return effect_mod.resumeTail(continuation.?, arg, null);
1351 }
1352 
1353 fn stateDef(comptime kind: effect_mod.OperationKind) effect_mod.HandlerDef {
1354     return effect_mod.handlerDef(state_effect, null, &.{
1355         .{ .opkind = kind, .optag = &state_get_tag, .opfun = handleStateGet },
1356         .{ .opkind = kind, .optag = &state_set_tag, .opfun = handleStateSet },
1357     });
1358 }
1359 
1360 const tail_state_def = stateDef(.tail_noop);
1361 const under_state_def = stateDef(.tail);
1362 const once_state_def = stateDef(.scoped_once);
1363 const allocated_once_state_def = stateDef(.once);
1364 const multi_state_def = stateDef(.multi);
1365 
1366 fn stateHandle(def: *const effect_mod.HandlerDef, action: effect_mod.ActionFn, init: isize) ?*anyopaque {
1367     return effect_mod.handle(def, intToPtr(init), action, null);
1368 }
1369 
1370 fn counterAction(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1371     var count: isize = 0;
1372     while (true) {
1373         const current = rawStateGet();
1374         if (current <= 0) break;
1375         rawStateSet(current - 1);
1376         count += 1;
1377     }
1378     return intToPtr(count);
1379 }
1380 
1381 test "state counter works across tail, under, scoped-once, and multi handlers" {
1382     try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&tail_state_def, counterAction, 1000)));
1383     try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&under_state_def, counterAction, 1000)));
1384     try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&once_state_def, counterAction, 1000)));
1385     try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&allocated_once_state_def, counterAction, 1000)));
1386     try std.testing.expectEqual(@as(isize, 100), ptrToInt(stateHandle(&multi_state_def, counterAction, 100)));
1387 }
1388 
1389 test "state counter covers upstream debug workload sizes" {
1390     const count: isize = 100_100;
1391     try std.testing.expectEqual(count, ptrToInt(stateHandle(&tail_state_def, counterAction, count)));
1392     try std.testing.expectEqual(count, ptrToInt(stateHandle(&under_state_def, counterAction, count)));
1393     try std.testing.expectEqual(count, ptrToInt(stateHandle(&once_state_def, counterAction, count)));
1394     try std.testing.expectEqual(@divTrunc(count, 10), ptrToInt(stateHandle(&multi_state_def, counterAction, @divTrunc(count, 10))));
1395 }
1396 
1397 fn reader1(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1398     return readerHandle(counterAction, 1, arg);
1399 }
1400 
1401 fn reader2(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1402     return readerHandle(reader1, 2, arg);
1403 }
1404 
1405 fn reader3(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1406     return readerHandle(reader2, 3, arg);
1407 }
1408 
1409 fn reader4(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1410     return readerHandle(reader3, 4, arg);
1411 }
1412 
1413 fn reader5(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1414     return readerHandle(reader4, 5, arg);
1415 }
1416 
1417 fn reader6(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1418     return readerHandle(reader5, 6, arg);
1419 }
1420 
1421 fn reader7(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1422     return readerHandle(reader6, 7, arg);
1423 }
1424 
1425 fn reader8(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1426     return readerHandle(reader7, 8, arg);
1427 }
1428 
1429 fn reader9(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1430     return readerHandle(reader8, 9, arg);
1431 }
1432 
1433 fn reader10(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1434     return readerHandle(reader9, 10, arg);
1435 }
1436 
1437 test "state handler composes under nested reader handlers" {
1438     try std.testing.expectEqual(@as(isize, 250), ptrToInt(stateHandle(&tail_state_def, reader3, 250)));
1439     try std.testing.expectEqual(@as(isize, 250), ptrToInt(stateHandle(&once_state_def, reader3, 250)));
1440 }
1441 
1442 test "state handler composes under ten nested reader handlers" {
1443     try std.testing.expectEqual(@as(isize, 100), ptrToInt(stateHandle(&tail_state_def, reader10, 100)));
1444     try std.testing.expectEqual(@as(isize, 100), ptrToInt(stateHandle(&once_state_def, reader10, 100)));
1445 }
1446 
1447 fn finallyBody(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1448     return arg;
1449 }
1450 
1451 fn markReleased(local: ?*anyopaque) callconv(.c) void {
1452     const released: *bool = @ptrCast(@alignCast(local.?));
1453     released.* = true;
1454 }
1455 
1456 test "finally frame runs release function after normal return" {
1457     var released = false;
1458     try std.testing.expectEqual(@as(isize, 42), ptrToInt(effect_mod.finally(&released, markReleased, finallyBody, intToPtr(42))));
1459     try std.testing.expect(released);
1460 }
1461 
1462 const StateFn = struct {
1463     env: ?*anyopaque,
1464     applyFn: *const fn (?*anyopaque, isize) isize,
1465 
1466     fn apply(self: StateFn, state: isize) isize {
1467         return self.applyFn(self.env, state);
1468     }
1469 };
1470 
1471 fn stateFnToPtr(function: StateFn) ?*anyopaque {
1472     const box = std.testing.allocator.create(StateFn) catch @panic("unable to allocate state function");
1473     box.* = function;
1474     return @ptrCast(box);
1475 }
1476 
1477 fn stateFnFromPtr(value: ?*anyopaque) StateFn {
1478     const box: *StateFn = @ptrCast(@alignCast(value.?));
1479     const function = box.*;
1480     std.testing.allocator.destroy(box);
1481     return function;
1482 }
1483 
1484 fn mstateResultValue(env: ?*anyopaque, _: isize) isize {
1485     return ptrToInt(env);
1486 }
1487 
1488 fn handleMstateResult(_: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1489     return stateFnToPtr(.{ .env = arg, .applyFn = mstateResultValue });
1490 }
1491 
1492 fn mstateGetValue(env: ?*anyopaque, state: isize) isize {
1493     const continuation: *effect_mod.Resume = @ptrCast(@alignCast(env.?));
1494     const function = stateFnFromPtr(effect_mod.resumeFinal(continuation, null, intToPtr(state)));
1495     return function.apply(state);
1496 }
1497 
1498 fn handleMstateGet(continuation: ?*effect_mod.Resume, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1499     return stateFnToPtr(.{
1500         .env = @ptrCast(continuation.?),
1501         .applyFn = mstateGetValue,
1502     });
1503 }
1504 
1505 const PutEnv = struct {
1506     new_state: isize,
1507     continuation: *effect_mod.Resume,
1508 };
1509 
1510 fn mstatePutValue(env_ptr: ?*anyopaque, _: isize) isize {
1511     const env: *PutEnv = @ptrCast(@alignCast(env_ptr.?));
1512     const new_state = env.new_state;
1513     const continuation = env.continuation;
1514     std.testing.allocator.destroy(env);
1515 
1516     const function = stateFnFromPtr(effect_mod.resumeFinal(continuation, null, null));
1517     return function.apply(new_state);
1518 }
1519 
1520 fn handleMstateSet(continuation: ?*effect_mod.Resume, _: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1521     const env = std.testing.allocator.create(PutEnv) catch @panic("unable to allocate monadic state closure");
1522     env.* = .{
1523         .new_state = ptrToInt(arg),
1524         .continuation = continuation.?,
1525     };
1526     return stateFnToPtr(.{
1527         .env = @ptrCast(env),
1528         .applyFn = mstatePutValue,
1529     });
1530 }
1531 
1532 const mstate_def = effect_mod.handlerDef(state_effect, handleMstateResult, &.{
1533     .{ .opkind = .once, .optag = &state_get_tag, .opfun = handleMstateGet },
1534     .{ .opkind = .once, .optag = &state_set_tag, .opfun = handleMstateSet },
1535 });
1536 
1537 fn mstateHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque {
1538     const function = stateFnFromPtr(effect_mod.handle(&mstate_def, null, action, arg));
1539     return intToPtr(function.apply(init));
1540 }
1541 
1542 test "monadic state handler defers state threading through once resumptions" {
1543     try std.testing.expectEqual(@as(isize, 200), ptrToInt(mstateHandle(counterAction, 200, null)));
1544 }
1545 
1546 const exit_names = [_:null]?[*:0]const u8{ "exit", "exit/capture", null };
1547 const exit_effect: effect_mod.Effect = @ptrCast(&exit_names);
1548 const exit_capture_tag: effect_mod.Optag = .{ .effect = exit_effect, .opidx = 0 };
1549 
1550 fn exitCapture() ?*anyopaque {
1551     return effect_mod.perform(&exit_capture_tag, null);
1552 }
1553 
1554 fn handleExitCapture(continuation: ?*effect_mod.Resume, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1555     return @ptrCast(continuation.?);
1556 }
1557 
1558 const exit_def = effect_mod.handlerDef(exit_effect, null, &.{
1559     .{ .opkind = .once, .optag = &exit_capture_tag, .opfun = handleExitCapture },
1560 });
1561 
1562 fn exitHandle(action: effect_mod.ActionFn, arg: ?*anyopaque) ?*anyopaque {
1563     return effect_mod.handle(&exit_def, null, action, arg);
1564 }
1565 
1566 fn rehandleBody(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1567     const first = readerAsk();
1568     _ = exitCapture();
1569     const second = readerAsk();
1570     return intToPtr(first + second);
1571 }
1572 
1573 fn withExitHandle(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1574     return exitHandle(rehandleBody, arg);
1575 }
1576 
1577 fn withResume(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1578     const continuation: *effect_mod.Resume = @ptrCast(@alignCast(arg.?));
1579     return effect_mod.resumeFinal(continuation, null, null);
1580 }
1581 
1582 test "captured effect continuation can be resumed under a different handler" {
1583     const captured = readerHandle(withExitHandle, 1, null);
1584     try std.testing.expectEqual(@as(isize, 3), ptrToInt(readerHandle(withResume, 2, captured)));
1585 }
1586 
1587 const amb_names = [_:null]?[*:0]const u8{ "amb", "amb/flip", null };
1588 const amb_effect: effect_mod.Effect = @ptrCast(&amb_names);
1589 const amb_flip_tag: effect_mod.Optag = .{ .effect = amb_effect, .opidx = 0 };
1590 
1591 fn ambFlip() bool {
1592     return ptrToInt(effect_mod.perform(&amb_flip_tag, null)) != 0;
1593 }
1594 
1595 fn rawAmbBody(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1596     return intToPtr(if (ambFlip()) 10 else 1);
1597 }
1598 
1599 fn handleAmbFlip(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1600     const false_branch = ptrToInt(effect_mod.resumeEffect(continuation.?, local, intToPtr(0)));
1601     const true_branch = ptrToInt(effect_mod.resumeFinal(continuation.?, local, intToPtr(1)));
1602     return intToPtr(false_branch + true_branch);
1603 }
1604 
1605 const amb_def = effect_mod.handlerDef(amb_effect, null, &.{
1606     .{ .opkind = .scoped, .optag = &amb_flip_tag, .opfun = handleAmbFlip },
1607 });
1608 
1609 test "scoped multi-shot handler can resume both branches" {
1610     try std.testing.expectEqual(@as(isize, 11), ptrToInt(effect_mod.handle(&amb_def, null, rawAmbBody, null)));
1611 }
1612 
1613 fn handleAmbCountResult(_: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1614     return intToPtr(1);
1615 }
1616 
1617 fn handleAmbCountFlip(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1618     const false_branch = ptrToInt(effect_mod.resumeEffect(continuation.?, local, intToPtr(0)));
1619     const true_branch = ptrToInt(effect_mod.resumeFinal(continuation.?, local, intToPtr(1)));
1620     return intToPtr(false_branch + true_branch);
1621 }
1622 
1623 const amb_count_def = effect_mod.handlerDef(amb_effect, handleAmbCountResult, &.{
1624     .{ .opkind = .scoped, .optag = &amb_flip_tag, .opfun = handleAmbCountFlip },
1625 });
1626 
1627 fn ambCountHandle(action: effect_mod.ActionFn, arg: ?*anyopaque) isize {
1628     return ptrToInt(effect_mod.handle(&amb_count_def, null, action, arg));
1629 }
1630 
1631 fn xorAction(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1632     const x = ambFlip();
1633     const y = ambFlip();
1634     return intToPtr(if (x != y) 1 else 0);
1635 }
1636 
1637 test "ambiguity handler enumerates both boolean branches" {
1638     try std.testing.expectEqual(@as(isize, 4), ambCountHandle(xorAction, null));
1639 }
1640 
1641 const choice_names = [_:null]?[*:0]const u8{ "choice", "choice/choose", "choice/fail", null };
1642 const choice_effect: effect_mod.Effect = @ptrCast(&choice_names);
1643 const choice_choose_tag: effect_mod.Optag = .{ .effect = choice_effect, .opidx = 0 };
1644 const choice_fail_tag: effect_mod.Optag = .{ .effect = choice_effect, .opidx = 1 };
1645 
1646 fn choiceChoose(max: isize) isize {
1647     return ptrToInt(effect_mod.perform(&choice_choose_tag, intToPtr(max)));
1648 }
1649 
1650 fn choiceFail() void {
1651     _ = effect_mod.perform(&choice_fail_tag, null);
1652 }
1653 
1654 fn choiceBody(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1655     const chosen = choiceChoose(4);
1656     if (@rem(chosen, 2) == 0) return intToPtr(chosen);
1657     choiceFail();
1658     return intToPtr(99);
1659 }
1660 
1661 fn handleChoiceChoose(continuation: ?*effect_mod.Resume, local: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1662     const max = ptrToInt(arg);
1663     var total: isize = 0;
1664     var i: isize = 1;
1665     while (i <= max) : (i += 1) {
1666         const result = if (i == max)
1667             effect_mod.resumeFinal(continuation.?, local, intToPtr(i))
1668         else
1669             effect_mod.resumeEffect(continuation.?, local, intToPtr(i));
1670         total += ptrToInt(result);
1671     }
1672     return intToPtr(total);
1673 }
1674 
1675 fn handleChoiceFail(continuation: ?*effect_mod.Resume, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1676     effect_mod.resumeRelease(continuation);
1677     return intToPtr(0);
1678 }
1679 
1680 const choice_def = effect_mod.handlerDef(choice_effect, null, &.{
1681     .{ .opkind = .scoped, .optag = &choice_choose_tag, .opfun = handleChoiceChoose },
1682     .{ .opkind = .abort, .optag = &choice_fail_tag, .opfun = handleChoiceFail },
1683 });
1684 
1685 test "choice handler combines resumed branches and aborts failed branches" {
1686     try std.testing.expectEqual(@as(isize, 6), ptrToInt(effect_mod.handle(&choice_def, null, choiceBody, null)));
1687 }
1688 
1689 fn ambStateXor() bool {
1690     const x = ambFlip();
1691     const y = ambFlip();
1692     return x != y;
1693 }
1694 
1695 fn ambStateFoo(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1696     const p = ambFlip();
1697     const current = rawStateGet();
1698     rawStateSet(current + 1);
1699     const result = if (current > 0 and p) ambStateXor() else false;
1700     return intToPtr(if (result) 1 else 0);
1701 }
1702 
1703 fn stateInsideAmb(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1704     return stateHandle(&tail_state_def, ambStateFoo, ptrToInt(arg));
1705 }
1706 
1707 fn ambInsideState(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1708     return intToPtr(ambCountHandle(ambStateFoo, arg));
1709 }
1710 
1711 test "state and ambiguity handlers compose in both nesting orders" {
1712     try std.testing.expectEqual(@as(isize, 2), ambCountHandle(stateInsideAmb, intToPtr(0)));
1713     try std.testing.expectEqual(@as(isize, 5), ptrToInt(stateHandle(&tail_state_def, ambInsideState, 0)));
1714 }
1715 
1716 fn choiceCountBody(_: ?*anyopaque) callconv(.c) ?*anyopaque {
1717     return intToPtr(1);
1718 }
1719 
1720 fn queenSafe(queen: isize, queens: []const isize) bool {
1721     var diag: isize = 1;
1722     var i = queens.len;
1723     while (i > 0) {
1724         i -= 1;
1725         const previous = queens[i];
1726         if (queen == previous or queen == previous + diag or queen == previous - diag) return false;
1727         diag += 1;
1728     }
1729     return true;
1730 }
1731 
1732 fn findQueens(n: isize, col: usize, queens: *[12]isize) bool {
1733     if (col == 0) return true;
1734     if (!findQueens(n, col - 1, queens)) return false;
1735 
1736     const queen = choiceChoose(n);
1737     const placed = queens[0 .. col - 1];
1738     if (!queenSafe(queen, placed)) {
1739         choiceFail();
1740         return false;
1741     }
1742 
1743     queens[col - 1] = queen;
1744     return true;
1745 }
1746 
1747 fn nqueensBody(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1748     const n: usize = @intCast(ptrToInt(arg));
1749     var queens = @as([12]isize, @splat(0));
1750     return intToPtr(if (findQueens(@intCast(n), n, &queens)) 1 else 0);
1751 }
1752 
1753 test "choice handler counts n-queens solutions" {
1754     try std.testing.expectEqual(@as(isize, 92), ptrToInt(effect_mod.handle(&choice_def, null, nqueensBody, intToPtr(8))));
1755 }
1756 
1757 const yield_names = [_:null]?[*:0]const u8{ "yield", "yield/yield", null };
1758 const yield_effect: effect_mod.Effect = @ptrCast(&yield_names);
1759 const yield_yield_tag: effect_mod.Optag = .{ .effect = yield_effect, .opidx = 0 };
1760 
1761 fn yieldValue(value: isize) void {
1762     _ = effect_mod.perform(&yield_yield_tag, intToPtr(value));
1763 }
1764 
1765 fn handleYieldResult(local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1766     return local;
1767 }
1768 
1769 fn handleYieldYield(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque {
1770     return effect_mod.resumeTail(continuation.?, intToPtr(ptrToInt(local) + 1), local);
1771 }
1772 
1773 const yield_def = effect_mod.handlerDef(yield_effect, handleYieldResult, &.{
1774     .{ .opkind = .tail_noop, .optag = &yield_yield_tag, .opfun = handleYieldYield },
1775 });
1776 
1777 fn yieldHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque {
1778     return effect_mod.handle(&yield_def, intToPtr(init), action, arg);
1779 }
1780 
1781 fn handleChoiceIgnoreResult(_: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1782     return arg;
1783 }
1784 
1785 fn handleChoiceChooseIgnore(continuation: ?*effect_mod.Resume, local: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1786     const max = ptrToInt(arg);
1787     if (max <= 0) return intToPtr(0);
1788 
1789     var i: isize = 1;
1790     while (i <= max) : (i += 1) {
1791         _ = if (i == max)
1792             effect_mod.resumeFinal(continuation.?, local, intToPtr(i))
1793         else
1794             effect_mod.resumeEffect(continuation.?, local, intToPtr(i));
1795     }
1796     return intToPtr(0);
1797 }
1798 
1799 const choice_ignore_def = effect_mod.handlerDef(choice_effect, handleChoiceIgnoreResult, &.{
1800     .{ .opkind = .scoped, .optag = &choice_choose_tag, .opfun = handleChoiceChooseIgnore },
1801     .{ .opkind = .abort, .optag = &choice_fail_tag, .opfun = handleChoiceFail },
1802 });
1803 
1804 fn choiceIgnoreHandle(action: effect_mod.ActionFn, arg: ?*anyopaque) ?*anyopaque {
1805     return effect_mod.handle(&choice_ignore_def, null, action, arg);
1806 }
1807 
1808 fn triples(n: isize, sum: isize) void {
1809     const x = choiceChoose(n);
1810     const y = choiceChoose(x - 1);
1811     const z = choiceChoose(y - 1);
1812     if (x + y + z == sum) {
1813         yieldValue(x);
1814     } else {
1815         choiceFail();
1816     }
1817 }
1818 
1819 fn triplesBody(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1820     const payload = ptrToInt(arg);
1821     const n = @divTrunc(payload, 1 << 16);
1822     const sum = @mod(payload, 1 << 16);
1823     triples(n, sum);
1824     return intToPtr(0);
1825 }
1826 
1827 fn chooseTriples(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
1828     return choiceIgnoreHandle(triplesBody, arg);
1829 }
1830 
1831 test "choice and yield handlers count matching triples" {
1832     const payload = 100 * (1 << 16) + 27;
1833     try std.testing.expectEqual(@as(isize, 48), ptrToInt(yieldHandle(chooseTriples, 0, intToPtr(payload))));
1834 }
1835 
1836 const Reader = mp.EffectDefinition(.{
1837     .name = "typed-reader",
1838     .operations = .{
1839         .ask = mp.operation(void, isize),
1840     },
1841 });
1842 
1843 const ReaderContext = struct {
1844     value: isize,
1845 };
1846 
1847 fn askReader(context: *ReaderContext, _: void) isize {
1848     return context.value;
1849 }
1850 
1851 fn readerBody(_: *ReaderContext) isize {
1852     return Reader.performWithoutValue(.ask) + Reader.performWithoutValue(.ask);
1853 }
1854 
1855 test "typed effect handler answers tail operations without pointer casts" {
1856     var context: ReaderContext = .{ .value = 21 };
1857     try std.testing.expectEqual(@as(isize, 42), Reader.handle(isize, &context, readerBody, .{
1858         .ask = mp.on(.tail_noop, askReader),
1859     }));
1860 }
1861 
1862 test "typed effect lookup follows handler stack changes" {
1863     var first: ReaderContext = .{ .value = 3 };
1864     var second: ReaderContext = .{ .value = 11 };
1865 
1866     try std.testing.expectEqual(@as(isize, 6), Reader.handle(isize, &first, readerBody, .{
1867         .ask = mp.on(.tail_noop, askReader),
1868     }));
1869     try std.testing.expectEqual(@as(isize, 22), Reader.handle(isize, &second, readerBody, .{
1870         .ask = mp.on(.tail_noop, askReader),
1871     }));
1872 }
1873 
1874 fn typedAskOnce(_: *ReaderContext) isize {
1875     return Reader.performWithoutValue(.ask);
1876 }
1877 
1878 fn typedInnerForward(_: *ReaderContext) isize {
1879     var inner: ReaderContext = .{ .value = 99 };
1880     return Reader.handle(isize, &inner, typedAskOnce, .{
1881         .ask = mp.forward(),
1882     });
1883 }
1884 
1885 test "typed effect handler can forward an operation to an enclosing handler" {
1886     var outer: ReaderContext = .{ .value = 7 };
1887     try std.testing.expectEqual(@as(isize, 7), Reader.handle(isize, &outer, typedInnerForward, .{
1888         .ask = mp.on(.tail_noop, askReader),
1889     }));
1890 }
1891 
1892 const State = mp.EffectDefinition(.{
1893     .name = "typed-state",
1894     .operations = .{
1895         .get = mp.operation(void, isize),
1896         .set = mp.operation(isize, void),
1897     },
1898 });
1899 
1900 const StateContext = struct {
1901     current: isize,
1902 };
1903 
1904 fn stateGet(context: *StateContext, _: void) isize {
1905     return context.current;
1906 }
1907 
1908 fn stateSet(context: *StateContext, value: isize) void {
1909     context.current = value;
1910 }
1911 
1912 fn stateCounter(_: *StateContext) isize {
1913     var count: isize = 0;
1914     while (true) {
1915         const current = State.performWithoutValue(.get);
1916         if (current <= 0) break;
1917         State.perform(.set, current - 1);
1918         count += 1;
1919     }
1920     return count;
1921 }
1922 
1923 test "typed effect handler supports mutable Zig context" {
1924     var context: StateContext = .{ .current = 100 };
1925     const count = State.handle(isize, &context, stateCounter, .{
1926         .get = mp.on(.tail_noop, stateGet),
1927         .set = mp.on(.tail_noop, stateSet),
1928     });
1929 
1930     try std.testing.expectEqual(@as(isize, 100), count);
1931     try std.testing.expectEqual(@as(isize, 0), context.current);
1932 }
1933 
1934 const Amb = mp.EffectDefinition(.{
1935     .name = "typed-amb",
1936     .operations = .{
1937         .flip = mp.operation(void, bool),
1938     },
1939 });
1940 
1941 const AmbContext = struct {};
1942 
1943 fn ambBody(_: *AmbContext) isize {
1944     return if (Amb.performWithoutValue(.flip)) 10 else 1;
1945 }
1946 
1947 fn handleFlip(
1948     continuation: Amb.Continuation(.flip, .scoped, isize),
1949     _: *AmbContext,
1950     _: void,
1951 ) isize {
1952     const false_branch = continuation.continueWith(false);
1953     const true_branch = continuation.continueFinalWith(true);
1954     return false_branch + true_branch;
1955 }
1956 
1957 test "typed effect handler exposes scoped multi-shot continuations" {
1958     var context: AmbContext = .{};
1959     try std.testing.expectEqual(@as(isize, 11), Amb.handle(isize, &context, ambBody, .{
1960         .flip = mp.on(.scoped, handleFlip),
1961     }));
1962 }
1963 
1964 const Once = mp.EffectDefinition(.{
1965     .name = "typed-once",
1966     .operations = .{
1967         .bump = mp.operation(isize, isize),
1968     },
1969 });
1970 
1971 const OnceContext = struct {
1972     seen: isize = 0,
1973 };
1974 
1975 fn onceBody(_: *OnceContext) isize {
1976     return Once.perform(.bump, 41) + 1;
1977 }
1978 
1979 fn handleBump(
1980     continuation: Once.Continuation(.bump, .once, isize),
1981     context: *OnceContext,
1982     value: isize,
1983 ) isize {
1984     context.seen = value;
1985     return continuation.continueFinalWith(value + 1);
1986 }
1987 
1988 test "typed once handlers use final continuations" {
1989     var context: OnceContext = .{};
1990     try std.testing.expectEqual(@as(isize, 43), Once.handle(isize, &context, onceBody, .{
1991         .bump = mp.on(.once, handleBump),
1992     }));
1993     try std.testing.expectEqual(@as(isize, 41), context.seen);
1994 }
1995 
1996 const Pure = mp.EffectDefinition(.{
1997     .name = "typed-pure",
1998     .operations = .{
1999         .answer = mp.operation(void, isize),
2000         .double = mp.operation(isize, isize),
2001     },
2002 });
2003 
2004 fn pureAnswer() isize {
2005     return 21;
2006 }
2007 
2008 fn pureDouble(value: isize) isize {
2009     return value * 2;
2010 }
2011 
2012 fn pureBody() isize {
2013     return Pure.performWithoutValue(.answer) + Pure.perform(.double, 10);
2014 }
2015 
2016 test "typed effect handler supports no-context bodies and clauses" {
2017     try std.testing.expectEqual(@as(isize, 41), Pure.handleWithoutContext(isize, pureBody, .{
2018         .answer = mp.on(.tail_noop, pureAnswer),
2019         .double = mp.on(.tail_noop, pureDouble),
2020     }));
2021 }
2022 
2023 const FallibleReader = mp.EffectDefinition(.{
2024     .name = "typed-fallible-reader",
2025     .operations = .{
2026         .ask = mp.operation(void, error{Unavailable}!isize),
2027     },
2028 });
2029 
2030 const FallibleReaderContext = struct {
2031     available: bool,
2032     value: isize,
2033 };
2034 
2035 fn fallibleAsk(context: *FallibleReaderContext, _: void) error{Unavailable}!isize {
2036     if (!context.available) return error.Unavailable;
2037     return context.value;
2038 }
2039 
2040 fn fallibleBody(_: *FallibleReaderContext) error{Unavailable}!isize {
2041     const value = try FallibleReader.performWithoutValue(.ask);
2042     return value + 1;
2043 }
2044 
2045 test "typed effect handler preserves Zig error unions" {
2046     var unavailable: FallibleReaderContext = .{ .available = false, .value = 0 };
2047     try std.testing.expectError(error.Unavailable, FallibleReader.handle(error{Unavailable}!isize, &unavailable, fallibleBody, .{
2048         .ask = mp.on(.tail_noop, fallibleAsk),
2049     }));
2050 
2051     var available: FallibleReaderContext = .{ .available = true, .value = 41 };
2052     try std.testing.expectEqual(@as(isize, 42), try FallibleReader.handle(error{Unavailable}!isize, &available, fallibleBody, .{
2053         .ask = mp.on(.tail_noop, fallibleAsk),
2054     }));
2055 }
2056 
2057 const Choice = mp.EffectDefinition(.{
2058     .name = "typed-choice",
2059     .operations = .{
2060         .flip = mp.operation(void, bool),
2061     },
2062 });
2063 
2064 fn chooseBody() isize {
2065     return if (Choice.performWithoutValue(.flip)) 30 else 4;
2066 }
2067 
2068 fn chooseBoth(continuation: Choice.Continuation(.flip, .scoped, isize)) isize {
2069     const false_branch = continuation.continueWith(false);
2070     const true_branch = continuation.continueFinalWith(true);
2071     return false_branch + true_branch;
2072 }
2073 
2074 test "typed scoped continuations can omit unused context and argument" {
2075     try std.testing.expectEqual(@as(isize, 34), Choice.handleWithoutContext(isize, chooseBody, .{
2076         .flip = mp.on(.scoped, chooseBoth),
2077     }));
2078 }