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 }