From 5184d732c94b467e5a070b2636863d160991234a Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 10:48:21 +0700 Subject: [PATCH] Say what a typed restart still cannot do, and who has to do it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- BUILT.md | 67 ++++++++++++++++++++++++++++++++++++++++++++------ NEXT.md | 37 ++++++++++++++++++++-------- conditions.org | 34 ++++++++++++++++++++++--- 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/BUILT.md b/BUILT.md index 1fe5847..9a565ef 100644 --- a/BUILT.md +++ b/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. diff --git a/NEXT.md b/NEXT.md index 4f2b851..8cc64e4 100644 --- a/NEXT.md +++ b/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 diff --git a/conditions.org b/conditions.org index 7d81d3e..823b38f 100644 --- a/conditions.org +++ b/conditions.org @@ -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