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.
This commit is contained in:
parent
b50e8d6cad
commit
ca954a47b5
235
conditions.org
235
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user