#+TITLE: Conditions and restarts — a cheatsheet #+STARTUP: showeverything Everything here has been run. The refusal messages are verbatim, not paraphrased. The reference is [[file:spec-conditions.md][spec-conditions.md]]; this is how to drive what is built, and [[file:NEXT.md][NEXT.md]] says why each piece is shaped the way it is. * What exists | Operator | Type | State | |---------------------------+---------+----------------------------------------| | ~(signal c)~ | Unit | works — §1, §2 | | ~(error c)~ | Never | works — §2, stops if nothing transfers | | ~(handler-bind [...] ...)~ | Unit | works — §1 | | ~(restart-case B (n [] C))~ | B's type | works — §3, §4, §6 | | ~(invoke-restart 'n)~ | Never | works — §4, §5, §6 | | ~handler-case~ | | refused by name | | ~find-restart~ | | refused by name — §4 | | ~compute-restarts~ | | refused by name — §4 | | restarts with parameters | | refused by name — §3 | | the dev-build break loop | | not written — §2 | * Setup #+begin_src sh dune build ./_build/default/bin/main.exe run conditions-play.flan # prints, then waits #+end_src To poke at it live, which is the point of the whole thing: #+begin_src sh ./_build/default/bin/main.exe dev conditions-play.flan #+end_src then in Emacs, with =emacs/flan-mode.el= and =emacs/flan-dev.el= loaded: | ~C-c C-z~ | connect (finds =.flan-dev.sock= upward) | | ~C-c C-c~ | recompile the top-level form at point and install it | | ~C-c C-k~ | the whole buffer, as one module | | ~C-x C-e~ | evaluate the expression before point *in the running program* | | ~C-c C-o~ | the program's own output | | ~C-c C-d~ | what it currently defines | Editing ~probe~ or ~fetch~ and hitting ~C-c C-c~ changes what the *next* loop of ~run-once~ does, without restarting. That includes adding a ~handler-bind~ or a ~restart-case~ to a body that had none. * The five things you can do ** Signal, and carry on ~signal~ returns ~Unit~ whatever it finds. A handler that returns normally leaves the signalling function to carry on, and with nothing matching it is a no-op — not an abort, not a message. This is the accumulation case. #+begin_src lisp (defstruct AssetMissing [id i32]) (defvar seen i64) (handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))] (load-all)) ; signals twice, keeps going both times #+end_src Matching is by *type*, and there is no hierarchy — so a clause names one struct and nothing else reaches it. Nesting does not displace: an inner ~handler-bind~ and an outer one both run, innermost first. ** Error, which has to be answered Same walk, but a handler that returns normally has not answered it. Only a transfer gets past. #+begin_src lisp (defn strict [n i32] i32 (restart-case (do (error (AssetMissing {:id n})) 0) ; unreachable: error is Never (use-placeholder [] -2))) #+end_src Unanswered, the program stops: #+begin_example handler ran unhandled AssetMissing $ echo $? 134 #+end_example Because ~error~ is ~Never~ it unifies with anything, so it is also what you put on a ~restart-case~ body's fall-through path when there is nothing sensible to return. ~(exit 1)~ works there too. ** Offer restarts Every clause body *and* the body have the same type, and that is the type of the whole form. So the fall-through — what happens when nothing transfers — is visible in the source, which is the price of ~signal~ returning ~Unit~. #+begin_src lisp (defn fetch [n i32] i32 (restart-case (middle n) ; its value if nothing transfers (use-placeholder [] -1) (retry [] 7))) #+end_src ** Transfer #+begin_src lisp (handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))] (fetch 2)) ; -1 #+end_src The name is a *quoted symbol* and is resolved on the restart stack at run time. Lookup walks innermost outward and takes the first frame offering the name, so an inner ~restart-case~ shadows an outer one — and the clause yields to *its own* continuation, not to the call that signalled. ** Clean up on the way out ~defer~ forms in every frame between the invoke and the target run, innermost first, before the clause body starts. #+begin_src lisp (defn middle [n i32] i32 (defer (set ticks (+ ticks 1))) ; runs whether it returns or transfers (+ (probe n) 1)) #+end_src ~errdefer~ does *not* run — a restart is a chosen recovery, not a failure. It is moot today: ~try~/~Result~ is still refused by name, so nothing can run. * Runtime errors, verbatim #+begin_example file.flan:3:25: no restart named nope is active file.flan:4:7: a defer invoked a restart, which a defer may not do — it is the cleanup a transfer runs on its way out unhandled AssetMissing #+end_example All three exit 134, like every other trap. * What is refused, and the exact reason Each of these is rejected by name rather than left to mean something else — the house rule that anything binding a name, altering control flow, or not yet implemented must be recognised explicitly. #+begin_example (signal 1) a condition is a struct, not i32 — matching is by type and there is no condition hierarchy (let [n 0] (handler-bind [(C [c] (set n 1))] ...)) a handler cannot see n: it is a local of the function that established the handler, and a handler runs from wherever the signal was. Use a global, or pass it on the condition. (handler-bind [(C [c] ...)] (return 1)) return is not allowed inside handler-bind yet — the frames it established are popped on the way out and an early exit would leave them on the stack (restart-case (return 1) (skip [] 2)) return is not allowed inside restart-case yet — ... (restart-case 1 (skip [] 2) (skip [] 3)) this restart-case offers skip twice (restart-case 1 (skip [n i32] n)) a restart takes no parameters yet — spec-conditions.md §3 has them, and they need argument marshalling and a runtime arity check that this version does not do (invoke-restart skip) invoke-restart takes a quoted restart name, as in (invoke-restart 'use-placeholder) (invoke-restart 'skip 1) a restart takes no arguments yet — ... (defer (invoke-restart 'skip)) invoke-restart is not allowed inside a defer — a defer is the cleanup a transfer runs on its way out, so starting one there would leave this function's defers half run with two targets and no way to choose (handler-case 1) (find-restart 'skip) (compute-restarts) ... is not implemented yet (see the build sequence in plan.org) #+end_example * Gotchas - *A handler cannot close over anything.* It is lifted into a function of its own, because it runs from wherever the signal was. Accumulate into a global or put the value on the condition. Real capture is a closure with an explicit environment — milestone 5. - *Invoking a restart re-runs whatever is between it and the target.* Control resumes at the ~restart-case~, so side effects performed after it and before the signal happen again on a ~retry~. Put the ~restart-case~ at a point where re-entry is safe — that is what "restarts go at the resync point" means. - *An unknown restart name is a hard stop*, not a fallback. There is no ~find-restart~ yet, so a handler cannot test before committing. - *No supertype.* Nothing can say "any condition", so there is no generic logging handler and no catch-all. - *~signal~ cannot hand a value back.* Deliberate — §1 rejected it, because it would force every signal site to declare a default and a result type. * Under the hood, in one paragraph Transfer is lowered explicitly, never by platform unwinding: wasm32 cannot unwind, and a ~cmp~/~jne~ after a call reads like ordinary code. Every Flan signature carries one extra ~ptr~ — the transfer channel — written by an ~invoke-restart~ and checked after every call. What it carries is the *address* of the restart frame, which is an ~alloca~ in the function offering it, so the aim is exact and re-entering a ~restart-case~ needs nothing extra. Each region that established frames gets a landing block that pops them and either catches the transfer or forwards it outward; the function's own runs its defers and returns early. A foreign frame cannot be crossed — the one exception is ~flan_signal~ itself, which threads the channel through so a handler can transfer at all.