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.
6.6 KiB
Conditions and restarts — cheatsheet
Why it is shaped this way: spec-conditions.md. Something to poke at:
conditions-play.flan — flan dev conditions-play.flan, then C-c C-c.
Works
(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.
(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
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.
(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
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-caseand runs forward from there, so aretryrepeats 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. Arestart-caseat 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.flanis the worked example of the snapshot, including the ordering that matters: restore in the restart clause, not in adefer— 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 signalsBoundsErrorrather 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:StorageExhaustedwhen an allocator cannot satisfy a request,FileErrorwhen a file operation fails,BoundsErrorfor an index or slice outside its container, andArithErrorfor 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 bareSIGFPEwith no message and no location. The first two offer aretryat 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'scontinue— 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-restartto 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".
signalcannot hand a value back. Deliberate (§1).- A condition must be a struct.
returnis refused inside either form.
Stops the program, exit 134
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 — ...