Handler dispatch: deep, self-masking, by frame

The effects design states the discipline — handlers are deep, self-masking, and there is no resume. This is the runtime mechanism that realises it, in HandlerStack (types/value.rs) and the dispatch in runtime/command_call.rs.

Handlers intercept external command names only. When a head resolves (via classification) to neither a lexical binding nor a builtin, command_call consults the handler stack before falling through to external lookup. Lexical bindings, builtins, and the prelude are not shell aliases and cannot be intercepted.

Lookup is two innermost-first passes. HandlerStack::lookup(name) scans all frames innermost-first for an explicit per-name entry; only if none exists anywhere does a second innermost-first pass take the first catch-all (handler:). It returns the matched entry with a depth — the count of frames from the top down to and including the match.

The calling convention is fixed by surface form, not value shape. A per-name entry (and every alias) is a unary lambda { |args| … }, applied to the command’s argument list; a catch-all is a binary lambda { |name args| … }, applied to the name and the argument list. Dispatch invokes the matched entry under the convention its install site demands rather than inspecting the stored value; a bare block, a non-lambda, or a wrong-arity lambda is rejected at install time (docs/SPEC.md §3.2), so no malformed entry ever reaches lookup.

Self-masking is a strip-and-restore of one frame. For the dynamic extent of the matched handler’s body, strip_matched(depth) lifts only that frame off the stack. Frames newer or older than it stay in place, so outer handlers for other names remain visible, and a same-name call inside the body reaches the next outer handler — or the OS — never itself. restore_matched re-inserts the frame afterwards, using the monotonically allocated FrameHandle to find the position that preserves the original ordering even if the body pushed frames of its own. This is what makes the wrap-and-forward idiom within [handlers: [git: { |args| my-git ...$args }]] { … } terminate rather than diverge.

Deep means the frame persists across the body’s whole extent, re-reached by each successive operation — a consequence of the stack living on the dynamic frame Context and not being consumed at first use. Frames are pushed by the within guard (evaluator/scope.rs); at an IPC boundary a deserialised stack is rebuilt with fresh handles, since the wire format does not carry them.

See also effects-handlers, handlers-deep-self-masking; map shell-state. docs/SPEC.md §3.2.