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:
parent
468dab6e4c
commit
5184d732c9
67
BUILT.md
67
BUILT.md
@ -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
37
NEXT.md
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user