Say what a typed restart still cannot do, and who has to do it

The language half of §3's parameters is in; the half that makes it worth
having is not. A break loop chooses by position and has nothing to fill a
clause's parameters in with, and that is now the top item in NEXT.md,
spelled out end to end — the accessors the frame can already answer, the
signature on the wire, and the one store that has to happen before the
channel is aimed.

§3 asks for a clause's report string to be settled before parameters and
it was not. The field is cheap and so is the accessor; the only thing
that would read either is the break loop's listing, which lives in the
agent and the daemon, so it would have shipped as a field nothing read.
It belongs with the editor half, which is changing that listing anyway.
This commit is contained in:
Joseph Ferano 2026-09-12 10:48:21 +07:00
parent 468dab6e4c
commit 5184d732c9
3 changed files with 116 additions and 22 deletions

View File

@ -1069,14 +1069,8 @@ there are no function values yet, so nothing can call *back* into Flan across on
Scope, each piece refused by name with its reason and a test on the reason:
- **restarts take no parameters.** That covers §1's own `load-texture` example and skips argument marshalling and §3's
runtime arity check.
- **TODO — CL-style interactive recovery.** A stopped program should be able to offer a typed restart such as
`(use-value [value T] value)` or `(use-function [replacement (Fn ...)] ...)`, and the editor should show its signature
and ask for the replacement before invoking it. That is the missing "this variable is None; what should I use instead?"
path: named, hard-coded branches are useful for `retry` and `skip`, but not a substitute for an interactive value or
alternate implementation. It needs restart argument marshalling and validation in the runtime, plus an editor protocol
for entering and checking the supplied value/expression.
- **restarts took no parameters.** That covered §1's own `load-texture` example and skipped argument marshalling and
§3's runtime check. Both are in now; see "Conditions — step 3" below.
- `return` inside a `restart-case` body, exactly as inside `handler-bind`: a bare `ret` skips the pops.
- one `restart-case` offering a name twice — §4 finds the first frame offering it, and two in one frame makes that a
choice nothing in the source shows.
@ -1120,6 +1114,63 @@ else entirely.
here: the guard after every call is control flow the optimiser would otherwise launder, and the dev build is where each
of those calls goes through a cell.
### Conditions — step 3: restarts take parameters
§3's other half, and the half the comparative studies all ask for: `use-value` and `store-value` are the two restarts
whose answer is not in the program.
```
(restart-case (middle n)
(use-value [v i32] (* v 2))
(retry [] 7))
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))] (supplied 7)) ; 42
```
**The parameters live in a buffer the restart-case owns.** The obvious place is the invoker's frame — it is where the
values are — and it is wrong: a clause runs after every frame between the invoke and the target has returned (§5), so
the invoking side is gone by then. The invoker stores into the *target's* buffer while both are still alive, which is
the one moment they are. A clause's parameters are then ordinary slots of the establishing function, loaded out of that
buffer in the clause's landing block, and the clause body is in-frame code that sees this function's scope like any
other.
**What a restart takes is checked at run time, and it has to be.** §4 finds a restart by name on a dynamic stack: the
invoke site cannot see what it will find and the frame cannot see who will find it, so there is nothing for the checker
to compare. The frame therefore carries its parameter count and a 32-bit hash of how the types are spelled, and
`invoke-restart` compares both against its own before it stores anything. Every frame carries them, parameterless ones
included — a clause taking none has to refuse arguments as loudly as one taking two of the wrong type. The count is not
redundant with the hash: it is what keeps a hash collision between two *different* signatures harmless, since a
collision would then have to be between two lists of the same length. The spelling itself rides in the frame as well,
because the message has to say what was wanted and what was given and neither end knows both:
```
restarts.flan:79:36: restart use-value takes (i32), given ()
restarts.flan:87:36: restart retry takes (), given (i32)
```
**The arguments are evaluated into slots before the invoke node, not hung off it.** Two reasons and both are real. An
argument can transfer on its own, and that guard must fire before anything aims the channel. And a call written inside
an argument has to be on the walk `Reach` and `Load` already do — `InvokeRestart` was a leaf to both, and a leaf that
grew a subexpression would have dropped a function that is called from nowhere else and failed in the linker.
`restarts.flan` has exactly that function, `half`, to keep the claim tested.
**The break loop can take a restart it cannot fill in, so it is refused.** A transfer has two sources: an
`invoke-restart`, which writes the arguments first, and the break loop, which aims the channel at a frame by position
and has no value to supply. They reach a clause through the same channel by design, so nothing downstream can tell them
apart — which is what makes this the kind of hole that ships silently. The frame is pushed with its buffer marked
unfilled, `invoke-restart` marks it filled, and a clause with parameters checks the mark before reading. Choosing
`use-value` from a break loop today stops the program and says why. Filling it in is the editor half, and it is now the
top of `NEXT.md`: the answer is a Flan expression, and there is already something that compiles one against the live
program.
`runtime/flan_rt.c` gained two message functions and nothing else. The restart frame's first four fields are the ones C
declares and their offsets do not move; everything §3 needed is appended after them, and C never allocates one.
§3's other open point, a **report string per clause**, is still open and was not settled first as §3 asks. The field
and the accessor are both cheap; the only thing that would read them is the break loop's listing, which lives in the
agent and the daemon, so it would have landed as a field nothing read. It goes with the editor half, which is changing
that listing anyway.
### What is left
- **Editor comforts**: completion, eldoc, jump-to-definition, error overlays.

