NEXT.md prepends, so its second section still opened with "Read this first" while
pinning a tree six lanes behind and listing as live a queue most of which has
landed. It keeps its contents; what it gains is the sentence saying when it
stopped being where the tree is, and the survey count it quotes measured again on
this tree -- 103 MATCH, 0 DIFFER, 38 skipped.
The two generics entries get the file's own strikethrough treatment, which their
siblings already had: the {K V} catch resolved exactly as it predicted it would,
and there are five predicates rather than four.
Also here: conditions.org gains ArithError beside BoundsError, with the split
between the two runtime conditions that offer retry and the two that deliberately
offer nothing; and tast.ml's header stops naming a tree-walking interpreter among
the backends that consume the typed IR.
139 lines
6.6 KiB
Org Mode
139 lines
6.6 KiB
Org Mode
#+TITLE: Conditions and restarts — cheatsheet
|
|
#+STARTUP: showeverything
|
|
|
|
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~.
|
|
|
|
* Works
|
|
|
|
#+begin_src lisp
|
|
(signal c) ; (). Handler returns -> carry on. No handler -> no-op.
|
|
(error c) ; Never. Only a transfer gets past; else the program stops.
|
|
|
|
(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 [p T ...] CLAUSE) ...)
|
|
|
|
(invoke-restart 'name arg ...) ; Never. Innermost frame offering the name wins.
|
|
#+end_src
|
|
|
|
#+begin_src lisp
|
|
(defn supplied [n i32] i32
|
|
(restart-case (middle n)
|
|
(use-value [v i32] (* v 2)) ; the answer comes from outside
|
|
(retry [] 7)))
|
|
|
|
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
|
|
(supplied 7)) ; 42
|
|
#+end_src
|
|
|
|
A clause's parameters are slots of the function that wrote it, and the invoker
|
|
fills a buffer that function owns — by the time a clause runs, the invoking
|
|
frame has gone. What a clause takes is compared with what was given at *run
|
|
time*, count then spelling, because a restart is found by name on a dynamic
|
|
stack and neither end can see the other.
|
|
|
|
#+begin_src lisp
|
|
(defn fetch [n i32] i32
|
|
(restart-case (middle n) ; its value if nothing transfers
|
|
(use-placeholder [] -1)
|
|
(retry [] 7)))
|
|
|
|
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
|
|
(fetch 2)) ; -1
|
|
#+end_src
|
|
|
|
~defer~ between the invoke and the target runs, innermost first, before the
|
|
clause body. (~errdefer~ would not, and is in *Not yet* below — it is refused by
|
|
name today.)
|
|
|
|
* The break loop
|
|
|
|
An unhandled ~error~ in a dev build stops on the frame that erred, with nothing
|
|
unwound, and waits. ~C-c C-b~ in Emacs lists what is on offer and resumes into
|
|
the choice; ~flan:stopped(Missing)~ in the modeline says it happened.
|
|
|
|
The list is *numbered*, and the number is what is chosen. Two frames offering
|
|
~retry~ both appear and §4's by-name walk can only ever reach the first, so a
|
|
name cannot say which one is meant — ~restart-at~ can.
|
|
|
|
A restart below the evaluation a break is inside is listed, marked, and
|
|
refused: ~C-x C-e~ runs its thunk through a C frame that holds its own transfer
|
|
channel, so an unwind aimed past it would stop at the thunk. Choose one offered
|
|
above it, or ~abort~.
|
|
|
|
* Not yet
|
|
|
|
~handler-case~ · ~find-restart~ · ~compute-restarts~ · ~errdefer~ · a clause's
|
|
report string. Each refused by name with its reason.
|
|
|
|
A restart *with parameters* cannot be taken from the break loop: it aims at a
|
|
frame by position and has nothing to fill the parameters in with, so the clause
|
|
stops the program rather than running on values no one supplied. Choose one
|
|
that takes none, or ~abort~.
|
|
|
|
~find-restart~ and ~compute-restarts~ are blocked on a type rather than on
|
|
effort: §4 gives them ~(Option Restart)~ and a list, and there is no ~Restart~
|
|
type and no list to return one in. The break loop reads the same stack through
|
|
the agent's socket instead.
|
|
|
|
* Gotchas
|
|
|
|
- *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 is not a transaction.* Control resumes at the ~restart-case~ and
|
|
runs forward from there, so a ~retry~ repeats every side effect between it
|
|
and the target. Nothing rolls back: a global the frame already set stays set,
|
|
and gets set again. Common Lisp has exactly this property and offers no help
|
|
either — restarts are not transactional there and are not here.
|
|
|
|
So *you* choose where the retry boundary is. A ~restart-case~ at the top of a
|
|
frame re-runs everything, mutations included; one placed after the mutations
|
|
re-runs only what follows. Put the restart before anything mutates, make the
|
|
retried section idempotent, or snapshot what will be re-applied.
|
|
~test/programs/frame-rollback.flan~ is the worked example of the snapshot,
|
|
including the ordering that matters: restore *in the restart clause*, not in a
|
|
~defer~ — a defer runs on the ordinary return path too, so that version silently
|
|
rolls back the frames that succeeded.
|
|
|
|
This bites harder here than in most Lisps because the point is a *game loop*
|
|
— skip the frame, carry on, don't die. Since a bad index signals
|
|
~BoundsError~ rather than ending the process, abandoning a frame and retrying
|
|
it is a real thing to do, and a non-idempotent mutation is what makes it go
|
|
wrong.
|
|
- *Four conditions come from below your program*, all with ~error~:
|
|
~StorageExhausted~ when an allocator cannot satisfy a request, ~FileError~
|
|
when a file operation fails, ~BoundsError~ for an index or slice outside its
|
|
container, and ~ArithError~ for arithmetic with no answer — a divide or
|
|
remainder by zero, ~INT64_MIN / -1~, and a float-to-integer cast that does not
|
|
fit, each of which used to be a bare ~SIGFPE~ with no message and no location.
|
|
The first two offer a ~retry~ at the failing site, because freeing something
|
|
or supplying another path makes the same operation succeed. The last two offer
|
|
*nothing*: no handler makes index 51 valid for a length-50 array or gives a
|
|
division by zero a quotient, so there is nothing to resume into. The restart
|
|
that answers those is the one your program already established — the frame
|
|
loop's ~continue~ — and it is on the stack and reachable without anything
|
|
being pushed at the failure.
|
|
- *An unknown restart name is a hard stop.* No ~find-restart~ to test with.
|
|
- *So are the wrong arguments*, and for the same reason: nothing static can
|
|
know what a name will find. The message names both signatures.
|
|
- *Lookup is by name; the signature is checked after it.* Nothing searches for
|
|
a frame the arguments would fit. An inner ~(use-value [s string] ...)~
|
|
shadows an outer ~(use-value [v i32] ...)~, so ~(invoke-restart 'use-value
|
|
21)~ stops the program even though the outer clause would have taken it.
|
|
- *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.
|
|
|
|
* Stops the program, exit 134
|
|
|
|
#+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 — ...
|
|
file.flan:9:12: restart use-value takes (i32), given (string)
|
|
file.flan:6:5: restart use-value takes (i32), and whatever took it supplied
|
|
no arguments — ...
|
|
#+end_example
|