From ca954a47b59190a83ab6b66172b87fccbbf82632 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 09:22:44 +0700 Subject: [PATCH] Shorter The cheatsheet was a document. It should fit on a screen: the syntax, what is missing, the traps that are not in any error message, and the three ways it stops. What was cut is either in spec-conditions.md or in the compiler's own refusal, and both say it better. --- conditions.org | 235 +++++++------------------------------------------ 1 file changed, 34 insertions(+), 201 deletions(-) diff --git a/conditions.org b/conditions.org index c6d486a..0300423 100644 --- a/conditions.org +++ b/conditions.org @@ -1,224 +1,57 @@ -#+TITLE: Conditions and restarts — a cheatsheet +#+TITLE: Conditions and restarts — 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. +Why it is shaped this way: [[file:spec-conditions.md][spec-conditions.md]]. Something to poke at: +=conditions-play.flan= — =flan dev conditions-play.flan=, then ~C-c C-c~. -* 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. +* Works #+begin_src lisp -(defstruct AssetMissing [id i32]) -(defvar seen i64) +(signal c) ; Unit. Handler returns -> carry on. No handler -> no-op. +(error c) ; Never. Only a transfer gets past; else the program stops. -(handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))] - (load-all)) ; signals twice, keeps going both times +(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy + +(restart-case BODY ; BODY and every clause have the same type = the form's + (name [] CLAUSE) ...) + +(invoke-restart 'name) ; Never. Innermost frame offering the name wins. #+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 + (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 + (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. +~defer~ between the invoke and the target runs, innermost first, before the +clause body. ~errdefer~ does not. -** Clean up on the way out +* Not yet -~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 +~handler-case~ · ~find-restart~ · ~compute-restarts~ · restarts with +parameters · the dev-build break loop. Each refused by name with its reason. * 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. +- *A handler closes over nothing.* It is lifted into its own function. + Accumulate into a global, or put the value on the condition. +- *A restart re-runs whatever sits between it and the target.* Control resumes + at the ~restart-case~, so a ~retry~ repeats side effects after it. Put the + ~restart-case~ where re-entry is safe. +- *An unknown restart name is a hard stop.* No ~find-restart~ to test with. +- *No supertype*, so nothing can say "any condition". +- *~signal~ cannot hand a value back.* Deliberate (§1). +- A condition must be a *struct*. ~return~ is refused inside either form. -- *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. +* Stops the program, exit 134 -- *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. +#+begin_example +unhandled AssetMissing +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 — ... +#+end_example