37
NEXT.md
View File

@ -19,12 +19,19 @@ restarts and resumes into the choice. A restart is chosen **by position**, off a
entered, because a name resolves to the innermost frame offering it and the stopped thread's stack does not hold still.
Restarts below the evaluation a break is inside are listed, marked, and refused with the reason.
Still open from §3, each refused by name today: **restarts with parameters** (argument marshalling plus a runtime arity
check), and **`handler-case`**, which §"What this does not settle" leaves open as possibly a macro over `handler-bind`
**Restarts take parameters now** — §3's other half. `(use-value [v i32] ...)` binds them, `(invoke-restart 'use-value 21)`
supplies them, and what a clause takes against what was given is checked at run time and refused with both spellings,
because a restart is found by name on a dynamic stack and neither end of a transfer can see the other. The one path
that cannot yet supply a value is the break loop, which is item 2 below and is where the interesting half is.
Still open: **`handler-case`**, which §"What this does not settle" leaves open as possibly a macro over `handler-bind`
plus a transfer. **`find-restart` and `compute-restarts` are blocked on a type, not on effort** — §4 gives them
`(Option Restart)` and a list, and there is no `Restart` type and no list to return one in. The minibuffer prompt never
needed them; it reads the snapshot over the agent's socket. And a `restart-case` clause should carry a **report
string** before any of this: `use-placeholder` is what `invoke-restart` needs, not what a person reading a list needs.
needed them; it reads the snapshot over the agent's socket. And a `restart-case` clause should still carry a **report
string**: `use-placeholder` is what `invoke-restart` needs, not what a person reading a list needs. §3 says to settle
that *before* parameters and it was not settled — the field is cheap and the accessor is cheap, but the only consumer
is the break loop's listing, which lives in the agent and the daemon, so it would have shipped as a field nothing
read. It belongs with item 2, where the listing is being changed anyway.
Read SBCL for what restarts should *mean* and ignore how it moves control: it transfers with `block`/`return-from`,
which §6 rules out.
@ -340,12 +347,22 @@ plan.org's single line on it (831) names a `for` the language does not have and
nothing. Flan has a better answer available for free: a `StorageExhausted` condition with a `retry` restart.
Decide which, because `push`/`put`/`clone`'s signatures depend on it.
2. **Typed restarts — `(use-value [v T] v)`.** The author's third TODO, and the most-wanted thing across every
comparative study. SBCL's report: restarts without parameters lose "the entire supply-a-value half of the standard
vocabulary", because `use-value` and `store-value` are the only two whose answer comes from outside the program.
Needs argument marshalling in `emit.ml` and §3's arity check in `check.ml`; both files are free now. The leverage
SBCL lacks: `eval` already compiles and runs an expression inside the live program, and the daemon already holds
the struct layouts, so "ask the human, type-check the answer, hand it over" is a short hop.
2. **The editor half of a typed restart.** The language half is in (see "Landed"): `(use-value [v i32] ...)` and
`(invoke-restart 'use-value 21)` work, and a mismatch is refused at run time with both signatures in the message.
What is missing is the half only an editor can do — the leverage SBCL lacks. `eval` already compiles and runs an
expression inside the live program and the daemon already holds the struct layouts, so "ask the human, type-check
the answer, hand it over" is a short hop, and it is the one path the runtime today *refuses*: a restart with
parameters taken from the break loop traps, because `flan_break_resume` and `flan_restart_take` aim the channel at
a frame and have nothing to fill its buffer with. What it needs, end to end:
- the frame already carries the arity and the signature as a string — `flan_restart_arity` and `flan_restart_sig`
beside `flan_restart_name`, the same walk, so `restarts` can say what each one takes;
- `:restarts` on the wire carries the signature per entry, so the minibuffer can show `use-value (i32)` rather
than a bare name, and `restart-at` grows an `:args` form — a list of *expressions*, since the answer is a Flan
expression and there is already something that compiles one;
- the daemon compiles each argument against the declared type with the session's layouts (the same path `C-x C-e`
takes), refuses it there if it does not fit, and otherwise writes the values into the frame's buffer and marks it
filled before aiming the channel. That last store is what `flan_restart_take` cannot do today and is the whole
of the remaining work; the marking exists so this cannot be forgotten silently.
3. **`handler-case`.** Not a convenience — it is the fix for the loudest gotcha in `conditions.org`. A handler closes
over nothing *only because* a `handler-bind` clause runs at the signal point; a `handler-case` clause runs in the

View File

@ -13,11 +13,27 @@ Why it is shaped this way: [[file:spec-conditions.md][spec-conditions.md]]. Some
(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) ...)
(name [p T ...] CLAUSE) ...)
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.
(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
@ -48,8 +64,13 @@ above it, or ~abort~.
* Not yet
~handler-case~ · ~find-restart~ · ~compute-restarts~ · restarts with
parameters. Each refused by name with its reason.
~handler-case~ · ~find-restart~ · ~compute-restarts~ · 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~
@ -64,6 +85,8 @@ the agent's socket instead.
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.
- *So are the wrong arguments*, and for the same reason: nothing static can
know what a name will find. The message names both signatures.
- *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.
@ -74,4 +97,7 @@ the agent's socket instead.
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