A restart takes arguments, checked against the types the clause declared
This commit is contained in:
commit
33b6a65610
78
BUILT.md
78
BUILT.md
@ -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
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.
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
20
lib/ast.ml
20
lib/ast.ml
@ -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;
|
||||
|
||||
84
lib/check.ml
84
lib/check.ml
@ -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
|
||||
|
||||
159
lib/emit.ml
159
lib/emit.ml
@ -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
|
||||
|
||||
35
lib/load.ml
35
lib/load.ml
@ -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)
|
||||
|
||||
42
lib/parse.ml
42
lib/parse.ml
@ -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
|
||||
|
||||
26
lib/tast.ml
26
lib/tast.ml
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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\
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user