A restart takes arguments, checked against the types the clause declared

This commit is contained in:
Joseph Ferano 2026-09-12 10:51:32 +07:00
commit 33b6a65610
13 changed files with 669 additions and 102 deletions

View File

@ -1122,14 +1122,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.
@ -1173,6 +1167,74 @@ 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.
**Lookup stayed by name, and the signature is checked against what it found.** `flan_find_restart` matches the name
hash and nothing else, so an inner `(use-value [s string] ...)` shadows an outer `(use-value [v i32] ...)` and an i32
is refused there — the outer frame that would have taken it is never consulted. That is §4 read straight ("the first
frame offering the name") and it is the thing a reader will assume works the other way, so `restarts.flan` has a case
for it. Searching outward for a frame whose signature fits would make which restart runs depend on the arguments,
which is overload resolution on a dynamic stack.
Both of these guards go through `fail_block` unconditionally, unlike the bounds checks: `--no-bounds-checks` does not
remove them. A wrong index is a wrong answer, and a transfer into a clause whose parameters were never written, or
written to a different layout, is memory corruption.
`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.
@ -373,12 +380,22 @@ plan.org's single line on it (831) names a `for` the language does not have and
`push` and `put` are `Unit`, `clone` returns the container, and no signature grows a `Result`. One question is left
open in that section on purpose; it does not block the build.
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,12 @@ 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.
- *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.
@ -74,4 +101,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

View File

@ -57,10 +57,11 @@ and expr_kind =
A clause binds a name for the condition, so this cannot be a call. *)
| HandlerBind of hclause list * expr list
| Signal of sigkind * expr (* (signal c) / (error c) *)
(* (restart-case body (name [] body ...) ...) and (invoke-restart 'name).
Both alter control flow, so neither can be a call. *)
(* (restart-case body (name [p T] body ...) ...) and
(invoke-restart 'name arg ...). Both alter control flow, so neither can be
a call, and a clause binds its parameters §3. *)
| RestartCase of expr * rclause list
| InvokeRestart of string
| InvokeRestart of string * expr list
(* Two ways to signal, because they are two different things — §1 and §2.
[signal] returns Unit whatever it finds; [error] has type Never and, with
@ -68,7 +69,16 @@ and expr_kind =
and sigkind = Ssignal | Serror
and hclause = { hty : texpr; hname : string; hbody : expr list; hloc : Loc.t }
and rclause = { rname : string; rbody : expr list; rloc : Loc.t }
(* [rparams] are §3's inline annotations, the same name/type pairs a [defn]
takes. They are bound in the clause body and filled in by whatever invoked
the restart, which is why their count and types are checked at run time
(§3): a restart is found by name on a dynamic stack. *)
and rclause =
{ rname : string; rparams : field list; rbody : expr list; rloc : Loc.t }
(* Inline name/type pairs, as in [defn], [let] and [defstruct]. Here because a
restart clause's parameters are one, and a clause is part of an expression. *)
and field = { fname : string; fty : texpr; floc : Loc.t }
(* Two unwrap operators, because they are two different things — plan.org. *)
and unwrap = Usome | Utry
@ -90,8 +100,6 @@ and pattern =
(* ── Declarations ──────────────────────────────────────────────────── *)
type field = { fname : string; fty : texpr; floc : Loc.t }
type fn = {
name : string;
params : field list;

View File

@ -338,6 +338,21 @@ let type_id name =
name;
!h
(* How a restart's parameter list is spelled, and with it what the two ends of
an [invoke-restart] compare spec-conditions.md §3's run-time check. A
restart is found by name on a dynamic stack, so neither end can see the
other and nothing static can be checked: what is compared at run time is
this string's hash, alongside the count, and the string itself is carried so
that a mismatch can say what was wanted and what was given.
Comparing a 32-bit hash means two different parameter lists could in
principle collide. The count is checked separately, which rules out every
practical case (a collision would have to be between two lists of the same
length), and the types are parenthesised so that [(Option i32)] cannot read
as two parameters. *)
let restart_sig tys =
"(" ^ String.concat " " (List.map Types.to_string tys) ^ ")"
let expect loc ~want (got : Tast.expr) =
match want with
| None -> got
@ -489,18 +504,48 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
establishes frames around a body, and the other leaves the function it is
written in so both are their own nodes all the way down. *)
| Ast.RestartCase (body, clauses) -> check_restart_case ctx ?want loc body clauses
| Ast.InvokeRestart name ->
| Ast.InvokeRestart (name, args) ->
(* Never: control resumes at the restart-case, which yields the clause's
value to *its* continuation, so nothing here has a value and nothing
after it runs. The lookup is at run time because restarts are
dynamically scoped and named §4. *)
dynamically scoped and named §4 and so, for the same reason, is the
check that these arguments are the ones the clause takes (§3). *)
if ctx.in_defer then
fail loc
"invoke-restart is not allowed inside a defer — a defer is the cleanup \
a transfer runs on its way out, so starting one there would leave \
this function's defers half run with two targets and no way to \
choose";
expect loc ~want (mk loc Types.Never (Tast.InvokeRestart (type_id name, name, loc)))
let args = map_lr (fun a -> check ctx a) args in
List.iter
(fun (a : Tast.expr) ->
match a.Tast.ty with
| Types.Unit | Types.Never ->
fail a.Tast.loc
"a restart argument must be a value, and this one is %s"
(Types.to_string a.Tast.ty)
| _ -> ())
args;
let sg = restart_sig (List.map (fun (a : Tast.expr) -> a.Tast.ty) args) in
(* Evaluated into slots first, so that an argument which transfers on its
own is guarded before this form aims the channel, and so that a call
written in an argument is on the ordinary walk rather than hidden
inside a node that [Reach] and [Load] treat as a leaf. *)
let binds =
List.map (fun (a : Tast.expr) -> (fresh_slot ctx a.Tast.ty, a)) args
in
let locals =
List.map
(fun (s, (a : Tast.expr)) -> mk a.Tast.loc a.Tast.ty (Tast.Local s))
binds
in
let invoke =
mk loc Types.Never
(Tast.InvokeRestart (type_id name, name, locals, sg, type_id sg, loc))
in
expect loc ~want
(if binds = [] then invoke
else mk loc Types.Never (Tast.Let (binds, [ invoke ])))
| Ast.Defer _ ->
(* Registered by [check_fn], which is the only place that sees a form's
@ -703,13 +748,36 @@ and check_restart_case ctx ?want loc body clauses =
if List.mem c.Ast.rname !seen then
fail c.Ast.rloc "this restart-case offers %s twice" c.Ast.rname;
seen := c.Ast.rname :: !seen;
(* Each is checked against what the form has settled on so far, so a
clause that disagrees fails where it is written. The first one to
produce a value is what settles it when nothing outside did. *)
let b = block ctx ?want:!ty c.Ast.rloc c.Ast.rbody in
(* §3's parameters. They are slots in *this* function — a clause runs
here, not where the invoke was and the invoker stores into a
buffer this frame owns, because its own frame is gone by the time
the clause body starts (§5). Bound like a function's parameters:
visible only in the clause, and not assignable. *)
let params, b =
scoped ctx (fun () ->
let params =
List.map
(fun (p : Ast.field) ->
let ty = resolve ctx.env p.Ast.fty in
(match ty with
| Types.Unit | Types.Never ->
fail p.Ast.floc
"%s would be a restart parameter of type %s, which is \
not a value" p.Ast.fname (Types.to_string ty)
| _ -> ());
(bind ctx p.Ast.fname ty ~assignable:false, ty))
c.Ast.rparams
in
(* Each is checked against what the form has settled on so far, so
a clause that disagrees fails where it is written. The first one
to produce a value is what settles it when nothing outside
did. *)
(params, block ctx ?want:!ty c.Ast.rloc c.Ast.rbody))
in
if !ty = None && b.Tast.ty <> Types.Never then ty := Some b.Tast.ty;
let sg = restart_sig (List.map snd params) in
{ Tast.rname_id = type_id c.Ast.rname; rname = c.Ast.rname;
rbody = [ b ] })
rparams = params; rsig = sg; rsig_id = type_id sg; rbody = [ b ] })
clauses
in
let ty = match !ty with Some t -> t | None -> Types.Never in

View File

@ -638,7 +638,11 @@ and value_at f (e : Tast.expr) : string =
(* §4's lookup, then the transfer itself: the frame that was found goes into
the channel and this function leaves through its landing block. Type
Never, so nothing follows. *)
| Tast.InvokeRestart (id, name, rloc) ->
| Tast.InvokeRestart (id, name, args, sg, sg_id, rloc) ->
(* The arguments are already in slots — the checker put them there, so an
argument that transferred on its own has been guarded before anything
here runs. *)
let vals = List.map (fun a -> (value f a, a.Tast.ty)) args in
let t = fresh f in
ins f "%s = call ptr @flan_find_restart(i32 %d)" t id;
let ok = fresh f in
@ -649,6 +653,52 @@ and value_at f (e : Tast.expr) : string =
let nid, nn = string_bytes f.md name in
ins f "call void @flan_restart_fail(ptr %s, i64 %d, ptr %s, i64 %d)"
id n nid nn);
(* §3's run-time check. A restart is found by name on a dynamic stack, so
what it takes is not knowable here: the frame carries its parameter
count and the hash of how they are spelled, and both are compared.
The count is not redundant with the hash it is what makes a 32-bit
collision between two different signatures harmless in practice and
it is also the cheaper half. *)
let arity = fresh f in
ins f "%s = load i32, ptr %s" arity (restart_field f t 5);
let a_ok = fresh f in
ins f "%s = icmp eq i32 %s, %d" a_ok arity (List.length args);
let want = fresh f in
ins f "%s = load i32, ptr %s" want (restart_field f t 6);
let s_ok = fresh f in
ins f "%s = icmp eq i32 %s, %d" s_ok want sg_id;
let both = fresh f in
ins f "%s = and i1 %s, %s" both a_ok s_ok;
fail_block f rloc both (fun id n ->
let nid, nn = string_bytes f.md name in
(* What the frame says it takes is read off the frame, because only the
frame knows; what was given is this call site's own spelling. *)
let wp = fresh f in
ins f "%s = load ptr, ptr %s" wp (restart_field f t 8);
let wl = fresh f in
ins f "%s = load i64, ptr %s" wl (restart_field f t 9);
let gid, gn = string_bytes f.md sg in
ins f
"call void @flan_restart_args_fail(ptr %s, i64 %d, ptr %s, i64 %d, \
ptr %s, i64 %s, ptr %s, i64 %d)" id n nid nn wp wl gid gn);
(* Into the buffer the target frame owns, field by field: this frame is
about to go, and the clause runs after it has. The layout is the one the
signature just agreed on. *)
if vals <> [] then begin
let buf = fresh f in
ins f "%s = load ptr, ptr %s" buf (restart_field f t 4);
let sty =
"{ " ^ String.concat ", " (List.map (fun (_, ty) -> ll ty) vals) ^ " }"
in
List.iteri
(fun i (v, ty) ->
let p = fresh f in
ins f "%s = getelementptr inbounds %s, ptr %s, i32 0, i32 %d"
p sty buf i;
ins f "store %s %s, ptr %s" (ll ty) v p)
vals;
ins f "store i32 1, ptr %s" (restart_field f t 7)
end;
ins f "store ptr %s, ptr %s" t xfer_param;
term f "br label %%%s" (current_pad f);
"zeroinitializer"
@ -898,7 +948,19 @@ and emit_handled f frames body =
if not reached then begin f.live <- false; "zeroinitializer" end
else begin label f ld; "zeroinitializer" end
(* (restart-case BODY (name [] BODY-1) ...) — §3, §4 and §6 together.
(* A clause's parameters, as one LLVM struct: what the invoker stores into and
what the clause loads out of. The two ends never see each other, so the
layout is agreed by the signature hash they compare first same types in
the same order is the same struct. *)
and args_type (c : Tast.rclause) =
"{ " ^ String.concat ", " (List.map (fun (_, t) -> ll t) c.Tast.rparams) ^ " }"
and restart_field f slot i =
let p = fresh f in
ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 %d" p slot i;
p
(* (restart-case BODY (name [p T] BODY-1) ...) — §3, §4 and §6 together.
One frame per clause, so that the frame a transfer names says which clause
to run: the address is the identity, which is exact where a number would
@ -909,33 +971,55 @@ and emit_handled f frames body =
§5's defers between here and the invoke have already run each function on
the way out ran its own before returning. What is left here is to take these
frames off and start the clause. *)
frames off, copy §3's parameters out of the buffer the invoker filled, and
start the clause.
The parameters live in a buffer this frame owns, not the invoker's: by the
time a clause runs, every frame between the two has returned, so anything on
the invoking side is gone. The invoker stores into it while both are alive,
which is the one moment they are. *)
and emit_restart_case f ty clauses body =
let result = if is_void ty then None else Some (alloca f ty) in
let frames =
map_lr
(fun (c : Tast.rclause) ->
let slot = alloca_raw f "%restart" in
let nid = fresh f in
ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 1"
nid slot;
ins f "store i32 %d, ptr %s" c.Tast.rname_id nid;
ins f "store i32 %d, ptr %s" c.Tast.rname_id (restart_field f slot 1);
(* The name itself, beside the hash. A hash is all that matching
needs, but a break loop has to *show* someone their choices, and
nothing at run time can turn a hash back into a name. *)
let sid, slen = string_bytes f.md c.Tast.rname in
let np = fresh f in
ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 2"
np slot;
ins f "store ptr %s, ptr %s" sid np;
let nl = fresh f in
ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 3"
nl slot;
ins f "store i64 %d, ptr %s" slen nl;
ins f "store ptr %s, ptr %s" sid (restart_field f slot 2);
ins f "store i64 %d, ptr %s" slen (restart_field f slot 3);
(* §3's signature, which every frame carries whether it takes
parameters or not: an [invoke-restart] compares against whatever
frame the name found, and a clause taking none has to be able to
refuse arguments as loudly as one taking two of the wrong type. *)
ins f "store i32 %d, ptr %s"
(List.length c.Tast.rparams) (restart_field f slot 5);
ins f "store i32 %d, ptr %s" c.Tast.rsig_id (restart_field f slot 6);
let gid, glen = string_bytes f.md c.Tast.rsig in
ins f "store ptr %s, ptr %s" gid (restart_field f slot 8);
ins f "store i64 %d, ptr %s" glen (restart_field f slot 9);
let args =
if c.Tast.rparams = [] then None
else begin
let buf = alloca_raw f (args_type c) in
ins f "store ptr %s, ptr %s" buf (restart_field f slot 4);
(* Nothing has filled it in yet. Whoever aims a transfer at this
frame without going through an [invoke-restart] the break
loop, today leaves this zero, and the clause traps rather
than running on values no one supplied. *)
ins f "store i32 0, ptr %s" (restart_field f slot 7);
Some buf
end
in
ins f "call void @flan_restart_push(ptr %s)" slot;
slot)
(slot, args))
clauses
in
let args_of slot = List.assoc slot frames in
let frames = List.map fst frames in
let pop () =
List.iter
(fun slot -> ins f "call void @flan_restart_pop(ptr %s)" slot)
@ -966,6 +1050,37 @@ and emit_restart_case f ty clauses body =
guarded like any other; it must not start with the channel still set. *)
ins f "store ptr null, ptr %s" xfer_param;
pop ();
(* §3's parameters, copied out of the frame's buffer into the clause's own
slots before its body starts. The frame is still addressable it is an
alloca of *this* function and the buffer is whatever the invoker left
there. *)
let bind_params slot (c : Tast.rclause) =
match args_of slot with
| None -> ()
| Some buf ->
let armed = fresh f in
ins f "%s = load i32, ptr %s" armed (restart_field f slot 7);
let ok = fresh f in
ins f "%s = icmp ne i32 %s, 0" ok armed;
(* Aimed here by something that supplied no arguments — there is no such
path from an [invoke-restart], so this is the break loop taking a
restart it cannot yet fill in. Refused with the reason, rather than
running the clause on a buffer nobody wrote. *)
fail_block f (List.hd c.Tast.rbody).Tast.loc ok (fun id n ->
let nid, nn = string_bytes f.md c.Tast.rname in
let gid, gn = string_bytes f.md c.Tast.rsig in
ins f
"call void @flan_restart_unarmed(ptr %s, i64 %d, ptr %s, i64 %d, \
ptr %s, i64 %d)" id n nid nn gid gn);
List.iteri
(fun i (slot_i, ty) ->
let p = fresh f in
ins f "%s = getelementptr inbounds %s, ptr %s, i32 0, i32 %d"
p (args_type c) buf i;
let v = load f p ty in
ins f "store %s %s, ptr %s" (ll ty) v f.slots.(slot_i))
c.Tast.rparams
in
let rec dispatch = function
| [] ->
ins f "store ptr %s, ptr %s" tgt xfer_param;
@ -976,6 +1091,7 @@ and emit_restart_case f ty clauses body =
ins f "%s = icmp eq ptr %s, %s" t tgt slot;
term f "br i1 %s, label %%%s, label %%%s" t hit next;
label f hit;
bind_params slot c;
yield (block f c.Tast.rbody);
label f next;
dispatch rest
@ -1545,7 +1661,14 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
; target field, because the frame's own address *is* the target which makes
; a transfer's aim exact, and makes re-entering a restart-case work with
; nothing extra, since each activation allocates its own.
%restart = type { ptr, i32, ptr, i64 }
;
; Then §3's parameters: the buffer the clause reads them out of owned by the
; restart-case, because the invoker's frame is gone by the time a clause runs
; how many there are, the hash of how they are spelled, whether anything has
; filled the buffer in, and that spelling itself for the message when the two
; ends disagree. The first four fields are what the runtime's own
; [flan_restart] declares and their offsets do not move.
%restart = type { ptr, i32, ptr, i64, ptr, i32, i32, i32, ptr, i64 }
declare void @flan_rt_init(i32, ptr)
declare void @flan_argv(ptr)
@ -1565,6 +1688,8 @@ declare void @flan_restart_push(ptr)
declare void @flan_restart_pop(ptr)
declare ptr @flan_find_restart(i32)
declare void @flan_restart_fail(ptr, i64, ptr, i64) noreturn cold
declare void @flan_restart_args_fail(ptr, i64, ptr, i64, ptr, i64, ptr, i64) noreturn cold
declare void @flan_restart_unarmed(ptr, i64, ptr, i64, ptr, i64) noreturn cold
declare void @flan_transfer_fail(ptr, i64) noreturn cold
declare void @flan_bounds_fail(ptr, i64, i64, i64) noreturn cold
declare void @flan_slice_fail(ptr, i64, i64, i64, i64) noreturn cold

View File

@ -202,15 +202,29 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
| Ast.Unwrap (u, v) -> Ast.Unwrap (u, go v)
| Ast.Signal (k, c) -> Ast.Signal (k, go c)
(* A restart name is not a top-level name — it is looked up on the restart
stack, not in the environment so an import does not qualify it. Only
the bodies are rewritten. *)
stack, not in the environment so an import does not qualify it. The
bodies are rewritten, and so are a clause's parameter types, which name
types like any other annotation; the parameters themselves bind inside
the clause and shadow a package name there. *)
| Ast.RestartCase (body, clauses) ->
Ast.RestartCase
(go body,
List.map
(fun (c : Ast.rclause) -> { c with Ast.rbody = gos c.Ast.rbody })
(fun (c : Ast.rclause) ->
let ps =
List.map
(fun (p : Ast.field) ->
{ p with Ast.fty = rename_texpr owned alias p.Ast.fty })
c.Ast.rparams
in
let bound =
List.map (fun (p : Ast.field) -> p.Ast.fname) ps @ bound
in
{ c with
Ast.rparams = ps;
rbody = List.map (rename_expr owned alias bound) c.Ast.rbody })
clauses)
| Ast.InvokeRestart _ -> e.Ast.e
| Ast.InvokeRestart (n, args) -> Ast.InvokeRestart (n, gos args)
(* A clause names a condition *type*, which an import renames like any
other, and binds a name for the condition inside its own body. *)
| Ast.HandlerBind (clauses, body) ->
@ -336,8 +350,11 @@ let rec expr_uses acc (e : Ast.expr) =
let go = expr_uses acc in
let gos = List.iter go in
match e.Ast.e with
| Ast.Int _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ | Ast.Quote _
| Ast.InvokeRestart _ -> ()
| Ast.Int _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ | Ast.Quote _ ->
()
(* The name is not one an import can supply, but the arguments are ordinary
expressions and may well use one. *)
| Ast.InvokeRestart (_, args) -> gos args
| Ast.Var n -> acc := (n, e.Ast.loc) :: !acc
| Ast.Do body -> gos body
| Ast.Let (bs, body) ->
@ -365,7 +382,11 @@ let rec expr_uses acc (e : Ast.expr) =
| Ast.Signal (_, c) -> go c
| Ast.RestartCase (body, clauses) ->
go body;
List.iter (fun (c : Ast.rclause) -> gos c.Ast.rbody) clauses
List.iter
(fun (c : Ast.rclause) ->
List.iter (fun (p : Ast.field) -> texpr_uses acc p.Ast.fty) c.Ast.rparams;
gos c.Ast.rbody)
clauses
| Ast.HandlerBind (clauses, body) ->
List.iter
(fun (c : Ast.hclause) -> texpr_uses acc c.Ast.hty; gos c.Ast.hbody)

View File

@ -256,47 +256,41 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
in
mk (Ast.HandlerBind (List.map clause clauses, body_of body))
(* (restart-case BODY (name [] BODY-1) ...) — spec-conditions.md §3.
The body and every clause have the same type, which is the form's.
Restarts take no parameters in this version; a clause that declares one is
rejected below rather than ignored. *)
(* (restart-case BODY (name [p T ...] BODY-1) ...) — spec-conditions.md §3.
The body and every clause have the same type, which is the form's. A
clause's parameters are inline name/type pairs, like any other binding
form; what fills them in is the [invoke-restart] that chose the clause. *)
| Sym "restart-case" ->
let body, clauses =
match args with
| body :: clauses when clauses <> [] -> (body, clauses)
| _ ->
fail f "restart-case is (restart-case body (name [] body ...) ...)"
fail f "restart-case is (restart-case body (name [p T] body ...) ...)"
in
let clause (c : Form.t) =
match c.Form.v with
| Form.List ({ v = Form.Sym n; _ } :: { v = Form.Vec ps; _ } :: cbody)
when cbody <> [] ->
if ps <> [] then
fail c
"a restart takes no parameters yet — spec-conditions.md §3 has \
them, and they need argument marshalling and a runtime arity \
check that this version does not do";
{ Ast.rname = n; rbody = List.map expr cbody; rloc = c.Form.loc }
| _ -> fail c "a restart-case clause is (name [] body ...)"
{ Ast.rname = n; rparams = fields c ps;
rbody = List.map expr cbody; rloc = c.Form.loc }
| _ -> fail c "a restart-case clause is (name [p T] body ...)"
in
mk (Ast.RestartCase (expr body, List.map clause clauses))
(* (invoke-restart 'name) : Never. The name is a quoted symbol — that is what
the reader's quote is for and it is resolved on the restart stack at run
time, since restarts are dynamically scoped. *)
(* (invoke-restart 'name arg ...) : Never. The name is a quoted symbol — that
is what the reader's quote is for and it is resolved on the restart
stack at run time, since restarts are dynamically scoped. The arguments
fill in the clause's parameters, and how many there are and what they are
is settled at run time too, against the frame the name found (§3). *)
| Sym "invoke-restart" ->
(match args with
| [ { v = Form.List [ { v = Form.Sym "quote"; _ }; { v = Form.Sym n; _ } ]; _ } ] ->
mk (Ast.InvokeRestart n)
| [ _ ] ->
fail f
"invoke-restart takes a quoted restart name, as in \
(invoke-restart 'use-placeholder)"
| { v = Form.List [ { v = Form.Sym "quote"; _ }; { v = Form.Sym n; _ } ]; _ }
:: rest ->
mk (Ast.InvokeRestart (n, List.map expr rest))
| _ ->
fail f
"a restart takes no arguments yet — spec-conditions.md §3 has them, \
and they need argument marshalling and a runtime arity check that \
this version does not do")
"invoke-restart takes a quoted restart name and then its arguments, \
as in (invoke-restart 'use-value 42)")
(* ── macros ────────────────────────────────────────────────────── *)
(* The reader now produces these three, so they arrive here as ordinary heads

View File

@ -81,9 +81,20 @@ and expr_kind =
*its* frames it runs that clause instead, and the whole form yields either
way. [InvokeRestart] looks the name up on the restart stack, writes the
frame it found into the transfer channel and leaves it has type Never,
so nothing follows it. *)
so nothing follows it.
[InvokeRestart]'s arguments are already evaluated: the checker binds each
to a slot and wraps the node in a [Let], so what is left here is a list of
locals to copy into the frame. Two reasons, and both matter. An argument
that transfers on its own must be guarded before this one aims the
channel; and a call written in an argument has to be on the walk [Reach]
and [Load] already do, which a list hanging off a node they treat as a
leaf would not be. [rsig] is the argument types as written, and [rsig_id]
their hash §3's run-time check, since the name is resolved on a stack
nothing static can see. *)
| RestartCase of rclause list * expr
| InvokeRestart of int * string * Loc.t (* name id, name, where *)
(* name id, name, arguments, their spelling, its hash, where *)
| InvokeRestart of int * string * expr list * string * int * Loc.t
(* [Serror] is §2's diverging variant: the same lookup, type Never, and with
nothing transferring the program stops rather than carrying on. *)
@ -102,8 +113,15 @@ and hframe = { htype : int; hfn : string }
(* A restart clause. [rname_id] is what [invoke-restart] matches by name; the
body is a branch in the function that wrote it, because unlike a handler a
clause runs at the restart-case, which is where it was written. *)
and rclause = { rname_id : int; rname : string; rbody : expr list }
clause runs at the restart-case, which is where it was written.
[rparams] are the slots §3's parameters are bound to, in order, with their
types; the invoker stores into a buffer this frame owns and the clause loads
them from it. [rsig] is how those types are spelled and [rsig_id] its hash:
what the two ends compare, since neither can see the other. *)
and rclause =
{ rname_id : int; rname : string; rparams : (int * Types.t) list;
rsig : string; rsig_id : int; rbody : expr list }
(* [binds] are the slots the pattern's fields are bound to, in field order. *)
and arm = { acase : string option; binds : int list; abody : expr list }

View File

@ -398,6 +398,40 @@ _Noreturn void flan_restart_fail(const uint8_t *loc, int64_t loclen,
rt_die();
}
/* The frame the name found does not take these arguments — spec-conditions.md
* §3's run-time check. It has to be at run time: a restart is resolved on a
* dynamic stack, so the invoke site cannot see what it will find, and the
* frame cannot see who will find it. What each end knows is its own parameter
* list, so the message is both of them side by side. */
_Noreturn void flan_restart_args_fail(const uint8_t *loc, int64_t loclen,
const uint8_t *name, int64_t namelen,
const uint8_t *want, int64_t wantlen,
const uint8_t *got, int64_t gotlen) {
fflush(stdout);
fprintf(stderr, "%.*s: restart %.*s takes %.*s, given %.*s\n",
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
(int)wantlen, (const char *)want, (int)gotlen, (const char *)got);
rt_die();
}
/* A clause with parameters was reached by a transfer that filled none of them
* in. No [invoke-restart] can do that it writes the arguments before it aims
* the channel so this is the other way a transfer starts: the break loop,
* which today takes a restart by position and has no way to supply a value.
* Refused at the clause rather than run on a buffer nobody wrote. */
_Noreturn void flan_restart_unarmed(const uint8_t *loc, int64_t loclen,
const uint8_t *name, int64_t namelen,
const uint8_t *want, int64_t wantlen) {
fflush(stdout);
fprintf(stderr,
"%.*s: restart %.*s takes %.*s, and whatever took it supplied no "
"arguments — a restart with parameters cannot be taken from the "
"break loop yet\n",
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
(int)wantlen, (const char *)want);
rt_die();
}
/* Something a defer called invoked a restart. A defer is the cleanup a
* transfer runs on its way out (§5), so a transfer starting there would leave
* this frame's defers half run with two targets and no way to choose. The

View File

@ -3,7 +3,12 @@
;;;; The transfer. A handler runs where the signal was, decides, and control
;;;; resumes at a restart-case further out: every function in between returns
;;;; early with the target in the channel, running its defers on the way (§5).
;;;; Restarts take no parameters in this version.
;;;;
;;;; §3's parameters are here too, along with the run-time check they need: the
;;;; supply-a-value half of the vocabulary, which is the half whose answer comes
;;;; from outside the program. With no argument this program is the exit-0 case
;;;; the table pins; with one it selects a mismatch, which traps and is asserted
;;;; on its reason.
(defstruct AssetMissing [id i32])
(defvar log i64)
@ -46,7 +51,68 @@
0)
(use-placeholder [] -2)))
(defn main [] i32
;;; Called from nowhere but inside an [invoke-restart]'s argument list.
(defn half [x i32] i32 (/ x 2))
;;; §3: a clause with parameters. The value comes from the handler, which is
;;; the whole point — [use-value] and [store-value] are the two restarts whose
;;; answer is not in the program. The parameters are slots of *this* function
;;; and the invoker fills a buffer this frame owns, because by the time the
;;; clause runs the invoking frame has gone (§5).
(defn supplied [n i32] i32
(restart-case (middle n)
(use-value [v i32] (* v 2))
(use-pair [a i32 b i32] (+ a b))
(retry [] 7)))
;;; Parameters of more than one type, and one that is not a machine word: a
;;; string is ptr+len and crosses the transfer as the two of them.
(defn labelled [n i32] i32
(restart-case (middle n)
(use-labelled [label string v i32]
(do (print label) (println "") v))))
;;; The mismatch cases. Each is selected by the argument, because each stops
;;; the program: what a restart takes is not knowable where it is invoked, so
;;; §3 checks it at run time and this is what that check refuses.
(defn mismatched [n i32] i32
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value))]
(supplied n)))
(defn mistyped [n i32] i32
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value "forty-one"))]
(supplied n)))
(defn overfull [n i32] i32
(handler-bind [(AssetMissing [c] (invoke-restart 'retry 1))]
(supplied n)))
;;; §4 meets §3: lookup is by *name*, and the signature is checked against
;;; whatever that finds. An inner frame offering use-value shadows the outer
;;; one, so an i32 is refused here even though the outer clause would have
;;; taken it — a name finds one frame, it does not search for a fitting one.
(defn shadowed [n i32] i32
(restart-case
(restart-case (middle n)
(use-value [s string] (do (print s) 0)))
(use-value [v i32] v)))
(defn mislaid [n i32] i32
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
(shadowed n)))
(defn main [args [string]] i32
;; One argument selects a trap; none runs the table's case.
(if (> (len args) 1)
(let [k (i32 (bytes->i64 (bytes (at args 1))))]
(cond
(= k 1) (print (mismatched 90))
(= k 2) (print (mistyped 91))
(= k 3) (print (overfull 92))
(= k 4) (print (mislaid 93))
:else (println "?"))
(return 0)))
;; Nothing handles it, so signal is a no-op and the body's own value stands.
(print (fetch 1)) (println "") ; 101
(print log) (println "") ; 1
@ -77,4 +143,27 @@
;; the trap case in the acceptance table rather than a line here.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
(print (strict 6)) (println "")) ; -2
;; §3: the handler supplies the value, and the clause computes with it —
;; the arithmetic is in the clause so that a transfer which forgot to copy
;; the argument could not pass by returning what it was given.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
(print (supplied 7)) (println "")) ; 42
;; Two parameters, so their order is pinned: 1 and 2 would sum the same
;; whichever way round they landed.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-pair 30 4))]
(print (supplied 8)) (println "")) ; 34
;; A clause with no parameters is still reachable from a restart-case that
;; has some, and an invoke with no arguments still matches it.
(handler-bind [(AssetMissing [c] (invoke-restart 'retry))]
(print (supplied 9)) (println "")) ; 7
;; A string and an integer together: two different widths, and the string is
;; ptr+len rather than a machine word.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-labelled "supplied" 5))]
(print (labelled 10)) (println "")) ; supplied / 5
;; The argument is an ordinary expression, evaluated where the invoke is —
;; here a call, and [half] is reached from nowhere else, so a walk that did
;; not look inside an invoke-restart would drop it and fail to link.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value (half 42)))]
(print (supplied 12)) (println "")) ; 21 * 2
0)

View File

@ -256,11 +256,106 @@ let () =
shadowing an outer one of the same name, and a handler that returns
normally still transferring nothing. At -O0 as well, because the guard
after every call is control flow the optimiser would otherwise launder;
and as a dev build, where every one of those calls goes through a cell. *)
let restarts_out = "101\n1\n-1\n2\n7\n1010\n101\n105\n-2\n" in
and as a dev build, where every one of those calls goes through a cell.
Then §3's parameters: one, two of them in an order a sum would not pin,
a string beside an integer, a clause taking none in the same form as
clauses taking some, and an argument that is a call to a function
reached from nowhere else. The last one is the reachability claim an
invoke-restart whose arguments were not walked would drop [half] and
fail to link, which is why the arguments are evaluated into slots before
the node rather than hanging off it. *)
let restarts_out =
"101\n1\n-1\n2\n7\n1010\n101\n105\n-2\n42\n34\n7\nsupplied\n5\n42\n"
in
outputs "restarts" "programs/restarts.flan" restarts_out;
outputs ~opt:"-O0" "restarts, -O0" "programs/restarts.flan" restarts_out;
outputs ~dev:true "restarts, dev" "programs/restarts.flan" restarts_out;
(* §3's run-time check, which is the price of a restart being found by name
on a dynamic stack: neither end of an invoke can see the other, so what
a clause takes against what was given is settled where the transfer
starts. Each of these stops the program, so each is asserted on its
reason rather than on the exit status alone too few arguments, the
right count of the wrong type, and arguments handed to a clause that
takes none. *)
let restart_mismatch ?opt () =
let exe = compile ?opt "programs/restarts.flan" in
let refuses name arg reason =
let code, text = run exe (Some arg) in
if code <> 134
|| not (contains text "programs/restarts.flan:")
|| not (contains text reason)
then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted: %S (exit 134)\n"
name text code reason
end
in
refuses "a restart invoked with too few arguments" "1"
"restart use-value takes (i32), given ()";
refuses "a restart invoked with the wrong type" "2"
"restart use-value takes (i32), given (string)";
refuses "arguments given to a restart that takes none" "3"
"restart retry takes (), given (i32)";
(* §4 meets §3. The name finds the innermost frame offering it and the
signature is checked against *that*; nothing searches outward for a
frame the arguments would have fitted, and an outer clause that would
have taken them is not consulted. *)
refuses "a shadowing clause of the same name and a different signature" "4"
"restart use-value takes (string), given (i32)";
(try Sys.remove exe with Sys_error _ -> ())
in
restart_mismatch ();
restart_mismatch ~opt:"-O0" ();
(* The other way a transfer starts is the break loop, which chooses a
restart by position and has nothing to fill parameters in with. It
reaches the clause through the same channel an invoke-restart writes, so
nothing downstream could tell the two apart except that the clause's
buffer is still the zero the frame was pushed with. Asserted on the IR,
because driving it needs a stopped program and a socket, and what is
being claimed is that the guard exists at all. *)
let p =
Reader.read_file "programs/restarts.flan" |> Parse.program |> Check.program
in
if not (contains (Emit.program p) "call void @flan_restart_unarmed(") then begin
incr failures;
print_endline
"FAIL a clause with parameters has no guard against being taken \
without any"
end;
(* What is refused before anything runs, and why. Not everything about a
restart's arguments waits for run time: the shape of the form and
whether an argument is a value at all are here, and each is asserted on
its reason. *)
let refuses_src name src needle =
match Check.program (Parse.program (Reader.read_all ~file:"<restarts>" src)) with
| _ ->
incr failures;
Printf.printf "FAIL %s\n it was accepted\n" name
| exception Loc.Error (_, m) ->
if not (contains m needle) then begin
incr failures;
Printf.printf "FAIL %s\n said: %S\n wanted: %S in it\n"
name m needle
end
in
(* The name is still a quoted symbol, and now it is the *first* of several
things, so an unquoted one has to say what the form is rather than read
as a call with a spare argument. *)
refuses_src "invoke-restart without a quoted name"
"(defn main [] i32 (invoke-restart use-value 1) 0)"
"a quoted restart name and then its arguments";
(* A clause parameter is a binding, so it needs something to hold. *)
refuses_src "a restart parameter that is not a value"
"(defn main [] i32 (restart-case 0 (use-value [v Unit] 1)))"
"which is not a value";
(* And so does an argument: a [println] is Unit, and there would be nothing
to store into the clause's buffer. *)
refuses_src "a restart argument that is not a value"
"(defn main [] i32 (restart-case 0 (use-value [v i32] v))\n\
\ (invoke-restart 'use-value (println \"\")) 0)"
"a restart argument must be a value";
(* §2's other half, which cannot be an [outputs] case because it does not
exit 0: a handler runs, returns normally, and has still not answered the
error, so the program stops and names the condition. *)
@ -1512,10 +1607,11 @@ ERR@7 unexpected token: not the kind the caller was reading
(defn fetch [n i32] i32\n\
\ (restart-case\n\
\ (do (error (Missing {:id n})) 0)\n\
\ (use-value [v i32 s string] (do (print s) v))\n\
\ (use-placeholder [] -1)))\n\
(defn run [] i32\n\
\ (defer (set seen (+ seen 1)))\n\
\ (handler-bind [(Missing [m] (invoke-restart 'use-placeholder))]\n\
\ (handler-bind [(Missing [m] (invoke-restart 'use-value 4 \"\"))]\n\
\ (fetch 3)))\n\
(defn main [] i32\n\
\ (set (at arr 2) 9)\n\

View File

@ -811,17 +811,22 @@ let () =
rejects_check "return inside restart-case"
"(defn f [] i32 (restart-case (return 1) (skip [] 2)))"
~needle:"return is not allowed inside restart-case";
(* The two halves of §3 this version does not do, each refused by name with
the reason rather than parsed into something that means less. *)
rejects_check "a restart with parameters"
"(defn f [] i32 (restart-case 1 (skip [n i32] n)))"
~needle:"a restart takes no parameters yet";
rejects_check "invoke-restart with arguments"
"(defn f [] (invoke-restart 'skip 1))"
~needle:"a restart takes no arguments yet";
(* §3's parameters. A clause binds them like a function's, so the body sees
them and nothing outside does; what they are is checked against the
invoke at run time, because the two ends meet on a dynamic stack. *)
accepts "a restart with parameters"
"(defn f [] i32 (restart-case 1 (skip [n i32] n)))";
accepts "invoke-restart with arguments"
"(defn f [] (invoke-restart 'skip 1))";
rejects_check "a restart parameter outside its clause"
"(defn f [] i32 (+ (restart-case 1 (skip [n i32] n)) n))"
~needle:"unknown name n";
rejects_check "a restart argument that is not a value"
"(defn f [] (invoke-restart 'skip (println \"\")))"
~needle:"a restart argument must be a value";
rejects_check "invoke-restart on an unquoted name"
"(defn f [] (invoke-restart skip))"
~needle:"quoted restart name";
~needle:"a quoted restart name and then its arguments";
(* §5 runs the defers on the way out, so a defer is already the cleanup path
a transfer uses. One that starts its own transfer has no answer. *)
rejects_check "invoke-restart inside a defer"