tiny.mprompt.effect
Defined in tiny.mprompt.
Code in a body makes a named request, and the nearest enclosing code registered for that request answers it and decides whether the body continues, how often, and with what value: algebraic effect handlers, built on the package's stack-switching runtime.
API (32)
Actions
Public operations.
EffectContinuation: Returns the continuation type for clause kindsscoped_once,scoped,onceandmulti, so a typed clause that may keep the body waiting receives one and resumes the body with the operation's result.EffectDefinition: Returns a type for one effect so a caller declares an effect once in Zig, then performs and handles its operations with checked types and no pointer casts.effectName: Returns the effect's name, the first string of its name table, for a diagnostic to print the name of an effect.finally: Runsfun(arg), then callsfinally_fun(local), and returnsfun's result, so code can run a second function after its body returns.forward: Returns a clause that passes the operation to the next enclosing handler of the same effect, so an inner handler that answers some operations of an effect lets an outer handler answer the rest.handle: Raw callers use this name.handleRaw: The typedhandleand raw callers run a body under a handler table with this function.handlerDef: Builds a handler description from an effect, an optional result function, and up to eight operations, so a raw caller builds its handler constants.mask: Runsfun(arg)under a frame foreffect, for code that must reach an outer handler of an effect past the innermost one.on: Returns a clause that answers an operation withhandlerunder clause kindkind, so a caller writes one per operation in the clauses passed to a typedhandle.operation: Returns the signature of one operation with argument typeArgand result typeResult, so a caller writes one per field of anEffectDefinitionspec's.operations.operationTable: Returns an eight-slot table withentriesin order andnull_opin the remaining slots.optagName: Returns the string at position index plus one in the operation's effect table, for a diagnostic to print the name of an operation as the unhandled-operation message does.perform: Raw callers use this name.performRaw: The typedperformand raw code call this function to perform an operation.resumeEffect: Resumes the body: itsperformreturnsarg, andlocalbecomes the handler's new local state.resumeFinal: Resumes the body the wayresumeEffectdoes, as the resumption's last resume.resumeRelease: Ends an effect resumption for a clause that will not continue the body, such as a failing branch of a choice.resumeTail: Resumes the body for a clause whose last act is to resume, such as a state or reader clause, so no clause frame stays under the resumed body.
Types and contracts
Public types and contracts.
ActionFn: The function type for the body ofhandle,handleRaw,maskandfinally.Effect: A pointer to a null-terminated array of C strings: the effect's name, then one name per operation in index order.HandlerDef: Describes one handler: its effect, an optional result function, and a table of eight slots that hold its clauses.OpFn: The function type of a clause.Operation: One slot of a handler table: the operation's tag, its clause, and the kind of that clause.OperationKind: Lists the kinds of clause a handler table holds, as Cintvalues in declaration order.OperationSignature: Records an operation's argument type and result type.Optag: Names one operation by its effect and its index.ReleaseFn: The function type thatfinallycalls after its body returns.ResultFn: The function type of a handler's result function.Resume: Holds an effect resumption as a kind and a payload.ResumptionKind: Lists the four kinds of effect resumption as Cintvalues.
Values and defaults
Public values and defaults.
max_operations: The number of slots in every handler table, and so the most operations one effect can have.
Source
Source: lib/mprompt/src/effect.zig
zig
//! Code in a body makes a named request, and the nearest enclosing code registered for that request//! answers it and decides whether the body continues, how often, and with what value: algebraic//! effect handlers, built on the package's stack-switching runtime. Two layers provide this//! control: C-convention functions over tables of raw pointers, and a typed layer that builds those//! tables at compile time from a Zig description of the requests.//!//! A body wants to ask for things such as a value to read, a state to update, a choice among//! branches, or an early exit, without knowing which enclosing code answers, so the same body can//! run under different answers. The answering code needs a range of powers: answer and let the body//! go on at once, keep the body waiting and continue it later, continue it several times to explore//! each branch of a choice, or end it early.//!//! Finding the answering code happens at run time, by walking outward from the request through the//! code that encloses it, because answering code nests and one body can run under different//! answering code. Keeping the body waiting means switching stacks and saving registers, and an//! answer that lets the body go on at once, such as reading a value or updating a counter, needs//! neither. Continuing a body more than once needs a copy of its suspended stack, and ending it//! early has to discard that stack without running the rest of the body.//!//! Daan Leijen's paper [Implementing Algebraic Effects in//! C](https://www.microsoft.com/en-us/research/publication/implementing-algebraic-effects-c/) and//! his [libmprompt](https://github.com/koka-lang/libmprompt) library implement algebraic effect//! handlers on top of multi-prompt delimited continuations. The package keeps libmprompt's way of//! moving control through handlers: each handler runs its body under a prompt of its own, and an//! operation that suspends the body moves control to its handler's prompt. The package's reference//! benchmark reruns libmprompt's effect workloads, among them a reader, a state counter, a choice//! search, n-queens and a triples search, at the sizes meant for optimized builds.//!//! The package identifies each effect by one table of strings: the effect's name, then one name per//! operation. The runtime finds an operation's handler by comparing that table's address. A handler//! wraps its body in a frame on a new stack, and the runtime walks the frames outward from the call//! that performs an operation to the innermost handler for its effect. A handler answers each of//! its operations with a function written for that operation (*clause*). Each clause carries a kind//! fixed when the handler is written, so a clause that lets the body go on at once runs in place on//! the body's stack as an ordinary call, and only the kinds that may keep the body waiting switch//! stacks. One of those kinds allows at most one resume, made while its clause runs, so it keeps//! its handle in the clause's own stack frame and allocates nothing. The others allocate their//! handle from the process allocator. The handler search keeps the last handler it found and reuses//! it for the same effect until a frame is pushed or popped. The typed layer declares an effect//! once with `EffectDefinition`, builds its tables at compile time, allows at most eight//! operations, and rejects a handler that leaves an operation without a clause at compile time.//!//! - *local state*: a pointer the handler keeps and receives back with each resume//! - *result function*: a handler's optional function that replaces the body's result//! - *operation tag*: the record naming one operation by its effect and index//! - *mask*: a frame that makes operations of one effect skip their innermost handler//! - *release*: end a suspended body without running the rest of it//! - *tail resume*: a resume made as a function's last act that never returns to it//! - *one-shot handle*: a handle resumable at most once, continuing in place with no copy//! - *multi-shot handle*: a reference-counted handle resumable more than onceconst std = @import("std");const pretty = @import("pretty");const mp = @import("root.zig");const effect_mod = @This();const assert = std.debug.assert;/// The function type for the body of `handle`, `handleRaw`, `mask` and `finally`. A caller passes/// one as the code to run under a handler or a frame. The function uses the C calling convention,/// takes one pointer argument and returns one pointer result.pub const ActionFn = *const fn (?*anyopaque) callconv(.c) ?*anyopaque;/// The function type of a handler's result function. A handler that turns the body's final result/// into something else supplies one, as a counting handler turns each finished branch into 1. The/// function receives the handler's local state and the body's result, and its return value becomes/// the result of `handle`.pub const ResultFn = *const fn (?*anyopaque, ?*anyopaque) callconv(.c) ?*anyopaque;/// The function type that `finally` calls after its body returns. The function receives the local/// pointer given to `finally`.pub const ReleaseFn = *const fn (?*anyopaque) callconv(.c) void;/// The function type of a clause. A handler author writes one per operation. The function receives/// the resumption, the handler's local state and the operation's argument, and it returns the/// clause's result. Resuming the resumption makes the body's `perform` call return the value passed/// in. The resumption is null for `abort` and `never` clauses. For `tail_noop` and `tail` clauses,/// the return value becomes the result of `perform`. For the other kinds, the return value becomes/// the result of the `handle` call, or of the resume that last continued the body.pub const OpFn = *const fn (?*Resume, ?*anyopaque, ?*anyopaque) callconv(.c) ?*anyopaque;/// A pointer to a null-terminated array of C strings: the effect's name, then one name per/// operation in index order. A raw caller defines one per set of operations and uses it in tags and/// handler tables. The runtime matches a handler to an operation's effect by this pointer's/// address, so each effect needs exactly one array, and two arrays that hold the same strings are/// two different effects. The typed layer builds the array from the effect's name and one/// `name/operation` string per operation.pub const Effect = [*:null]const ?[*:0]const u8;/// Names one operation by its effect and its index. A raw caller defines one constant per operation/// and performs with its address. The `perform` call takes a pointer to one, and the tag's index/// picks the handler's clause.pub const Optag = extern struct { /// The effect the operation belongs to, which the handler search matches by address. The /// `optagName` function prints `<null>` for a null effect. effect: ?Effect, /// The operation's zero-based index. The index picks the handler's clause and the name at /// position index plus one in the effect's array. An index of 8 or more fails the bounds check /// in safe builds, and nothing checks that the clause at the index answers this operation. opidx: c_long,};/// Lists the kinds of clause a handler table holds, as C `int` values in declaration order. A/// handler author picks one per operation to say how its clause may continue the body, and the/// runtime takes the cheapest way to run the clause from it. The kind decides whether the clause/// runs on the body's stack or after a stack switch, and whether its resumption lives on a stack or/// is allocated.pub const OperationKind = enum(c_int) { /// Marks an empty slot, and handler tables are padded with it. Performing an operation whose /// slot holds it runs nothing and returns null. The `on` helper rejects it at compile time. null_op, /// The clause passes the operation to the next enclosing handler of the same effect: it /// performs the operation again inside a mask, and the mask covers this handler's effect. The /// slot holds no function. The typed layer builds this clause with `forward()`, and `on` /// rejects this kind. forward, /// The clause ends the body: control goes back to the handler, the body's stacklets are dropped /// without running the rest of the body, and the clause runs with a null resumption. The /// clause's result becomes the result of `handle`, and the handler's result function does not /// run. A `finally` call inside the dropped body never calls its `finally_fun` function. A /// typed clause of this kind returns the result type of `handle`. abort, /// Runs the same code path as `abort`. never, /// The clause runs in place on the body's stack as an ordinary call, with no stack switch. The /// resumption lives on that stack, and a raw clause continues the body by returning /// `resumeTail(...)`. A typed clause of this kind returns the operation's result. An operation /// the clause performs itself starts its search at the body's innermost frame, so it can reach /// this same handler. tail_noop, /// The clause runs in place on the body's stack, as with `tail_noop`. While the clause runs, an /// extra frame makes the operations it performs skip this handler and every frame inside it, so /// they reach the handlers outside. tail, /// The body suspends to the handler's prompt, and the clause runs on the handler's side with a /// resumption kept in its own stack frame, so nothing is allocated. The clause resumes the body /// at most once, before it returns, and it cannot release the resumption. A clause that returns /// without resuming leaves the body's stacklet allocated. scoped_once, /// Runs the same code path as `multi`: the clause gets an allocated resumption that can resume /// more than once. The tests use it for clauses that resume the body several times before they /// return. scoped, /// The body suspends, and the clause gets an allocated resumption that it may keep after it /// returns. The clause resumes it at most once, with `resumeFinal` or `resumeTail`, and /// `resumeEffect` on it fails an assertion in safe builds. A kept resumption can be resumed /// later under a different handler. once, /// The body suspends, and the clause gets an allocated resumption that can resume more than /// once. The `resumeEffect` call keeps the resumption for another resume, and `resumeFinal`, /// `resumeTail` or `resumeRelease` ends it. multi,};/// One slot of a handler table: the operation's tag, its clause, and the kind of that clause. A raw/// handler table holds one per operation.pub const Operation = extern struct { /// The kind of the slot's clause, which decides how `perform` runs the clause. opkind: OperationKind, /// The tag of the operation this slot answers. A `forward` slot performs the operation again /// with it. The `perform` call selects the slot by the performed tag's index and does not /// compare this field with that tag. optag: ?*const Optag, /// The slot's clause, null for `forward` and `null_op` slots. opfun: ?OpFn,};/// The number of slots in every handler table, and so the most operations one effect can have. A/// caller sizes handler tables with it, and `EffectDefinition` checks each effect against it.pub const max_operations = 8;/// Describes one handler: its effect, an optional result function, and a table of eight slots that/// hold its clauses. A raw caller builds one per handler, usually as a constant, and passes its/// address to `handle`. The `handle` function keeps a pointer to it while the body runs, so it must/// outlive the call.pub const HandlerDef = extern struct { /// The effect this handler answers. The runtime picks this handler for an operation when this /// pointer equals the operation's effect. effect: ?Effect, /// An optional function applied to the body's result, with the handler's current local state, /// when the body returns. A null value leaves the result unchanged. The function does not run /// when an `abort` or `never` clause ends the body. resultfun: ?ResultFn, /// The eight slots that hold the handler's clauses: slot i answers the operation whose index is /// i. Unused slots hold `null_op`. operations: [max_operations]Operation,};/// Records an operation's argument type and result type. A caller writes one with `operation` for/// each entry of an `EffectDefinition` spec.pub const OperationSignature = struct { /// The type `perform` passes to the clause, and `void` means the operation takes no argument. Arg: type, /// The type the clause gives back to `perform`. Result: type,};/// Returns the signature of one operation with argument type `Arg` and result type `Result`, so a/// caller writes one per field of an `EffectDefinition` spec's `.operations`.pub fn operation(comptime Arg: type, comptime Result: type) OperationSignature { return .{ .Arg = Arg, .Result = Result };}/// Returns a clause that answers an operation with `handler` under clause kind `kind`, so a caller/// writes one per operation in the clauses passed to a typed `handle`. A `kind` of `.forward` is a/// compile error that points to `forward()`, and `.null_op` is a compile error. For `tail_noop`,/// `tail`, `abort` and `never`, the handler takes `()`, `(arg)` or `(context, arg)`, and the empty/// form needs a `void` argument. For `scoped_once`, `scoped`, `once` and `multi`, the handler takes/// `(continuation)`, `(continuation, arg)` or `(continuation, context, arg)`, and the one-parameter/// form needs a `void` argument. The `tail_noop` and `tail` handlers return the operation's result,/// and the other kinds return the result type of `handle`. The handler must be a function with a/// declared return type, and a generic handler is a compile error.pub fn on(comptime kind: OperationKind, comptime handler: anytype) HandlerClause(@TypeOf(handler)) { if (kind == .forward) { @compileError("use forward() for forwarded operations"); } if (kind == .null_op) { @compileError("null operations are only used to terminate raw handler tables"); } return .{ .kind = kind, .handler = handler };}/// Returns a clause that passes the operation to the next enclosing handler of the same effect, so/// an inner handler that answers some operations of an effect lets an outer handler answer the/// rest.pub fn forward() ForwardClause { return .{};}fn HandlerClause(comptime Handler: type) type { return struct { kind: OperationKind, handler: Handler, };}const ForwardClause = struct { kind: OperationKind = .forward,};const ContinueKind = enum { regular, final, tail,};/// Returns a type for one effect so a caller declares an effect once in Zig, then performs and/// handles its operations with checked types and no pointer casts. The type is built from a spec/// with a `.name` string and an `.operations` struct literal whose fields are/// `operation(Arg, Result)` values. A spec with more than eight operations is a compile error, and/// so is an `.operations` value of any type other than a struct literal. The type builds the/// effect's name table and one operation tag per operation at compile time, with operation names of/// the form `name/operation`.pub fn EffectDefinition(comptime spec: anytype) type { const Operations = @TypeOf(spec.operations); const operations_info = switch (@typeInfo(Operations)) { .@"struct" => |info| info, else => @compileError("effect operations must be a struct literal"), }; const operation_names = operations_info.field_names; if (operation_names.len > max_operations) { @compileError("effect definitions support at most eight operations"); } return struct { /// An enum with one tag per field of `.operations`, in declaration order. `optag`, /// `Signature`, `Continuation`, `perform` and `performWithoutValue` take one to pick the /// operation. pub const Op: type = std.meta.FieldEnum(Operations); /// The number of operations in the spec. pub const operation_count = operation_names.len; const Self = @This(); const names = blk: { var result: [operation_count + 2:null]?[*:0]const u8 = undefined; result[0] = spec.name; for (operation_names, 0..) |operation_name, index| { result[index + 1] = std.fmt.comptimePrint("{s}/{s}", .{ spec.name, operation_name }); } result[operation_count + 1] = null; break :blk result; }; const tags = blk: { var result: [operation_count]Optag = undefined; for (operation_names, 0..) |_, index| { result[index] = .{ .effect = rawEffect(), .opidx = index }; } break :blk result; }; /// Returns the effect's name table so raw calls that name this effect take it. pub fn rawEffect() Effect { return @ptrCast(&names); } /// Returns a pointer to `op_id`'s operation tag so raw calls that perform or handle one /// operation take its tag. The tag's index is the operation's position in `.operations`. pub fn optag(comptime op_id: Op) *const Optag { return &tags[operationIndex(op_id)]; } /// Returns the argument type and result type of `op_id` as the spec declares them. pub fn Signature(comptime op_id: Op) OperationSignature { return @field(spec.operations, @tagName(op_id)); } /// Returns the continuation type that a clause of kind `kind` for `op_id` receives, so a /// continuation clause names its parameter type with it. A resume of that continuation /// passes a value of `op_id`'s result type into the body and returns `HandlerResult`. /// `HandlerResult` must be the `Result` of the `handle` call, and the compiler rejects a /// mismatch at the clause call site. pub fn Continuation(comptime op_id: Op, comptime kind: OperationKind, comptime HandlerResult: type) type { const sig = Signature(op_id); return EffectContinuation(sig.Result, HandlerResult, kind); } /// Performs `op_id` with `arg` and returns the result the clause gives, so code under a /// handler asks for an operation with a typed argument and gets a typed result. The /// argument and the result travel in a small record on the performer's stack, and the /// clause reaches that record by address. Whether the call suspends the body depends on the /// clause's kind. With no enclosing handler for the effect, the runtime prints /// `lib/mpeff: unhandled operation:` and the operation's name to standard error. An /// operation with a non-`void` result then unwraps a null pointer, which panics in safe /// builds, and an operation with a `void` result returns normally. pub fn perform(comptime op_id: Op, arg: Signature(op_id).Arg) Signature(op_id).Result { const sig = Signature(op_id); const OpFrame = PerformFrame(sig.Arg, sig.Result); var frame: OpFrame = .{}; writeSlot(sig.Arg, &frame.arg, arg); return readSlot(sig.Result, performRaw(optag(op_id), &frame)); } /// Performs `op_id` with no argument, for an operation whose `Arg` is `void`. Any other /// operation is a compile error. pub fn performWithoutValue(comptime op_id: Op) Signature(op_id).Result { const sig = Signature(op_id); if (sig.Arg != void) { @compileError("performWithoutValue requires an operation with a void argument"); } return Self.perform(op_id, {}); } /// Runs `body(context)` on a new stacklet under a handler for this effect and returns a /// `Result`, so a caller runs a body under this effect's typed clauses and gets back the /// body's result, or the result a clause chose. The `context` parameter must be a pointer, /// and every clause that asks for it receives it. The `clauses` argument is a struct /// literal with one field per operation, each made by `on` or `forward()`, and a missing /// field is a compile error. The handler table is built at compile time. The result is the /// body's return value, the value an `abort` or `never` clause returns, or the value a /// continuation clause returns. An error union passes through as `Result`. pub fn handle( comptime Result: type, context: anytype, comptime body: *const fn (@TypeOf(context)) Result, comptime clauses: anytype, ) Result { const Context = @TypeOf(context); requirePointer(Context, "effect handler context"); const Clauses = @TypeOf(clauses); const Runner = struct { const Env = struct { context: Context, result: Slot(Result) = .{}, }; fn start(arg: ?*anyopaque) callconv(.c) ?*anyopaque { const env: *Env = @ptrCast(@alignCast(arg.?)); writeSlot(Result, &env.result, body(env.context)); return slotPtr(Result, &env.result); } fn table() [max_operations]Operation { var entries = @as([max_operations]Operation, @splat(.{ .opkind = .null_op, .optag = null, .opfun = null })); inline for (operation_names) |operation_name| { if (!hasStructField(Clauses, operation_name)) { @compileError("missing handler clause for operation '" ++ operation_name ++ "'"); } const op_id = std.meta.stringToEnum(Op, operation_name).?; entries[operationIndex(op_id)] = operationEntry(op_id); } return entries; } fn operationEntry(comptime op_id: Op) Operation { const clause = @field(clauses, @tagName(op_id)); return .{ .opkind = clause.kind, .optag = optag(op_id), .opfun = switch (clause.kind) { .forward => null, .null_op => null, else => thunk(op_id), }, }; } fn thunk(comptime op_id: Op) OpFn { return struct { fn call(raw_resume: ?*Resume, local: ?*anyopaque, raw_arg: ?*anyopaque) callconv(.c) ?*anyopaque { const env: *Env = @ptrCast(@alignCast(local.?)); const sig = Signature(op_id); const OpFrame = PerformFrame(sig.Arg, sig.Result); const frame: *OpFrame = @ptrCast(@alignCast(raw_arg.?)); const op_arg = readSlotValue(sig.Arg, &frame.arg); const clause = @field(clauses, @tagName(op_id)); switch (clause.kind) { .tail_noop, .tail => { const op_result = callTailHandler(clause.handler, env.context, op_arg); writeSlot(sig.Result, &frame.result, op_result); return slotPtr(sig.Result, &frame.result); }, .abort, .never => { const result = callTailHandler(clause.handler, env.context, op_arg); writeSlot(Result, &env.result, result); return slotPtr(Result, &env.result); }, .scoped_once, .scoped, .once, .multi => { const continuation: EffectContinuation(sig.Result, Result, clause.kind) = .{ .raw_resume = raw_resume.?, .local = local, }; const result = callContinuationHandler(clause.handler, continuation, env.context, op_arg); writeSlot(Result, &env.result, result); return slotPtr(Result, &env.result); }, .forward, .null_op => unreachable, } } }.call; } const hdef = HandlerDef{ .effect = rawEffect(), .resultfun = null, .operations = table(), }; }; var env: Runner.Env = .{ .context = context }; return readSlot(Result, handleRaw(&Runner.hdef, &env, Runner.start, &env)); } /// Runs a body that takes no arguments, the way `handle` does, so a caller whose body and /// clauses need no context skips the pointer. A clause of the `(context, arg)` form /// receives a pointer to an empty struct. pub fn handleWithoutContext( comptime Result: type, comptime body: *const fn () Result, comptime clauses: anytype, ) Result { const Context = struct {}; const Runner = struct { fn start(_: *Context) Result { return body(); } }; var context: Context = .{}; return Self.handle(Result, &context, Runner.start, clauses); } fn operationIndex(comptime op_id: Op) usize { inline for (operation_names, 0..) |operation_name, index| { if (std.mem.eql(u8, operation_name, @tagName(op_id))) return index; } unreachable; } };}/// Returns the continuation type for clause kinds `scoped_once`, `scoped`, `once` and `multi`, so a/// typed clause that may keep the body waiting receives one and resumes the body with the/// operation's result. Any other kind is a compile error. `ResumeValue` is the operation's result/// type, which `perform` returns in the body. `HandlerResult` is the result type of the `handle`/// call. A resume returns when the resumed body finishes, with the body's result, or when a later/// clause suspends the body and returns, with that clause's result. A `scoped_once` or `once`/// continuation is resumed at most once, and a `scoped` or `multi` continuation is resumed any/// number of times, with its last resume final. A `scoped`, `once` or `multi` continuation that/// gets no final resume and no release stays allocated.pub fn EffectContinuation(comptime ResumeValue: type, comptime HandlerResult: type, comptime kind: OperationKind) type { comptime { switch (kind) { .scoped_once, .scoped, .once, .multi => {}, else => @compileError("effect continuations are only available for scoped_once, scoped, once, and multi operations"), } } return struct { /// The raw resumption for the suspended body. raw_resume: *Resume, /// The handler's local state, passed back with each resume. The typed `handle` sets it to /// the address of its own environment, which holds the context and the result. local: ?*anyopaque, const Self = @This(); /// Resumes the body with `value` as the result of its `perform` and returns the handler's /// result, for every resume but the last in a clause that resumes the body more than once, /// as a search over both branches of a choice does. A `scoped` or `multi` continuation /// stays usable for another resume. This call is the one resume for a `scoped_once` /// continuation. A `once` continuation is a compile error here, and it uses /// `continueFinalWith` or `continueTailWith`. pub fn continueWith(self: Self, value: ResumeValue) HandlerResult { if (kind == .once) { @compileError("once continuations must use continueFinalWith or continueTailWith"); } return self.continueInternal(.regular, value); } /// Resumes the body with `value` as the result of its `perform` and returns the handler's /// result, so a clause makes its last resume of the body. This call is the continuation's /// last resume: it frees an allocated continuation, which no caller uses again. pub fn continueFinalWith(self: Self, value: ResumeValue) HandlerResult { return self.continueInternal(.final, value); } /// Resumes the body with `value` as a tail resume and frees an allocated continuation, for /// a clause whose last act is to resume the body, so no clause frame stays under the /// resumed body. For every kind this type allows, control does not come back to the clause, /// and the result goes to the call that entered or last resumed the handled body. The call /// must be the clause's last action. pub fn continueTailWith(self: Self, value: ResumeValue) HandlerResult { return self.continueInternal(.tail, value); } /// Resumes the body with no value, for a `ResumeValue` of `void`. Any other `ResumeValue` /// is a compile error. pub fn continueWithoutValue(self: Self) HandlerResult { if (ResumeValue != void) { @compileError("continueWithoutValue requires ResumeValue to be void"); } return self.continueWith({}); } /// Makes the final resume with no value, for a `ResumeValue` of `void`. Any other /// `ResumeValue` is a compile error. pub fn continueFinalWithoutValue(self: Self) HandlerResult { if (ResumeValue != void) { @compileError("continueFinalWithoutValue requires ResumeValue to be void"); } return self.continueFinalWith({}); } /// Makes a tail resume with no value, for a `ResumeValue` of `void`. Any other /// `ResumeValue` is a compile error. pub fn continueTailWithoutValue(self: Self) HandlerResult { if (ResumeValue != void) { @compileError("continueTailWithoutValue requires ResumeValue to be void"); } return self.continueTailWith({}); } /// Ends the suspended body without running its rest, for a clause that will not continue /// the body, such as a failing branch of a search. The body is resumed only to jump back to /// its handler, and its stacklet is freed. For a `multi` or `scoped` continuation that /// other references share, or that has resumed before, the call gives up one reference and /// resumes nothing. The call is valid for `scoped`, `once` and `multi` continuations, and /// any other kind is a compile error. A `finally` call inside the ended body never calls /// its `finally_fun` function. pub fn release(self: Self) void { switch (kind) { .scoped, .once, .multi => {}, else => @compileError("release is only valid for scoped, once, and multi continuations"), } resumeRelease(self.raw_resume); } fn continueInternal(self: Self, comptime continue_kind: ContinueKind, value: ResumeValue) HandlerResult { var slot: Slot(ResumeValue) = .{}; writeSlot(ResumeValue, &slot, value); const result = switch (continue_kind) { .regular => resumeEffect(self.raw_resume, self.local, slotPtr(ResumeValue, &slot)), .final => resumeFinal(self.raw_resume, self.local, slotPtr(ResumeValue, &slot)), .tail => resumeTail(self.raw_resume, self.local, slotPtr(ResumeValue, &slot)), }; return readSlot(HandlerResult, result); } };}/// Builds a handler description from an effect, an optional result function, and up to eight/// operations, so a raw caller builds its handler constants. The operations are copied in slice/// order, and the remaining slots hold `null_op`. Each operation's position in the slice must equal/// its tag's index, because `perform` picks the clause by that index. The function runs at compile/// time when its arguments are constants.pub fn handlerDef(effect: ?Effect, resultfun: ?ResultFn, operations: []const Operation) HandlerDef { return .{ .effect = effect, .resultfun = resultfun, .operations = operationTable(operations), };}/// Returns an eight-slot table with `entries` in order and `null_op` in the remaining slots./// `handlerDef` calls it to fill the table of each handler description it builds. More than eight/// entries fails an assertion in safe builds.pub fn operationTable(entries: []const Operation) [max_operations]Operation { assert(entries.len <= max_operations); var table = @as([max_operations]Operation, @splat(.{ .opkind = .null_op, .optag = null, .opfun = null })); for (entries, 0..) |entry, index| table[index] = entry; return table;}fn PerformFrame(comptime Arg: type, comptime Result: type) type { return struct { arg: Slot(Arg) = .{}, result: Slot(Result) = .{}, };}fn Slot(comptime T: type) type { return if (T == void) struct {} else struct { value: T = undefined, };}fn writeSlot(comptime T: type, slot: *Slot(T), value: T) void { if (comptime T != void) { slot.value = value; }}fn readSlotValue(comptime T: type, slot: *Slot(T)) T { if (comptime T != void) { return slot.value; } return {};}fn slotPtr(comptime T: type, slot: *Slot(T)) ?*anyopaque { if (comptime T != void) { return @ptrCast(slot); } return null;}fn readSlot(comptime T: type, ptr: ?*anyopaque) T { if (comptime T != void) { const slot: *Slot(T) = @ptrCast(@alignCast(ptr.?)); return slot.value; } return {};}fn callTailHandler(handler: anytype, context: anytype, arg: anytype) handlerReturnType(@TypeOf(handler)) { const Arg = @TypeOf(arg); const count = comptime handlerParamCount(@TypeOf(handler)); if (count == 0) { if (Arg != void) @compileError("handler without parameters requires a void operation argument"); return handler(); } if (count == 1) return handler(arg); if (count == 2) return handler(context, arg); @compileError("tail handlers must accept (), (arg), or (context, arg)");}fn callContinuationHandler( handler: anytype, continuation: anytype, context: anytype, arg: anytype,) handlerReturnType(@TypeOf(handler)) { const Arg = @TypeOf(arg); const count = comptime handlerParamCount(@TypeOf(handler)); if (count == 1) { if (Arg != void) @compileError("continuation-only handlers require a void operation argument"); return handler(continuation); } if (count == 2) return handler(continuation, arg); if (count == 3) return handler(continuation, context, arg); @compileError("continuation handlers must accept (continuation), (continuation, arg), or (continuation, context, arg)");}fn handlerReturnType(comptime Handler: type) type { return handlerFnInfo(Handler).return_type orelse @compileError("generic effect handlers are not supported");}fn handlerParamCount(comptime Handler: type) usize { return handlerFnInfo(Handler).param_types.len;}fn handlerFnInfo(comptime Handler: type) std.builtin.Type.Fn { const Fn = switch (@typeInfo(Handler)) { .pointer => |ptr| ptr.child, .@"fn" => Handler, else => @compileError("effect handlers must be functions"), }; return switch (@typeInfo(Fn)) { .@"fn" => |info| info, else => @compileError("effect handlers must be functions"), };}fn requirePointer(comptime T: type, comptime name: []const u8) void { switch (@typeInfo(T)) { .pointer => {}, else => @compileError(name ++ " must be a pointer"), }}fn hasStructField(comptime T: type, comptime name: []const u8) bool { const info = switch (@typeInfo(T)) { .@"struct" => |struct_info| struct_info, else => return false, }; inline for (info.field_names) |field_name| { if (std.mem.eql(u8, field_name, name)) return true; } return false;}const Frame = extern struct { effect: ?Effect, parent: ?*Frame,};const HandleFrame = struct { frame: Frame, prompt: *mp.Prompt, hdef: *const HandlerDef, local: ?*anyopaque,};const UnderFrame = struct { frame: Frame, under: ?Effect,};const MaskFrame = struct { frame: Frame, mask: ?Effect, from: usize,};const FinallyFrame = struct { frame: Frame, fun: ReleaseFn, local: ?*anyopaque,};/// Lists the four kinds of effect resumption as C `int` values. The runtime records in each/// resumption where it lives and how it resumes, so one set of resume functions serves every kind/// of clause.pub const ResumptionKind = enum(c_int) { /// The clause runs on the body's stack, and the resumption points at the handler's local state. /// Resuming it stores the new local state and returns the value. `tail_noop` and `tail` clauses /// get this kind. inplace, /// The resumption lives in a stack frame of the clause and wraps the suspended prompt's /// one-shot handle. scoped_once, /// The resumption is allocated and wraps the suspended prompt's one-shot handle. Its final /// resume frees it. once, /// The resumption is allocated and wraps the suspended prompt's multi-shot handle. Each resume /// before the final resume adds a reference first. multi,};const ResumePayload = extern union { plocal: *?*anyopaque, continuation: *mp.Resume,};/// Holds an effect resumption as a kind and a payload. A raw clause receives a pointer to one and/// passes it to `resumeEffect`, `resumeFinal`, `resumeTail` or `resumeRelease`.pub const Resume = extern struct { /// Records which of the four kinds this resumption is. kind: ResumptionKind, /// Holds a pointer to the handler's local state for an `inplace` resumption. For the other /// kinds, the payload holds the prompt resumption for the suspended body. payload: ResumePayload,};const PerformEnv = extern struct { rkind: ResumptionKind, opfun: OpFn, local: ?*anyopaque, oparg: ?*anyopaque,};const ResumeEnv = extern struct { local: ?*anyopaque, result: ?*anyopaque, unwind: bool,};const HandleStartEnv = extern struct { hdef: *const HandlerDef, local: ?*anyopaque, body: ActionFn, arg: ?*anyopaque,};const under_names = [_:null]?[*:0]const u8{ "mpe_frame_under", null };const mask_names = [_:null]?[*:0]const u8{ "mpe_frame_mask", null };const finally_names = [_:null]?[*:0]const u8{ "mpe_frame_finally", null };const unwind_names = [_:null]?[*:0]const u8{ "mpe_unwind", "mpe_unwind/mpe_unwind", null };const under_effect: Effect = @ptrCast(&under_names);const mask_effect: Effect = @ptrCast(&mask_names);const finally_effect: Effect = @ptrCast(&finally_names);const unwind_effect: Effect = @ptrCast(&unwind_names);const unwind_optag: Optag = .{ .effect = unwind_effect, .opidx = 0 };threadlocal var frame_top: ?*Frame = null;threadlocal var find_cache: ?*HandleFrame = null;/// Returns the effect's name, the first string of its name table, for a diagnostic to print the/// name of an effect. The call returns `<null>` for a null effect or a null first entry.pub fn effectName(effect: ?Effect) [*:0]const u8 { const names = effect orelse return "<null>"; return names[0] orelse "<null>";}/// Returns the string at position index plus one in the operation's effect table, for a diagnostic/// to print the name of an operation as the unhandled-operation message does. The call returns/// `<null>` for a null tag, a null effect or a null entry.pub fn optagName(optag: ?*const Optag) [*:0]const u8 { const tag = optag orelse return "<null>"; const names = tag.effect orelse return "<null>"; const index: usize = @intCast(tag.opidx + 1); return names[index] orelse "<null>";}/// The typed `handle` and raw callers run a body under a handler table with this function. The call/// creates a prompt with a new stacklet, pushes a handler frame for `hdef` with local state/// `local`, and runs `body(arg)` there. The call returns the body's result, passed through the/// handler's result function when it has one. When an `abort` or `never` clause ends the body, the/// call returns that clause's result. `hdef` must stay valid until the call returns. Handler frames/// live in a per-thread list, so a handler answers operations performed on its own thread.pub fn handleRaw(hdef: *const HandlerDef, local: ?*anyopaque, body: ActionFn, arg: ?*anyopaque) ?*anyopaque { var env: HandleStartEnv = .{ .hdef = hdef, .local = local, .body = body, .arg = arg, }; return mp.prompt(handleStart, &env);}/// Raw callers use this name. The function does exactly what `handleRaw` does.pub fn handle(hdef: *const HandlerDef, local: ?*anyopaque, body: ActionFn, arg: ?*anyopaque) ?*anyopaque { return handleRaw(hdef, local, body, arg);}/// The typed `perform` and raw code call this function to perform an operation. The call finds the/// innermost handler for the tag's effect and runs its clause at the tag's index, in the way the/// clause's kind requires. The search walks the thread's frames outward and passes over one more/// handler of the operation's effect for each mask of that effect that applies. Inside a `tail`/// clause, the search jumps from the clause's frame to the frames outside the clause's own handler./// The last handler found is kept and reused for the same effect until a frame is pushed or popped./// The call returns the clause's result for kinds that run in place, and the value a resume passed/// for kinds that suspend the body. With no handler for the effect, the call prints/// `lib/mpeff: unhandled operation:` and the operation's name to standard error and returns null.pub fn performRaw(optag: *const Optag, arg: ?*anyopaque) ?*anyopaque { const h = find(optag) orelse { @branchHint(.unlikely); return unhandledOperation(optag); }; const op_index: usize = @intCast(optag.opidx); const op = &h.hdef.operations[op_index]; return performAt(h, op, arg);}/// Raw callers use this name. The function does exactly what `performRaw` does.pub fn perform(optag: *const Optag, arg: ?*anyopaque) ?*anyopaque { return performRaw(optag, arg);}/// Resumes the body: its `perform` returns `arg`, and `local` becomes the handler's new local/// state. A clause that resumes the body more than once, as one that tries each branch of a choice/// does, calls it for every resume but the last. When the body finishes and the handler has no/// result function, the call returns the body's result. When the body finishes and the handler has/// a result function, the call returns the value that the function puts in place of the body's/// result. When a later clause suspends the body and returns, the call returns that clause's/// result. For a `multi` resumption, the call adds a reference first, so the resumption stays/// usable. For a `scoped_once` resumption, this call is the one resume. An `inplace` or `once`/// resumption fails an assertion in safe builds.pub fn resumeEffect(resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque) ?*anyopaque { return resumeInternal(false, resume_ptr, local, arg, false);}/// Resumes the body the way `resumeEffect` does, as the resumption's last resume. A clause ends/// every allocated resumption with this function, `resumeTail` or `resumeRelease`, and makes its/// last resume of a `multi` resumption with it. For `once` and `multi` resumptions, the call frees/// the allocated record first, and the caller never uses the resumption again.pub fn resumeFinal(resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque) ?*anyopaque { return resumeInternal(true, resume_ptr, local, arg, false);}/// Resumes the body for a clause whose last act is to resume, such as a state or reader clause, so/// no clause frame stays under the resumed body. For an `inplace` resumption, the call stores/// `local` as the handler's local state and returns `arg`, which the clause returns to `perform`./// For the other kinds, the call resumes the body as a tail resume, so control does not come back/// to the clause. The call frees an allocated resumption first. The call must be the clause's last/// action, with its value returned.pub fn resumeTail(resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque) ?*anyopaque { if (resume_ptr.kind == .inplace) { @branchHint(.likely); resume_ptr.payload.plocal.* = local; return arg; } var renv: ResumeEnv = .{ .local = local, .result = arg, .unwind = false, }; if (resume_ptr.kind == .scoped_once) { @branchHint(.likely); return mp.resumeTailPrompt(resume_ptr.payload.continuation, &renv); } const mpr = resume_ptr.payload.continuation; mp.processAllocator().destroy(resume_ptr); return mp.resumeTailPrompt(mpr, &renv);}/// Ends an effect resumption for a clause that will not continue the body, such as a failing branch/// of a choice. The call does nothing for null, which an `abort` clause receives. For a `once`/// resumption, and for a `multi` resumption that holds the last reference and never resumed, the/// call resumes the body with an unwind flag. The unwind makes the body's `perform` jump back to/// the handler, and the runtime drops the body's stacklet without running the rest of the body. For/// a `multi` resumption with other references or earlier resumes, the call frees the record and/// gives up one reference. A `scoped_once` or `inplace` resumption fails an assertion in safe/// builds. A `finally` call inside the dropped body never calls its `finally_fun` function.pub fn resumeRelease(resume_ptr: ?*Resume) void { const r = resume_ptr orelse return; if (r.kind == .once) { resumeUnwind(r); return; } assert(r.kind == .multi); const mpr = r.payload.continuation; if (mp.resumeShouldUnwind(mpr) != 0) { resumeUnwind(r); } else { mp.processAllocator().destroy(r); mp.resumeDrop(mpr); }}/// Runs `fun(arg)` under a frame for `effect`, for code that must reach an outer handler of an/// effect past the innermost one. While `fun` runs, a search for a handler of `effect` passes over/// one more handler of that effect. The frame applies only when the search has at least `from`/// handlers of that effect still to pass over when it reaches the frame, so a `from` of 0 always/// applies. The call returns what `fun` returns, and pops the frame on return.pub fn mask(effect: ?Effect, from: usize, fun: ActionFn, arg: ?*anyopaque) ?*anyopaque { var f: MaskFrame = .{ .frame = .{ .effect = mask_effect, .parent = null }, .mask = effect, .from = from, }; pushFrame(&f.frame); defer popFrame(&f.frame); return fun(arg);}/// Runs `fun(arg)`, then calls `finally_fun(local)`, and returns `fun`'s result, so code can run a/// second function after its body returns. `finally_fun` runs only when `fun` returns normally, so/// a body ended by an `abort` or `never` clause, or by `resumeRelease`, skips it.pub fn finally(local: ?*anyopaque, finally_fun: ReleaseFn, fun: ActionFn, arg: ?*anyopaque) ?*anyopaque { var f: FinallyFrame = .{ .frame = .{ .effect = finally_effect, .parent = null }, .fun = finally_fun, .local = local, }; pushFrame(&f.frame); const result = fun(arg); popFrame(&f.frame); f.fun(f.local); return result;}fn pushFrame(f: *Frame) void { f.parent = frame_top; assert(f.parent != f); frame_top = f; clearFindCache();}fn popFrame(f: *Frame) void { assert(frame_top == f); frame_top = f.parent; clearFindCache();}fn clearFindCache() void { find_cache = null;}fn handleStart(prompt: *mp.Prompt, earg: ?*anyopaque) callconv(.c) ?*anyopaque { const env: *HandleStartEnv = @ptrCast(@alignCast(earg.?)); var h: HandleFrame = .{ .frame = .{ .effect = env.hdef.effect, .parent = null }, .prompt = prompt, .hdef = env.hdef, .local = env.local, }; pushFrame(&h.frame); var result = env.body(env.arg); popFrame(&h.frame); if (h.hdef.resultfun) |resultfun| { result = resultfun(h.local, result); } return result;}fn find(optag: *const Optag) ?*HandleFrame { var f = frame_top; const operation_effect = optag.effect; var mask_level: usize = 0; if (find_cache) |cached| { if (cached.frame.effect == operation_effect) { @branchHint(.likely); return cached; } } while (f) |frame| { @branchHint(.likely); const eff = frame.effect; if (eff == operation_effect) { @branchHint(.likely); if (mask_level == 0) return cacheFind(@fieldParentPtr("frame", frame)); mask_level -= 1; } else if (eff == under_effect) { @branchHint(.unlikely); const under: *UnderFrame = @fieldParentPtr("frame", frame); var cursor = frame.parent; while (cursor) |candidate| { if (candidate.effect == under.under) break; cursor = candidate.parent; } f = cursor orelse return null; } else if (eff == mask_effect) { @branchHint(.unlikely); const masked: *MaskFrame = @fieldParentPtr("frame", frame); if (masked.mask == operation_effect and masked.from <= mask_level) { mask_level += 1; } } f = f.?.parent; } return null;}fn cacheFind(h: *HandleFrame) *HandleFrame { find_cache = h; return h;}fn performAt(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque { if (op.opkind == .tail_noop) { @branchHint(.likely); var r: Resume = .{ .kind = .inplace, .payload = .{ .plocal = &h.local }, }; return op.opfun.?(&r, h.local, arg); } if (op.opkind == .tail) { @branchHint(.likely); return performUnder(h, op, arg); } if (op.opkind == .scoped_once) return performYieldTo(.scoped_once, h, op, arg); if (op.opkind == .once) return performYieldTo(.once, h, op, arg); if (op.opkind == .never) return unwindTo(h, op, arg); if (op.opkind == .abort) return performYieldToAbort(h, op, arg); if (op.opkind == .forward) return performForward(h, op, arg); if (op.opkind == .null_op) return null; return performYieldTo(.multi, h, op, arg);}fn performForward(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque { const optag = op.optag orelse return null; var f: MaskFrame = .{ .frame = .{ .effect = mask_effect, .parent = null }, .mask = h.frame.effect, .from = 0, }; pushFrame(&f.frame); defer popFrame(&f.frame); return perform(optag, arg);}fn performUnder(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque { const saved_find_cache = find_cache; var f: UnderFrame = .{ .frame = .{ .effect = under_effect, .parent = null }, .under = h.frame.effect, }; f.frame.parent = frame_top; assert(f.frame.parent != &f.frame); frame_top = &f.frame; clearFindCache(); defer { assert(frame_top == &f.frame); frame_top = f.frame.parent; find_cache = saved_find_cache; } var r: Resume = .{ .kind = .inplace, .payload = .{ .plocal = &h.local }, }; return op.opfun.?(&r, h.local, arg);}fn performYieldTo(rkind: ResumptionKind, h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque { const resume_top = frame_top; frame_top = h.frame.parent; clearFindCache(); var penv: PerformEnv = .{ .rkind = rkind, .opfun = op.opfun.?, .local = h.local, .oparg = arg, }; const result = mp.yieldPrompt(h.prompt, performOpClause, &penv); const renv: *ResumeEnv = @ptrCast(@alignCast(result.?)); h.local = renv.local; assert(frame_top != &h.frame); h.frame.parent = frame_top; frame_top = resume_top; clearFindCache(); if (renv.unwind) return unwindTo(h, &unwind_operation, renv.result); return renv.result;}fn performOpClause(mpr: *mp.Resume, earg: ?*anyopaque) callconv(.c) ?*anyopaque { const env: *PerformEnv = @ptrCast(@alignCast(earg.?)); var stack_resume: Resume = undefined; const r = if (env.rkind == .scoped_once) blk: { @branchHint(.likely); break :blk &stack_resume; } else allocateResume(); r.kind = env.rkind; r.payload.continuation = if (env.rkind == .multi) mp.resumeMulti(mpr) else mpr; return env.opfun(r, env.local, env.oparg);}fn performYieldToAbort(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque { frame_top = h.frame.parent; clearFindCache(); var env: PerformEnv = .{ .rkind = .scoped_once, .opfun = op.opfun.?, .local = h.local, .oparg = arg, }; return mp.yieldPrompt(h.prompt, performOpClauseAbort, &env);}fn performOpClauseAbort(mpr: *mp.Resume, earg: ?*anyopaque) callconv(.c) ?*anyopaque { const env: PerformEnv = (@as(*PerformEnv, @ptrCast(@alignCast(earg.?)))).*; mp.resumeDrop(mpr); return env.opfun(null, env.local, env.oparg);}fn unwindTo(h: *HandleFrame, op: *const Operation, arg: ?*anyopaque) ?*anyopaque { return performYieldToAbort(h, op, arg);}fn handleOpUnwind(_: ?*Resume, _: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { return arg;}const unwind_operation: Operation = .{ .opkind = .abort, .optag = &unwind_optag, .opfun = handleOpUnwind,};fn resumeInternal(final: bool, resume_ptr: *Resume, local: ?*anyopaque, arg: ?*anyopaque, unwind: bool) ?*anyopaque { assert(@backingInt(resume_ptr.kind) >= @backingInt(ResumptionKind.scoped_once)); var renv: ResumeEnv = .{ .local = local, .result = arg, .unwind = unwind, }; switch (resume_ptr.kind) { .scoped_once => { @branchHint(.likely); return mp.resumePrompt(resume_ptr.payload.continuation, &renv); }, .once => { assert(final); const mpr = resume_ptr.payload.continuation; mp.processAllocator().destroy(resume_ptr); return mp.resumePrompt(mpr, &renv); }, .multi => { const mpr = resume_ptr.payload.continuation; if (final) { mp.processAllocator().destroy(resume_ptr); } else { _ = mp.resumeDup(mpr); } return mp.resumePrompt(mpr, &renv); }, .inplace => unreachable, }}fn resumeUnwind(resume_ptr: *Resume) void { _ = resumeInternal(true, resume_ptr, null, null, true);}fn allocateResume() *Resume { return mp.processAllocator().create(Resume) catch std.debug.panic("lib/mpeff: out of memory", .{});}fn unhandledOperation(optag: *const Optag) ?*anyopaque { @branchHint(.cold); pretty.diagnostic.writeStderrText("lib/mpeff: unhandled operation: {s}\n", .{optagName(optag)}); return null;}fn intToPtr(value: isize) ?*anyopaque { if (value == 0) return null; return @ptrFromInt(@as(usize, @intCast(value)));}fn ptrToInt(value: ?*anyopaque) isize { return if (value) |ptr| @intCast(@intFromPtr(ptr)) else 0;}const reader_names = [_:null]?[*:0]const u8{ "reader", "reader/ask", null };const reader_effect: effect_mod.Effect = @ptrCast(&reader_names);const reader_ask_tag: effect_mod.Optag = .{ .effect = reader_effect, .opidx = 0 };fn readerAsk() isize { return ptrToInt(effect_mod.perform(&reader_ask_tag, null));}fn stackUse(kb: usize) isize { if (kb <= 4) return readerAsk(); var page: [4096]u8 = undefined; page[4095] = @truncate(kb); std.mem.doNotOptimizeAway(&page); return stackUse(kb - 4);}fn handleReaderAsk(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return effect_mod.resumeTail(continuation.?, local, local);}fn handleGeneralReaderAsk(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { _ = local; return effect_mod.resumeTail(continuation.?, intToPtr(42), intToPtr(42));}const reader_def = effect_mod.handlerDef(reader_effect, null, &.{ .{ .opkind = .tail_noop, .optag = &reader_ask_tag, .opfun = handleReaderAsk },});const general_reader_def = effect_mod.handlerDef(reader_effect, null, &.{ .{ .opkind = .scoped_once, .optag = &reader_ask_tag, .opfun = handleGeneralReaderAsk },});const forward_reader_def = effect_mod.handlerDef(reader_effect, null, &.{ .{ .opkind = .forward, .optag = &reader_ask_tag, .opfun = null },});fn handleUnderReaderAsk(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return effect_mod.resumeTail(continuation.?, local, intToPtr(readerAsk() + 1));}const under_reader_def = effect_mod.handlerDef(reader_effect, null, &.{ .{ .opkind = .tail, .optag = &reader_ask_tag, .opfun = handleUnderReaderAsk },});fn readerHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&reader_def, intToPtr(init), action, arg);}fn generalReaderHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&general_reader_def, intToPtr(init), action, arg);}fn forwardingReaderHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&forward_reader_def, intToPtr(init), action, arg);}fn underReaderHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&under_reader_def, intToPtr(init), action, arg);}fn readerAction(_: ?*anyopaque) callconv(.c) ?*anyopaque { return intToPtr(stackUse(64) + readerAsk());}test "reader handles tail and scoped-once ask operations" { try std.testing.expectEqual(@as(isize, 84), ptrToInt(readerHandle(readerAction, 42, null))); try std.testing.expectEqual(@as(isize, 84), ptrToInt(generalReaderHandle(readerAction, 99, null)));}fn askOnce(_: ?*anyopaque) callconv(.c) ?*anyopaque { return intToPtr(readerAsk());}fn innerForwardingReader(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return forwardingReaderHandle(askOnce, 99, arg);}test "forward operation skips the current handler and reaches the enclosing handler" { try std.testing.expectEqual(@as(isize, 7), ptrToInt(readerHandle(innerForwardingReader, 7, null)));}fn innerUnderReader(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return underReaderHandle(askOnce, 99, arg);}test "tail operation runs under-frame effects below the current handler" { try std.testing.expectEqual(@as(isize, 8), ptrToInt(readerHandle(innerUnderReader, 7, null)));}const state_names = [_:null]?[*:0]const u8{ "state", "state/get", "state/set", null };const state_effect: effect_mod.Effect = @ptrCast(&state_names);const state_get_tag: effect_mod.Optag = .{ .effect = state_effect, .opidx = 0 };const state_set_tag: effect_mod.Optag = .{ .effect = state_effect, .opidx = 1 };fn rawStateGet() isize { return ptrToInt(effect_mod.perform(&state_get_tag, null));}fn rawStateSet(value: isize) void { _ = effect_mod.perform(&state_set_tag, intToPtr(value));}fn handleStateGet(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return effect_mod.resumeTail(continuation.?, local, local);}fn handleStateSet(continuation: ?*effect_mod.Resume, _: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { return effect_mod.resumeTail(continuation.?, arg, null);}fn stateDef(comptime kind: effect_mod.OperationKind) effect_mod.HandlerDef { return effect_mod.handlerDef(state_effect, null, &.{ .{ .opkind = kind, .optag = &state_get_tag, .opfun = handleStateGet }, .{ .opkind = kind, .optag = &state_set_tag, .opfun = handleStateSet }, });}const tail_state_def = stateDef(.tail_noop);const under_state_def = stateDef(.tail);const once_state_def = stateDef(.scoped_once);const allocated_once_state_def = stateDef(.once);const multi_state_def = stateDef(.multi);fn stateHandle(def: *const effect_mod.HandlerDef, action: effect_mod.ActionFn, init: isize) ?*anyopaque { return effect_mod.handle(def, intToPtr(init), action, null);}fn counterAction(_: ?*anyopaque) callconv(.c) ?*anyopaque { var count: isize = 0; while (true) { const current = rawStateGet(); if (current <= 0) break; rawStateSet(current - 1); count += 1; } return intToPtr(count);}test "state counter works across tail, under, scoped-once, and multi handlers" { try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&tail_state_def, counterAction, 1000))); try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&under_state_def, counterAction, 1000))); try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&once_state_def, counterAction, 1000))); try std.testing.expectEqual(@as(isize, 1000), ptrToInt(stateHandle(&allocated_once_state_def, counterAction, 1000))); try std.testing.expectEqual(@as(isize, 100), ptrToInt(stateHandle(&multi_state_def, counterAction, 100)));}test "state counter covers upstream debug workload sizes" { const count: isize = 100_100; try std.testing.expectEqual(count, ptrToInt(stateHandle(&tail_state_def, counterAction, count))); try std.testing.expectEqual(count, ptrToInt(stateHandle(&under_state_def, counterAction, count))); try std.testing.expectEqual(count, ptrToInt(stateHandle(&once_state_def, counterAction, count))); try std.testing.expectEqual(@divTrunc(count, 10), ptrToInt(stateHandle(&multi_state_def, counterAction, @divTrunc(count, 10))));}fn reader1(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(counterAction, 1, arg);}fn reader2(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader1, 2, arg);}fn reader3(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader2, 3, arg);}fn reader4(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader3, 4, arg);}fn reader5(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader4, 5, arg);}fn reader6(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader5, 6, arg);}fn reader7(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader6, 7, arg);}fn reader8(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader7, 8, arg);}fn reader9(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader8, 9, arg);}fn reader10(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return readerHandle(reader9, 10, arg);}test "state handler composes under nested reader handlers" { try std.testing.expectEqual(@as(isize, 250), ptrToInt(stateHandle(&tail_state_def, reader3, 250))); try std.testing.expectEqual(@as(isize, 250), ptrToInt(stateHandle(&once_state_def, reader3, 250)));}test "state handler composes under ten nested reader handlers" { try std.testing.expectEqual(@as(isize, 100), ptrToInt(stateHandle(&tail_state_def, reader10, 100))); try std.testing.expectEqual(@as(isize, 100), ptrToInt(stateHandle(&once_state_def, reader10, 100)));}fn finallyBody(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return arg;}fn markReleased(local: ?*anyopaque) callconv(.c) void { const released: *bool = @ptrCast(@alignCast(local.?)); released.* = true;}test "finally frame runs release function after normal return" { var released = false; try std.testing.expectEqual(@as(isize, 42), ptrToInt(effect_mod.finally(&released, markReleased, finallyBody, intToPtr(42)))); try std.testing.expect(released);}const StateFn = struct { env: ?*anyopaque, applyFn: *const fn (?*anyopaque, isize) isize, fn apply(self: StateFn, state: isize) isize { return self.applyFn(self.env, state); }};fn stateFnToPtr(function: StateFn) ?*anyopaque { const box = std.testing.allocator.create(StateFn) catch @panic("unable to allocate state function"); box.* = function; return @ptrCast(box);}fn stateFnFromPtr(value: ?*anyopaque) StateFn { const box: *StateFn = @ptrCast(@alignCast(value.?)); const function = box.*; std.testing.allocator.destroy(box); return function;}fn mstateResultValue(env: ?*anyopaque, _: isize) isize { return ptrToInt(env);}fn handleMstateResult(_: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { return stateFnToPtr(.{ .env = arg, .applyFn = mstateResultValue });}fn mstateGetValue(env: ?*anyopaque, state: isize) isize { const continuation: *effect_mod.Resume = @ptrCast(@alignCast(env.?)); const function = stateFnFromPtr(effect_mod.resumeFinal(continuation, null, intToPtr(state))); return function.apply(state);}fn handleMstateGet(continuation: ?*effect_mod.Resume, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return stateFnToPtr(.{ .env = @ptrCast(continuation.?), .applyFn = mstateGetValue, });}const PutEnv = struct { new_state: isize, continuation: *effect_mod.Resume,};fn mstatePutValue(env_ptr: ?*anyopaque, _: isize) isize { const env: *PutEnv = @ptrCast(@alignCast(env_ptr.?)); const new_state = env.new_state; const continuation = env.continuation; std.testing.allocator.destroy(env); const function = stateFnFromPtr(effect_mod.resumeFinal(continuation, null, null)); return function.apply(new_state);}fn handleMstateSet(continuation: ?*effect_mod.Resume, _: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { const env = std.testing.allocator.create(PutEnv) catch @panic("unable to allocate monadic state closure"); env.* = .{ .new_state = ptrToInt(arg), .continuation = continuation.?, }; return stateFnToPtr(.{ .env = @ptrCast(env), .applyFn = mstatePutValue, });}const mstate_def = effect_mod.handlerDef(state_effect, handleMstateResult, &.{ .{ .opkind = .once, .optag = &state_get_tag, .opfun = handleMstateGet }, .{ .opkind = .once, .optag = &state_set_tag, .opfun = handleMstateSet },});fn mstateHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque { const function = stateFnFromPtr(effect_mod.handle(&mstate_def, null, action, arg)); return intToPtr(function.apply(init));}test "monadic state handler defers state threading through once resumptions" { try std.testing.expectEqual(@as(isize, 200), ptrToInt(mstateHandle(counterAction, 200, null)));}const exit_names = [_:null]?[*:0]const u8{ "exit", "exit/capture", null };const exit_effect: effect_mod.Effect = @ptrCast(&exit_names);const exit_capture_tag: effect_mod.Optag = .{ .effect = exit_effect, .opidx = 0 };fn exitCapture() ?*anyopaque { return effect_mod.perform(&exit_capture_tag, null);}fn handleExitCapture(continuation: ?*effect_mod.Resume, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return @ptrCast(continuation.?);}const exit_def = effect_mod.handlerDef(exit_effect, null, &.{ .{ .opkind = .once, .optag = &exit_capture_tag, .opfun = handleExitCapture },});fn exitHandle(action: effect_mod.ActionFn, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&exit_def, null, action, arg);}fn rehandleBody(_: ?*anyopaque) callconv(.c) ?*anyopaque { const first = readerAsk(); _ = exitCapture(); const second = readerAsk(); return intToPtr(first + second);}fn withExitHandle(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return exitHandle(rehandleBody, arg);}fn withResume(arg: ?*anyopaque) callconv(.c) ?*anyopaque { const continuation: *effect_mod.Resume = @ptrCast(@alignCast(arg.?)); return effect_mod.resumeFinal(continuation, null, null);}test "captured effect continuation can be resumed under a different handler" { const captured = readerHandle(withExitHandle, 1, null); try std.testing.expectEqual(@as(isize, 3), ptrToInt(readerHandle(withResume, 2, captured)));}const amb_names = [_:null]?[*:0]const u8{ "amb", "amb/flip", null };const amb_effect: effect_mod.Effect = @ptrCast(&amb_names);const amb_flip_tag: effect_mod.Optag = .{ .effect = amb_effect, .opidx = 0 };fn ambFlip() bool { return ptrToInt(effect_mod.perform(&amb_flip_tag, null)) != 0;}fn rawAmbBody(_: ?*anyopaque) callconv(.c) ?*anyopaque { return intToPtr(if (ambFlip()) 10 else 1);}fn handleAmbFlip(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { const false_branch = ptrToInt(effect_mod.resumeEffect(continuation.?, local, intToPtr(0))); const true_branch = ptrToInt(effect_mod.resumeFinal(continuation.?, local, intToPtr(1))); return intToPtr(false_branch + true_branch);}const amb_def = effect_mod.handlerDef(amb_effect, null, &.{ .{ .opkind = .scoped, .optag = &amb_flip_tag, .opfun = handleAmbFlip },});test "scoped multi-shot handler can resume both branches" { try std.testing.expectEqual(@as(isize, 11), ptrToInt(effect_mod.handle(&amb_def, null, rawAmbBody, null)));}fn handleAmbCountResult(_: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return intToPtr(1);}fn handleAmbCountFlip(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { const false_branch = ptrToInt(effect_mod.resumeEffect(continuation.?, local, intToPtr(0))); const true_branch = ptrToInt(effect_mod.resumeFinal(continuation.?, local, intToPtr(1))); return intToPtr(false_branch + true_branch);}const amb_count_def = effect_mod.handlerDef(amb_effect, handleAmbCountResult, &.{ .{ .opkind = .scoped, .optag = &amb_flip_tag, .opfun = handleAmbCountFlip },});fn ambCountHandle(action: effect_mod.ActionFn, arg: ?*anyopaque) isize { return ptrToInt(effect_mod.handle(&amb_count_def, null, action, arg));}fn xorAction(_: ?*anyopaque) callconv(.c) ?*anyopaque { const x = ambFlip(); const y = ambFlip(); return intToPtr(if (x != y) 1 else 0);}test "ambiguity handler enumerates both boolean branches" { try std.testing.expectEqual(@as(isize, 4), ambCountHandle(xorAction, null));}const choice_names = [_:null]?[*:0]const u8{ "choice", "choice/choose", "choice/fail", null };const choice_effect: effect_mod.Effect = @ptrCast(&choice_names);const choice_choose_tag: effect_mod.Optag = .{ .effect = choice_effect, .opidx = 0 };const choice_fail_tag: effect_mod.Optag = .{ .effect = choice_effect, .opidx = 1 };fn choiceChoose(max: isize) isize { return ptrToInt(effect_mod.perform(&choice_choose_tag, intToPtr(max)));}fn choiceFail() void { _ = effect_mod.perform(&choice_fail_tag, null);}fn choiceBody(_: ?*anyopaque) callconv(.c) ?*anyopaque { const chosen = choiceChoose(4); if (@rem(chosen, 2) == 0) return intToPtr(chosen); choiceFail(); return intToPtr(99);}fn handleChoiceChoose(continuation: ?*effect_mod.Resume, local: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { const max = ptrToInt(arg); var total: isize = 0; var i: isize = 1; while (i <= max) : (i += 1) { const result = if (i == max) effect_mod.resumeFinal(continuation.?, local, intToPtr(i)) else effect_mod.resumeEffect(continuation.?, local, intToPtr(i)); total += ptrToInt(result); } return intToPtr(total);}fn handleChoiceFail(continuation: ?*effect_mod.Resume, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { effect_mod.resumeRelease(continuation); return intToPtr(0);}const choice_def = effect_mod.handlerDef(choice_effect, null, &.{ .{ .opkind = .scoped, .optag = &choice_choose_tag, .opfun = handleChoiceChoose }, .{ .opkind = .abort, .optag = &choice_fail_tag, .opfun = handleChoiceFail },});test "choice handler combines resumed branches and aborts failed branches" { try std.testing.expectEqual(@as(isize, 6), ptrToInt(effect_mod.handle(&choice_def, null, choiceBody, null)));}fn ambStateXor() bool { const x = ambFlip(); const y = ambFlip(); return x != y;}fn ambStateFoo(_: ?*anyopaque) callconv(.c) ?*anyopaque { const p = ambFlip(); const current = rawStateGet(); rawStateSet(current + 1); const result = if (current > 0 and p) ambStateXor() else false; return intToPtr(if (result) 1 else 0);}fn stateInsideAmb(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return stateHandle(&tail_state_def, ambStateFoo, ptrToInt(arg));}fn ambInsideState(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return intToPtr(ambCountHandle(ambStateFoo, arg));}test "state and ambiguity handlers compose in both nesting orders" { try std.testing.expectEqual(@as(isize, 2), ambCountHandle(stateInsideAmb, intToPtr(0))); try std.testing.expectEqual(@as(isize, 5), ptrToInt(stateHandle(&tail_state_def, ambInsideState, 0)));}fn choiceCountBody(_: ?*anyopaque) callconv(.c) ?*anyopaque { return intToPtr(1);}fn queenSafe(queen: isize, queens: []const isize) bool { var diag: isize = 1; var i = queens.len; while (i > 0) { i -= 1; const previous = queens[i]; if (queen == previous or queen == previous + diag or queen == previous - diag) return false; diag += 1; } return true;}fn findQueens(n: isize, col: usize, queens: *[12]isize) bool { if (col == 0) return true; if (!findQueens(n, col - 1, queens)) return false; const queen = choiceChoose(n); const placed = queens[0 .. col - 1]; if (!queenSafe(queen, placed)) { choiceFail(); return false; } queens[col - 1] = queen; return true;}fn nqueensBody(arg: ?*anyopaque) callconv(.c) ?*anyopaque { const n: usize = @intCast(ptrToInt(arg)); var queens = @as([12]isize, @splat(0)); return intToPtr(if (findQueens(@intCast(n), n, &queens)) 1 else 0);}test "choice handler counts n-queens solutions" { try std.testing.expectEqual(@as(isize, 92), ptrToInt(effect_mod.handle(&choice_def, null, nqueensBody, intToPtr(8))));}const yield_names = [_:null]?[*:0]const u8{ "yield", "yield/yield", null };const yield_effect: effect_mod.Effect = @ptrCast(&yield_names);const yield_yield_tag: effect_mod.Optag = .{ .effect = yield_effect, .opidx = 0 };fn yieldValue(value: isize) void { _ = effect_mod.perform(&yield_yield_tag, intToPtr(value));}fn handleYieldResult(local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return local;}fn handleYieldYield(continuation: ?*effect_mod.Resume, local: ?*anyopaque, _: ?*anyopaque) callconv(.c) ?*anyopaque { return effect_mod.resumeTail(continuation.?, intToPtr(ptrToInt(local) + 1), local);}const yield_def = effect_mod.handlerDef(yield_effect, handleYieldResult, &.{ .{ .opkind = .tail_noop, .optag = &yield_yield_tag, .opfun = handleYieldYield },});fn yieldHandle(action: effect_mod.ActionFn, init: isize, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&yield_def, intToPtr(init), action, arg);}fn handleChoiceIgnoreResult(_: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { return arg;}fn handleChoiceChooseIgnore(continuation: ?*effect_mod.Resume, local: ?*anyopaque, arg: ?*anyopaque) callconv(.c) ?*anyopaque { const max = ptrToInt(arg); if (max <= 0) return intToPtr(0); var i: isize = 1; while (i <= max) : (i += 1) { _ = if (i == max) effect_mod.resumeFinal(continuation.?, local, intToPtr(i)) else effect_mod.resumeEffect(continuation.?, local, intToPtr(i)); } return intToPtr(0);}const choice_ignore_def = effect_mod.handlerDef(choice_effect, handleChoiceIgnoreResult, &.{ .{ .opkind = .scoped, .optag = &choice_choose_tag, .opfun = handleChoiceChooseIgnore }, .{ .opkind = .abort, .optag = &choice_fail_tag, .opfun = handleChoiceFail },});fn choiceIgnoreHandle(action: effect_mod.ActionFn, arg: ?*anyopaque) ?*anyopaque { return effect_mod.handle(&choice_ignore_def, null, action, arg);}fn triples(n: isize, sum: isize) void { const x = choiceChoose(n); const y = choiceChoose(x - 1); const z = choiceChoose(y - 1); if (x + y + z == sum) { yieldValue(x); } else { choiceFail(); }}fn triplesBody(arg: ?*anyopaque) callconv(.c) ?*anyopaque { const payload = ptrToInt(arg); const n = @divTrunc(payload, 1 << 16); const sum = @mod(payload, 1 << 16); triples(n, sum); return intToPtr(0);}fn chooseTriples(arg: ?*anyopaque) callconv(.c) ?*anyopaque { return choiceIgnoreHandle(triplesBody, arg);}test "choice and yield handlers count matching triples" { const payload = 100 * (1 << 16) + 27; try std.testing.expectEqual(@as(isize, 48), ptrToInt(yieldHandle(chooseTriples, 0, intToPtr(payload))));}const Reader = mp.EffectDefinition(.{ .name = "typed-reader", .operations = .{ .ask = mp.operation(void, isize), },});const ReaderContext = struct { value: isize,};fn askReader(context: *ReaderContext, _: void) isize { return context.value;}fn readerBody(_: *ReaderContext) isize { return Reader.performWithoutValue(.ask) + Reader.performWithoutValue(.ask);}test "typed effect handler answers tail operations without pointer casts" { var context: ReaderContext = .{ .value = 21 }; try std.testing.expectEqual(@as(isize, 42), Reader.handle(isize, &context, readerBody, .{ .ask = mp.on(.tail_noop, askReader), }));}test "typed effect lookup follows handler stack changes" { var first: ReaderContext = .{ .value = 3 }; var second: ReaderContext = .{ .value = 11 }; try std.testing.expectEqual(@as(isize, 6), Reader.handle(isize, &first, readerBody, .{ .ask = mp.on(.tail_noop, askReader), })); try std.testing.expectEqual(@as(isize, 22), Reader.handle(isize, &second, readerBody, .{ .ask = mp.on(.tail_noop, askReader), }));}fn typedAskOnce(_: *ReaderContext) isize { return Reader.performWithoutValue(.ask);}fn typedInnerForward(_: *ReaderContext) isize { var inner: ReaderContext = .{ .value = 99 }; return Reader.handle(isize, &inner, typedAskOnce, .{ .ask = mp.forward(), });}test "typed effect handler can forward an operation to an enclosing handler" { var outer: ReaderContext = .{ .value = 7 }; try std.testing.expectEqual(@as(isize, 7), Reader.handle(isize, &outer, typedInnerForward, .{ .ask = mp.on(.tail_noop, askReader), }));}const State = mp.EffectDefinition(.{ .name = "typed-state", .operations = .{ .get = mp.operation(void, isize), .set = mp.operation(isize, void), },});const StateContext = struct { current: isize,};fn stateGet(context: *StateContext, _: void) isize { return context.current;}fn stateSet(context: *StateContext, value: isize) void { context.current = value;}fn stateCounter(_: *StateContext) isize { var count: isize = 0; while (true) { const current = State.performWithoutValue(.get); if (current <= 0) break; State.perform(.set, current - 1); count += 1; } return count;}test "typed effect handler supports mutable Zig context" { var context: StateContext = .{ .current = 100 }; const count = State.handle(isize, &context, stateCounter, .{ .get = mp.on(.tail_noop, stateGet), .set = mp.on(.tail_noop, stateSet), }); try std.testing.expectEqual(@as(isize, 100), count); try std.testing.expectEqual(@as(isize, 0), context.current);}const Amb = mp.EffectDefinition(.{ .name = "typed-amb", .operations = .{ .flip = mp.operation(void, bool), },});const AmbContext = struct {};fn ambBody(_: *AmbContext) isize { return if (Amb.performWithoutValue(.flip)) 10 else 1;}fn handleFlip( continuation: Amb.Continuation(.flip, .scoped, isize), _: *AmbContext, _: void,) isize { const false_branch = continuation.continueWith(false); const true_branch = continuation.continueFinalWith(true); return false_branch + true_branch;}test "typed effect handler exposes scoped multi-shot continuations" { var context: AmbContext = .{}; try std.testing.expectEqual(@as(isize, 11), Amb.handle(isize, &context, ambBody, .{ .flip = mp.on(.scoped, handleFlip), }));}const Once = mp.EffectDefinition(.{ .name = "typed-once", .operations = .{ .bump = mp.operation(isize, isize), },});const OnceContext = struct { seen: isize = 0,};fn onceBody(_: *OnceContext) isize { return Once.perform(.bump, 41) + 1;}fn handleBump( continuation: Once.Continuation(.bump, .once, isize), context: *OnceContext, value: isize,) isize { context.seen = value; return continuation.continueFinalWith(value + 1);}test "typed once handlers use final continuations" { var context: OnceContext = .{}; try std.testing.expectEqual(@as(isize, 43), Once.handle(isize, &context, onceBody, .{ .bump = mp.on(.once, handleBump), })); try std.testing.expectEqual(@as(isize, 41), context.seen);}const Pure = mp.EffectDefinition(.{ .name = "typed-pure", .operations = .{ .answer = mp.operation(void, isize), .double = mp.operation(isize, isize), },});fn pureAnswer() isize { return 21;}fn pureDouble(value: isize) isize { return value * 2;}fn pureBody() isize { return Pure.performWithoutValue(.answer) + Pure.perform(.double, 10);}test "typed effect handler supports no-context bodies and clauses" { try std.testing.expectEqual(@as(isize, 41), Pure.handleWithoutContext(isize, pureBody, .{ .answer = mp.on(.tail_noop, pureAnswer), .double = mp.on(.tail_noop, pureDouble), }));}const FallibleReader = mp.EffectDefinition(.{ .name = "typed-fallible-reader", .operations = .{ .ask = mp.operation(void, error{Unavailable}!isize), },});const FallibleReaderContext = struct { available: bool, value: isize,};fn fallibleAsk(context: *FallibleReaderContext, _: void) error{Unavailable}!isize { if (!context.available) return error.Unavailable; return context.value;}fn fallibleBody(_: *FallibleReaderContext) error{Unavailable}!isize { const value = try FallibleReader.performWithoutValue(.ask); return value + 1;}test "typed effect handler preserves Zig error unions" { var unavailable: FallibleReaderContext = .{ .available = false, .value = 0 }; try std.testing.expectError(error.Unavailable, FallibleReader.handle(error{Unavailable}!isize, &unavailable, fallibleBody, .{ .ask = mp.on(.tail_noop, fallibleAsk), })); var available: FallibleReaderContext = .{ .available = true, .value = 41 }; try std.testing.expectEqual(@as(isize, 42), try FallibleReader.handle(error{Unavailable}!isize, &available, fallibleBody, .{ .ask = mp.on(.tail_noop, fallibleAsk), }));}const Choice = mp.EffectDefinition(.{ .name = "typed-choice", .operations = .{ .flip = mp.operation(void, bool), },});fn chooseBody() isize { return if (Choice.performWithoutValue(.flip)) 30 else 4;}fn chooseBoth(continuation: Choice.Continuation(.flip, .scoped, isize)) isize { const false_branch = continuation.continueWith(false); const true_branch = continuation.continueFinalWith(true); return false_branch + true_branch;}test "typed scoped continuations can omit unused context and argument" { try std.testing.expectEqual(@as(isize, 34), Choice.handleWithoutContext(isize, chooseBody, .{ .flip = mp.on(.scoped, chooseBoth), }));}Source: lib/mprompt/src/root.zig:55
zig
pub const effect = @import("effect.zig");Audit
| Definitions | 28 |
|---|---|
| Public names | 28 |
| Members | 26 |
| Version | 26.7.0 |
| Revision | daab053ee433 |