A consistency sweep, run by checking claims against the compiler rather
than reading for style. Every edit here corrects something that is false
now, or adds something the page had no way to say.
`web/index.html` was the worst of it, and the worst of that was control
flow: the page said there is no `loop`/`recur` and no `break` or
`continue`, and printed the refusal message for `break` as evidence. All
four are built, with loop labels. A live code example called
`index-of-i32`, which no longer exists and would not compile. The prelude
table was the pre-generics per-type families, a paragraph said there is
no `println` two paragraphs after calling `println` the compiler's, and
`sqrt-f32` was "the one `declare` in the file" when there are five. The
"Not implemented yet" table listed `Vec`, `Map`, `Handle`, `Fn`, `fn`,
unions, `defmacro` and quasiquote, all of which check; what is actually
left is `Result`, `try`, a quoted symbol as a value, `errdefer`, `await`,
`handler-case` and the restart-stack readers. Restarts take parameters
(`(invoke-restart 'use-value 21)` answers 42), `defer` in a `let` is
allowed, and there is both an allocator and a `context`.
Generics is a new section, because nobody had documented the syntax. The
brief for it was wrong in one place and the corpus settled it: `$t` goes
in *every* type position including the return type, and bare `t` is the
type-name argument in expression position — `(vec-new t)`, `(t x)`. It
says what a type variable is move-only by default means, since that is
the rule a reader hits first and it is not Odin's.
`FLAN_RAYLIB_H` is gone from every doc that claimed it still decided
something. The passages that say "this used to be opt-in" are kept and
labelled; the ones that said "this is opt-in" are not. `plan.org` had
`{string i32}` in the type list and four predicates where there are five.
`conditions.org` described `errdefer`'s behaviour without saying it is
refused by name. `REFERENCES.md` pointed at the gitignored copy of the
raylib header rather than the committed one, which is the exact trap that
made committing it necessary.
Found and not fixed, because it is not documentation: `vendor/raylib/headers`
still says a build reads it "when the variable happens to be set", which
contradicts the section below it in the same file and is false — moving
the header makes every build fail by name.
126 lines
5.6 KiB
Org Mode
126 lines
5.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.
|
|
- *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
|