Merge branch 'worktree-agent-a5df4c0409bf9d7c4' into dev-loop
This commit is contained in:
commit
f360531918
25
FIX.org
25
FIX.org
@ -520,6 +520,31 @@ answers the same question at a fraction of the wall clock. The consequence to
|
|||||||
accept is that a lane is reviewed on its code rather than on sweep numbers it
|
accept is that a lane is reviewed on its code rather than on sweep numbers it
|
||||||
no longer produces, which is what the review before a merge is for.
|
no longer produces, which is what the review before a merge is for.
|
||||||
|
|
||||||
|
* handler-case, decided 2026-09-19
|
||||||
|
Built, both backends, and it needed no backend work at all: it is a
|
||||||
|
handler-bind whose clause invokes a restart the form established around
|
||||||
|
itself, which is spec-conditions.md's one open question about the operator
|
||||||
|
answered in the affirmative. The shape is (handler-case BODY [(T [c] ...)]),
|
||||||
|
body first and clauses after, the opposite of handler-bind's order because a
|
||||||
|
handler-bind reads as something put around a body and this one reads as a body
|
||||||
|
with answers hung off it.
|
||||||
|
|
||||||
|
Everything the unwinding form needs it inherits. Defers and the
|
||||||
|
with-allocator restore run on the way out because a transfer already runs them
|
||||||
|
for every frame it leaves. The body and every clause agree on one type because
|
||||||
|
§3 already says a restart-case's do, and a clause that disagrees is refused
|
||||||
|
with the same message an if with disagreeing arms gets. A condition no clause
|
||||||
|
lists installs no matching frame and carries on outward untouched. A clause
|
||||||
|
runs at the form, so it sees the establishing function's locals, which a
|
||||||
|
handler-bind clause cannot — that is the whole difference, and it falls out of
|
||||||
|
where a restart clause runs rather than being arranged for.
|
||||||
|
|
||||||
|
The one wart, noted and left: the restart the form makes up for itself is on
|
||||||
|
the restart stack like any other, so a break loop entered underneath one lists
|
||||||
|
it. Choosing it there is refused loudly rather than answered wrongly, and
|
||||||
|
hiding it would mean a new field in a frame layout written out in emit.ml, in
|
||||||
|
x86.ml and in flan_rt.c.
|
||||||
|
|
||||||
* Surface syntax discussion, 2026-09-19
|
* Surface syntax discussion, 2026-09-19
|
||||||
The author wants an F#-ish indentation-based ML surface living side by side
|
The author wants an F#-ish indentation-based ML surface living side by side
|
||||||
with s-expressions, not replacing them. The languages that disappear for the
|
with s-expressions, not replacing them. The languages that disappear for the
|
||||||
|
|||||||
@ -186,10 +186,23 @@ Neither implementation returns errors as values. Both use the host's exception o
|
|||||||
condition system throughout. Porting this game exercises `signal`/`error`/`restart-case`
|
condition system throughout. Porting this game exercises `signal`/`error`/`restart-case`
|
||||||
and never wants a `Result`.
|
and never wants a `Result`.
|
||||||
|
|
||||||
### `handler-case` — one site, and `handler-bind` already covers it
|
### `handler-case` — one site, and it is now the natural spelling of it
|
||||||
|
|
||||||
`engine.clj`'s `reload-config!` is `(try … (catch Exception e (println e)))`. That is
|
`engine.clj`'s `reload-config!` is `(try … (catch Exception e (println e)))`. The ranking
|
||||||
"log it and carry on", which is what a `handler-bind` clause returning normally does.
|
below was written when the operator did not exist, and said `handler-bind` covers it,
|
||||||
|
which it does: "log it and carry on" is what a clause returning normally does. It is no
|
||||||
|
longer the closer translation. `handler-case` landed on 2026-09-19 and is Clojure's
|
||||||
|
try/catch by construction, so that site ports across as itself:
|
||||||
|
|
||||||
|
```
|
||||||
|
(handler-case (read-config path)
|
||||||
|
[(FileError [c] (do (println (.path c)) default-config))])
|
||||||
|
```
|
||||||
|
|
||||||
|
The difference that decides it is not spelling. A `handler-bind` clause is lifted into a
|
||||||
|
function of its own and cannot see `reload-config!`'s locals; a `handler-case` clause runs
|
||||||
|
at the form and can, which is what a catch block is assumed to do everywhere it is
|
||||||
|
written.
|
||||||
|
|
||||||
### `Handle` and pools — nothing to pool
|
### `Handle` and pools — nothing to pool
|
||||||
|
|
||||||
@ -642,9 +655,11 @@ not compete for the same slot.
|
|||||||
|
|
||||||
**Not ranked, because this game does not need them:** escaping closures and capture (one
|
**Not ranked, because this game does not need them:** escaping closures and capture (one
|
||||||
site, fixed by one parameter), `Handle` and pools (nothing to pool), `Result`/`try`
|
site, fixed by one parameter), `Handle` and pools (nothing to pool), `Result`/`try`
|
||||||
(neither host uses that discipline), `handler-case` (one site, `handler-bind` covers it),
|
(neither host uses that discipline), `loop`/`recur` and tail calls (nothing recurses),
|
||||||
`loop`/`recur` and tail calls (nothing recurses), user-written allocators, structural
|
user-written allocators, structural typing. `handler-case` was on this list for the same
|
||||||
typing.
|
reason and has since been built anyway — it cost one function in the checker and nothing
|
||||||
|
in either backend, being a `handler-bind` whose clause invokes a restart the form
|
||||||
|
established around itself.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -292,6 +292,10 @@ line is off screen."
|
|||||||
("dotimes" . 1)
|
("dotimes" . 1)
|
||||||
;; The clause vector, then the protected body. Same shape as `let'.
|
;; The clause vector, then the protected body. Same shape as `let'.
|
||||||
("handler-bind" . 1)
|
("handler-bind" . 1)
|
||||||
|
;; `handler-case' is the other way round — the body first and the clause
|
||||||
|
;; vector after it — but the entry is the same number, because what it
|
||||||
|
;; says is that one argument is special and the rest indent as a body,
|
||||||
|
;; and that is true of both orders.
|
||||||
("handler-case" . 1)
|
("handler-case" . 1)
|
||||||
;; Test first, body after.
|
;; Test first, body after.
|
||||||
("if" . 1)
|
("if" . 1)
|
||||||
|
|||||||
@ -86,6 +86,13 @@ and expr_kind =
|
|||||||
(* (handler-bind [(Type [c] body ...) ...] body ...) — spec-conditions.md.
|
(* (handler-bind [(Type [c] body ...) ...] body ...) — spec-conditions.md.
|
||||||
A clause binds a name for the condition, so this cannot be a call. *)
|
A clause binds a name for the condition, so this cannot be a call. *)
|
||||||
| HandlerBind of hclause list * expr list
|
| HandlerBind of hclause list * expr list
|
||||||
|
(* (handler-case BODY [(Type [c] body ...) ...]) — the other half of
|
||||||
|
spec-conditions.md's pair. Where a handler-bind clause runs at the signal
|
||||||
|
with the stack below it intact, a handler-case clause runs *here*, after
|
||||||
|
that stack has gone, and its value is the value of the whole form. The
|
||||||
|
clauses share [hclause] with handler-bind because the syntax is the same
|
||||||
|
one; what differs is entirely where the body runs. *)
|
||||||
|
| HandlerCase of expr * hclause list
|
||||||
| Signal of sigkind * expr (* (signal c) / (error c) *)
|
| Signal of sigkind * expr (* (signal c) / (error c) *)
|
||||||
(* (restart-case body (name [p T] body ...) ...) and
|
(* (restart-case body (name [p T] body ...) ...) and
|
||||||
(invoke-restart 'name arg ...). Both alter control flow, so neither can be
|
(invoke-restart 'name arg ...). Both alter control flow, so neither can be
|
||||||
@ -271,6 +278,7 @@ let map_children f (e : expr) : expr =
|
|||||||
| Defer es -> Defer (List.map ex es)
|
| Defer es -> Defer (List.map ex es)
|
||||||
| Unwrap (u, x) -> Unwrap (u, ex x)
|
| Unwrap (u, x) -> Unwrap (u, ex x)
|
||||||
| HandlerBind (cs, es) -> HandlerBind (List.map hcl cs, List.map ex es)
|
| HandlerBind (cs, es) -> HandlerBind (List.map hcl cs, List.map ex es)
|
||||||
|
| HandlerCase (b, cs) -> HandlerCase (ex b, List.map hcl cs)
|
||||||
| Signal (k, x) -> Signal (k, ex x)
|
| Signal (k, x) -> Signal (k, ex x)
|
||||||
| RestartCase (b, cs) -> RestartCase (ex b, List.map rcl cs)
|
| RestartCase (b, cs) -> RestartCase (ex b, List.map rcl cs)
|
||||||
| InvokeRestart (n, args) -> InvokeRestart (n, List.map ex args)
|
| InvokeRestart (n, args) -> InvokeRestart (n, List.map ex args)
|
||||||
|
|||||||
149
lib/check.ml
149
lib/check.ml
@ -2059,6 +2059,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
|
|||||||
expect loc ~want (mk loc ty (Tast.Signal (kind, type_id name, c)))
|
expect loc ~want (mk loc ty (Tast.Signal (kind, type_id name, c)))
|
||||||
|
|
||||||
| Ast.HandlerBind (clauses, body) -> check_handler_bind ctx ?want loc clauses body
|
| Ast.HandlerBind (clauses, body) -> check_handler_bind ctx ?want loc clauses body
|
||||||
|
| Ast.HandlerCase (body, clauses) -> check_handler_case ctx ?want loc body clauses
|
||||||
|
|
||||||
(* spec-conditions.md §3–§6: the transfer. Neither of these is a call — one
|
(* spec-conditions.md §3–§6: the transfer. Neither of these is a call — one
|
||||||
establishes frames around a body, and the other leaves the function it is
|
establishes frames around a body, and the other leaves the function it is
|
||||||
@ -2404,9 +2405,15 @@ and check_fn ctx ~want loc (params : string list) body =
|
|||||||
The body may not [return] either. The frames are pushed and popped around
|
The body may not [return] either. The frames are pushed and popped around
|
||||||
it, and an early exit would leave them on the stack pointing into a function
|
it, and an early exit would leave them on the stack pointing into a function
|
||||||
that has gone. *)
|
that has gone. *)
|
||||||
and check_handler_bind ctx ?want loc clauses body =
|
and check_handler_bind ctx ?want ?(what = "handler-bind") loc clauses body =
|
||||||
let frames =
|
let frames =
|
||||||
List.map
|
(* Left to right, and not [List.map], whose order is unspecified: each of
|
||||||
|
these calls lifts a function onto [ctx.env.lifted] and names it after
|
||||||
|
the count already there, so an order nobody chose would number the
|
||||||
|
clauses of one handler-bind differently between builds. The names go in
|
||||||
|
a redefinition module, which is where that would be noticed — see the
|
||||||
|
argument in [check_fn]. *)
|
||||||
|
map_lr
|
||||||
(fun (c : Ast.hclause) ->
|
(fun (c : Ast.hclause) ->
|
||||||
let ty = resolve ctx.env c.Ast.hty in
|
let ty = resolve ctx.env c.Ast.hty in
|
||||||
let name =
|
let name =
|
||||||
@ -2471,16 +2478,20 @@ and check_handler_bind ctx ?want loc clauses body =
|
|||||||
into a record the function never sees again and the indices would
|
into a record the function never sees again and the indices would
|
||||||
collide. *)
|
collide. *)
|
||||||
let saved = ctx.in_frames in
|
let saved = ctx.in_frames in
|
||||||
ctx.in_frames <- Some "handler-bind";
|
(* [what] is the form the reader wrote. A handler-case establishes its
|
||||||
|
frames through this function, so a [return] under one has to be refused
|
||||||
|
naming handler-case rather than naming the machinery underneath it. *)
|
||||||
|
ctx.in_frames <- Some what;
|
||||||
(* The body's last form is the form's value, which is [with-allocator]'s
|
(* The body's last form is the form's value, which is [with-allocator]'s
|
||||||
shape and for the same reason: both wrap a body in something established
|
shape and for the same reason: both wrap a body in something established
|
||||||
around it and taken off after, and neither is a reason for the body to
|
around it and taken off after, and neither is a reason for the body to
|
||||||
stop being an expression. §3 needs it — a [restart-case] whose body is a
|
stop being an expression. §3 needs it — a [restart-case] whose body is a
|
||||||
[handler-bind] has to agree in type with its clauses, which is how all
|
[handler-bind] has to agree in type with its clauses, which is how all
|
||||||
four of this repository's crossing probes are written — and it is what
|
four of this repository's crossing probes are written — and it is what
|
||||||
[handler-case] will *not* be: that one's value is the handler's, which is
|
[handler-case] is *not*: that one's value is its clause's, which is the
|
||||||
the whole difference between the two and is why it is still refused by
|
whole difference between the two. [check_handler_case] below builds one
|
||||||
name in [parse.ml].
|
out of this form and a [restart-case], so both spellings run through
|
||||||
|
here and only the clause's landing place differs.
|
||||||
|
|
||||||
This used to be [ignore want] and a flat [Types.Unit], and nothing
|
This used to be [ignore want] and a flat [Types.Unit], and nothing
|
||||||
complained, because a unit in value position is only caught where the
|
complained, because a unit in value position is only caught where the
|
||||||
@ -2489,7 +2500,7 @@ and check_handler_bind ctx ?want loc clauses body =
|
|||||||
machine whatever the body's last form had left in the slot. Neither was a
|
machine whatever the body's last form had left in the slot. Neither was a
|
||||||
value; one of them merely looked like one. *)
|
value; one of them merely looked like one. *)
|
||||||
let body, ty =
|
let body, ty =
|
||||||
barrier ctx "a handler-bind" (fun () ->
|
barrier ctx ("a " ^ what) (fun () ->
|
||||||
let rec go = function
|
let rec go = function
|
||||||
| [] -> [ unit_at loc ], Types.Unit
|
| [] -> [ unit_at loc ], Types.Unit
|
||||||
| [ last ] -> let l = check ctx ?want last in [ l ], l.Tast.ty
|
| [ last ] -> let l = check ctx ?want last in [ l ], l.Tast.ty
|
||||||
@ -2520,6 +2531,13 @@ and check_restart_case ctx ?want loc body clauses =
|
|||||||
ctx.in_frames <- Some "restart-case";
|
ctx.in_frames <- Some "restart-case";
|
||||||
let tbody = barrier ctx "a restart-case" (fun () -> check ctx ?want body) in
|
let tbody = barrier ctx "a restart-case" (fun () -> check ctx ?want body) in
|
||||||
ctx.in_frames <- saved;
|
ctx.in_frames <- saved;
|
||||||
|
restart_clauses ctx ?want ~what:"restart-case" loc tbody clauses
|
||||||
|
|
||||||
|
(* The clauses of a [restart-case], checked against a body that has already
|
||||||
|
been checked. Separate from the form above because [handler-case] supplies
|
||||||
|
its own body — a [handler-bind] it built — and has to name itself in the
|
||||||
|
refusals rather than naming the machinery it is made of. *)
|
||||||
|
and restart_clauses ctx ?want ~what loc (tbody : Tast.expr) clauses =
|
||||||
(* With no expectation from outside, the body's own type is the expectation
|
(* With no expectation from outside, the body's own type is the expectation
|
||||||
the clauses are checked against — unless it produced no value at all, in
|
the clauses are checked against — unless it produced no value at all, in
|
||||||
which case the first clause that does decides. *)
|
which case the first clause that does decides. *)
|
||||||
@ -2537,7 +2555,7 @@ and check_restart_case ctx ?want loc body clauses =
|
|||||||
the name" pick between them by an order nothing in the source
|
the name" pick between them by an order nothing in the source
|
||||||
shows. *)
|
shows. *)
|
||||||
if List.mem c.Ast.rname !seen then
|
if List.mem c.Ast.rname !seen then
|
||||||
fail c.Ast.rloc "this restart-case offers %s twice" c.Ast.rname;
|
fail c.Ast.rloc "this %s offers %s twice" what c.Ast.rname;
|
||||||
seen := c.Ast.rname :: !seen;
|
seen := c.Ast.rname :: !seen;
|
||||||
(* §3's parameters. They are slots in *this* function — a clause runs
|
(* §3's parameters. They are slots in *this* function — a clause runs
|
||||||
here, not where the invoke was — and the invoker stores into a
|
here, not where the invoke was — and the invoker stores into a
|
||||||
@ -2567,7 +2585,7 @@ and check_restart_case ctx ?want loc body clauses =
|
|||||||
(* The same barrier the body gets, and for the same reason: a
|
(* The same barrier the body gets, and for the same reason: a
|
||||||
clause runs after a transfer landed at this restart-case, with
|
clause runs after a transfer landed at this restart-case, with
|
||||||
its frames still to be popped. *)
|
its frames still to be popped. *)
|
||||||
barrier ctx "a restart-case"
|
barrier ctx ("a " ^ what)
|
||||||
(fun () -> block ctx ?want:!ty c.Ast.rloc c.Ast.rbody)))
|
(fun () -> block ctx ?want:!ty c.Ast.rloc c.Ast.rbody)))
|
||||||
in
|
in
|
||||||
if !ty = None && b.Tast.ty <> Types.Never then ty := Some b.Tast.ty;
|
if !ty = None && b.Tast.ty <> Types.Never then ty := Some b.Tast.ty;
|
||||||
@ -2579,6 +2597,119 @@ and check_restart_case ctx ?want loc body clauses =
|
|||||||
let ty = match !ty with Some t -> t | None -> Types.Never in
|
let ty = match !ty with Some t -> t | None -> Types.Never in
|
||||||
mk loc ty (Tast.RestartCase (clauses, tbody))
|
mk loc ty (Tast.RestartCase (clauses, tbody))
|
||||||
|
|
||||||
|
(* (handler-case BODY [(Type [c] BODY-1) ...]) — the unwinding handler, and
|
||||||
|
spec-conditions.md's one remaining open question about it, answered: it *is*
|
||||||
|
a handler-bind plus a transfer, built here rather than given nodes and
|
||||||
|
backends of its own.
|
||||||
|
|
||||||
|
(handler-case B [(T [c] A)])
|
||||||
|
== (restart-case (handler-bind [(T [c] (invoke-restart 'R c))] B)
|
||||||
|
(R [c T] A))
|
||||||
|
|
||||||
|
That is Common Lisp's own definition of the operator, and every property
|
||||||
|
this form is supposed to have falls out of the two it is made of rather
|
||||||
|
than being re-implemented beside them.
|
||||||
|
|
||||||
|
- The clause runs *here*, at the handler-case, because a restart clause
|
||||||
|
does; so it sees this function's locals, which a handler clause cannot,
|
||||||
|
and its value is the whole form's, because a restart-case's clause value
|
||||||
|
is the whole restart-case's.
|
||||||
|
- The stack below is gone by then. §5's defers, and the allocator a
|
||||||
|
[with-allocator] rebound, are honoured on the way out because that is
|
||||||
|
what a transfer already does for every frame it leaves.
|
||||||
|
- A condition matching no clause installs no frame, so nothing here sees
|
||||||
|
it and it keeps going outward exactly as it would have.
|
||||||
|
- The body and every clause agree on one type, because §3 already says a
|
||||||
|
restart-case's body and clauses do. A clause that disagrees is refused
|
||||||
|
where it is written, like an [if] whose arms disagree.
|
||||||
|
|
||||||
|
The condition crosses as a restart argument, which means by value into a
|
||||||
|
buffer this frame owns — which is the only thing that can work, since §5
|
||||||
|
kills the signalling frame the condition was living on the moment the
|
||||||
|
transfer starts.
|
||||||
|
|
||||||
|
The restart the two halves meet over is named after this function and
|
||||||
|
numbered within it, and the number is the count of handler clauses already
|
||||||
|
lifted out of this function. That count never goes down, and every
|
||||||
|
handler-case lifts at least one clause before the next one can read it, so
|
||||||
|
within a name's own bucket the numbers are strictly increasing and no two
|
||||||
|
forms can mint the same name. A clause body written inside another handler
|
||||||
|
clause counts against the [<none>] bucket rather than against a function's,
|
||||||
|
which is the same argument again and not a hole: that bucket is one list
|
||||||
|
for the whole program and it only grows.
|
||||||
|
|
||||||
|
Uniqueness is the requirement rather than a nicety. Two handler-cases
|
||||||
|
sharing a name, one inside the other's extent, would have the inner frame
|
||||||
|
shadow the outer one (§4), which lands a condition at the wrong form —
|
||||||
|
and, because the two would be expecting different condition types, lands it
|
||||||
|
as a run-time signature refusal rather than as a wrong answer. *)
|
||||||
|
and check_handler_case ctx ?want loc body clauses =
|
||||||
|
let what = "handler-case" in
|
||||||
|
(* Resolved once here, for the refusal below; [check_handler_bind] resolves
|
||||||
|
them again for the frames, which is cheap and keeps that function whole. *)
|
||||||
|
let names =
|
||||||
|
List.map
|
||||||
|
(fun (c : Ast.hclause) ->
|
||||||
|
match resolve ctx.env c.Ast.hty with
|
||||||
|
| Types.Named n -> n
|
||||||
|
| t ->
|
||||||
|
fail c.Ast.hloc
|
||||||
|
"a handler matches a struct type, not %s" (Types.to_string t))
|
||||||
|
clauses
|
||||||
|
in
|
||||||
|
(* Two clauses for one condition type: the first would take every one of
|
||||||
|
them and the second could never run, and nothing in the source says which
|
||||||
|
the reader meant. The same refusal a duplicate restart name gets, and for
|
||||||
|
the same reason. *)
|
||||||
|
let seen = ref [] in
|
||||||
|
List.iter2
|
||||||
|
(fun (c : Ast.hclause) n ->
|
||||||
|
if List.mem n !seen then
|
||||||
|
fail c.Ast.hloc "this handler-case handles %s twice" n;
|
||||||
|
seen := n :: !seen)
|
||||||
|
clauses names;
|
||||||
|
let k =
|
||||||
|
List.length
|
||||||
|
(List.filter
|
||||||
|
(fun (l : Tast.fn) ->
|
||||||
|
l.Tast.fparent = Some ctx.owner
|
||||||
|
&& String.length l.Tast.name >= 8
|
||||||
|
&& String.sub l.Tast.name 0 8 = "handler/")
|
||||||
|
ctx.env.lifted)
|
||||||
|
in
|
||||||
|
let rnames =
|
||||||
|
List.map (Printf.sprintf "handler-case/%s/%d/%s" ctx.owner k) names
|
||||||
|
in
|
||||||
|
(* The handler half: one clause per arm, whose whole body is the transfer.
|
||||||
|
It is lifted into a function of its own like any handler clause, and the
|
||||||
|
condition it was handed is copied into the restart frame's buffer on its
|
||||||
|
way out. *)
|
||||||
|
let handlers =
|
||||||
|
List.map2
|
||||||
|
(fun (c : Ast.hclause) r ->
|
||||||
|
{ c with
|
||||||
|
Ast.hbody =
|
||||||
|
[ { Ast.e =
|
||||||
|
Ast.InvokeRestart
|
||||||
|
(r, [ { Ast.e = Ast.Var c.Ast.hname; loc = c.Ast.hloc } ]);
|
||||||
|
loc = c.Ast.hloc } ] })
|
||||||
|
clauses rnames
|
||||||
|
in
|
||||||
|
(* The landing half: one restart clause per arm, taking the condition as its
|
||||||
|
single parameter and running what the reader actually wrote. *)
|
||||||
|
let landings =
|
||||||
|
List.map2
|
||||||
|
(fun (c : Ast.hclause) r ->
|
||||||
|
{ Ast.rname = r;
|
||||||
|
rparams =
|
||||||
|
[ { Ast.fname = c.Ast.hname; fty = c.Ast.hty;
|
||||||
|
floc = c.Ast.hloc } ];
|
||||||
|
rbody = c.Ast.hbody; rloc = c.Ast.hloc })
|
||||||
|
clauses rnames
|
||||||
|
in
|
||||||
|
let tbody = check_handler_bind ctx ?want ~what loc handlers [ body ] in
|
||||||
|
restart_clauses ctx ?want ~what loc tbody landings
|
||||||
|
|
||||||
(* The forms of a [defer], checked in place and hung on the function. It emits
|
(* The forms of a [defer], checked in place and hung on the function. It emits
|
||||||
nothing where it stands, so what is left behind is [unit]. *)
|
nothing where it stands, so what is left behind is [unit]. *)
|
||||||
and register_defer ctx loc forms =
|
and register_defer ctx loc forms =
|
||||||
|
|||||||
18
lib/load.ml
18
lib/load.ml
@ -316,6 +316,19 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
|
|||||||
c.Ast.hbody })
|
c.Ast.hbody })
|
||||||
clauses,
|
clauses,
|
||||||
gos body)
|
gos body)
|
||||||
|
(* The same clause shape, and the same rewrite — the difference between the
|
||||||
|
two forms is where a clause runs, which an import does not see. *)
|
||||||
|
| Ast.HandlerCase (body, clauses) ->
|
||||||
|
Ast.HandlerCase
|
||||||
|
(go body,
|
||||||
|
List.map
|
||||||
|
(fun (c : Ast.hclause) ->
|
||||||
|
{ c with
|
||||||
|
Ast.hty = rename_texpr owned alias c.Ast.hty;
|
||||||
|
hbody =
|
||||||
|
List.map (rename_expr owned alias (c.Ast.hname :: bound))
|
||||||
|
c.Ast.hbody })
|
||||||
|
clauses)
|
||||||
in
|
in
|
||||||
{ e with Ast.e = k }
|
{ e with Ast.e = k }
|
||||||
|
|
||||||
@ -680,6 +693,11 @@ let rec expr_uses acc (e : Ast.expr) =
|
|||||||
(fun (c : Ast.hclause) -> texpr_uses acc c.Ast.hty; gos c.Ast.hbody)
|
(fun (c : Ast.hclause) -> texpr_uses acc c.Ast.hty; gos c.Ast.hbody)
|
||||||
clauses;
|
clauses;
|
||||||
gos body
|
gos body
|
||||||
|
| Ast.HandlerCase (body, clauses) ->
|
||||||
|
go body;
|
||||||
|
List.iter
|
||||||
|
(fun (c : Ast.hclause) -> texpr_uses acc c.Ast.hty; gos c.Ast.hbody)
|
||||||
|
clauses
|
||||||
|
|
||||||
(* A place carries no location of its own, so it borrows the [set] form's. *)
|
(* A place carries no location of its own, so it borrows the [set] form's. *)
|
||||||
and place_uses acc loc (p : Ast.place) =
|
and place_uses acc loc (p : Ast.place) =
|
||||||
|
|||||||
38
lib/parse.ml
38
lib/parse.ml
@ -420,6 +420,35 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
|||||||
in
|
in
|
||||||
mk (Ast.HandlerBind (List.map clause clauses, body_of body))
|
mk (Ast.HandlerBind (List.map clause clauses, body_of body))
|
||||||
|
|
||||||
|
(* (handler-case BODY [(Type [c] body ...) ...]) — spec-conditions.md, the
|
||||||
|
unwinding half of the pair.
|
||||||
|
|
||||||
|
The body comes first and the clauses after it, which is the opposite of
|
||||||
|
handler-bind's order and is deliberate: a handler-bind is read as
|
||||||
|
something established *around* a body, and a handler-case is read as a
|
||||||
|
body with answers hung off the end of it. A clause is spelled exactly as
|
||||||
|
handler-bind spells one, because it names the same thing — a condition
|
||||||
|
type and a name to bind it to. At least one clause, since a handler-case
|
||||||
|
with none would be its body and nothing else. *)
|
||||||
|
| Sym "handler-case" ->
|
||||||
|
let body, clauses =
|
||||||
|
match args with
|
||||||
|
| [ body; { v = Vec clauses; _ } ] when clauses <> [] -> (body, clauses)
|
||||||
|
| _ ->
|
||||||
|
fail f
|
||||||
|
"handler-case is (handler-case body [(Type [name] body ...) ...]) \
|
||||||
|
with at least one clause"
|
||||||
|
in
|
||||||
|
let clause (c : Form.t) =
|
||||||
|
match c.Form.v with
|
||||||
|
| Form.List (ty :: { v = Form.Vec [ { v = Form.Sym n; _ } ]; _ } :: cbody)
|
||||||
|
when cbody <> [] ->
|
||||||
|
{ Ast.hty = texpr ty; hname = n; hbody = List.map expr cbody;
|
||||||
|
hloc = c.Form.loc }
|
||||||
|
| _ -> fail c "a handler-case clause is (Type [name] body ...)"
|
||||||
|
in
|
||||||
|
mk (Ast.HandlerCase (expr body, List.map clause clauses))
|
||||||
|
|
||||||
(* (restart-case BODY (name [p T ...] BODY-1) ...) — spec-conditions.md §3.
|
(* (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
|
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
|
clause's parameters are inline name/type pairs, like any other binding
|
||||||
@ -499,12 +528,9 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
|||||||
|
|
||||||
(* Recognised, deliberately unimplemented. Rejected rather than left to fall
|
(* Recognised, deliberately unimplemented. Rejected rather than left to fall
|
||||||
through to Call, where they would parse and mean nothing. *)
|
through to Call, where they would parse and mean nothing. *)
|
||||||
| Sym ("handler-case"
|
(* [find-restart] and [compute-restarts] are §4's two ways to look at the
|
||||||
(* Named in the spec and not written yet, so each says so rather than
|
restart stack without committing to one. *)
|
||||||
falling through to Call and coming back as an unknown name:
|
| Sym ("find-restart" | "compute-restarts"
|
||||||
[find-restart] and [compute-restarts] are §4's two ways to look at
|
|
||||||
the restart stack without committing to one. *)
|
|
||||||
| "find-restart" | "compute-restarts"
|
|
||||||
| "errdefer"
|
| "errdefer"
|
||||||
| "await" as name) ->
|
| "await" as name) ->
|
||||||
fail f "%s is not implemented yet (see the build sequence in plan.org)" name
|
fail f "%s is not implemented yet (see the build sequence in plan.org)" name
|
||||||
|
|||||||
@ -179,7 +179,14 @@ and expr_kind =
|
|||||||
and [Load] already do, which a list hanging off a node they treat as a
|
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]
|
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
|
their hash — §3's run-time check, since the name is resolved on a stack
|
||||||
nothing static can see. *)
|
nothing static can see.
|
||||||
|
|
||||||
|
Not every one of these was written as a [restart-case]. A [handler-case]
|
||||||
|
is a [Handled] whose clause invokes a restart, wrapped in one of these to
|
||||||
|
catch it — see [Check.check_handler_case] — so the unwinding handler
|
||||||
|
reaches a backend as this node and needs nothing of its own. A clause
|
||||||
|
whose name begins with [handler-case/] is one the checker made up for
|
||||||
|
that. *)
|
||||||
| RestartCase of rclause list * expr
|
| RestartCase of rclause list * expr
|
||||||
(* (with-allocator A BODY...) — spec-memory.md. It rebinds the current
|
(* (with-allocator A BODY...) — spec-memory.md. It rebinds the current
|
||||||
allocator for its dynamic extent and releases nothing. Its own node
|
allocator for its dynamic extent and releases nothing. Its own node
|
||||||
|
|||||||
@ -219,6 +219,34 @@ per-frame slot nests correctly with no threads involved.
|
|||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
Condition inheritance/predicate-matching details, the break-loop UI, restart
|
Condition inheritance/predicate-matching details, the break-loop UI, and restart
|
||||||
interaction with threads, and whether `handler-case` should be a macro over
|
interaction with threads. None of these block milestone 5.
|
||||||
`handler-bind` + a transfer. None of these block milestone 5.
|
|
||||||
|
`handler-case` **is** a `handler-bind` plus a transfer, decided 2026-09-19 and
|
||||||
|
built that way. `(handler-case B [(T [c] A)])` is
|
||||||
|
|
||||||
|
```
|
||||||
|
(restart-case (handler-bind [(T [c] (invoke-restart 'R c))] B)
|
||||||
|
(R [c T] A))
|
||||||
|
```
|
||||||
|
|
||||||
|
with `R` a name the form makes up for itself, which is Common Lisp's own
|
||||||
|
definition of the operator. Everything the unwinding form needs it inherits
|
||||||
|
rather than re-implements: §5's defers and the `with-allocator` restore,
|
||||||
|
because a transfer already runs both for every frame it leaves; §3's rule that
|
||||||
|
the body and every clause share one type; §4's shadowing, which is why the name
|
||||||
|
has to be unique per form; and both backends, which needed no new node. The
|
||||||
|
clause runs at the `handler-case` and therefore sees the establishing
|
||||||
|
function's locals, which a `handler-bind` clause cannot — that is the whole of
|
||||||
|
the difference between the two, and it is a consequence of where a restart
|
||||||
|
clause runs rather than something arranged for it.
|
||||||
|
|
||||||
|
The condition crosses as the restart's single argument, which means by value
|
||||||
|
into a buffer the form owns. §5 requires it: the signalling frame the condition
|
||||||
|
was living on dies the moment the transfer starts.
|
||||||
|
|
||||||
|
The visible cost is that the made-up restart is on the restart stack like any
|
||||||
|
other, so a break loop entered under a `handler-case` lists it. Taking it from
|
||||||
|
there is refused loudly — nothing filled its argument buffer in — rather than
|
||||||
|
answered wrongly, and hiding it would mean a field in a frame layout spelled
|
||||||
|
out in three places. Left as it is.
|
||||||
|
|||||||
231
test/programs/handler-case.flan
Normal file
231
test/programs/handler-case.flan
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
;;;; handler-case — the unwinding handler, spec-conditions.md.
|
||||||
|
;;;;
|
||||||
|
;;;; handler-bind runs its clause at the signal, with everything below still
|
||||||
|
;;;; standing, and carries on from there. This one is the other half: a listed
|
||||||
|
;;;; condition unwinds the stack back to the form, the clause runs *here*, and
|
||||||
|
;;;; its value is the value of the whole handler-case. Clojure's try/catch and
|
||||||
|
;;;; Common Lisp's handler-case, and built out of the two operators that were
|
||||||
|
;;;; already here — a handler-bind whose clause invokes a restart the form
|
||||||
|
;;;; established around itself.
|
||||||
|
;;;;
|
||||||
|
;;;; What this program pins is the list of things that only an unwind can get
|
||||||
|
;;;; wrong: the defers between the signal and the form, a condition nobody
|
||||||
|
;;;; listed carrying on outward untouched, the two nestings against
|
||||||
|
;;;; handler-bind, and a clause that signals — which must not be caught by the
|
||||||
|
;;;; handler-case it belongs to, because by the time it runs that form's frames
|
||||||
|
;;;; are off the stack.
|
||||||
|
;;;;
|
||||||
|
;;;; [log] is a digit trace rather than a running sum, which is cleanup.flan's
|
||||||
|
;;;; device and is here for cleanup.flan's reason: a sum commutes, so a backend
|
||||||
|
;;;; that ran the defers outermost-first would print exactly the same total as
|
||||||
|
;;;; one that got them right. A shift records the *order* and a wrong order is
|
||||||
|
;;;; a different number.
|
||||||
|
(defstruct Missing [id i32])
|
||||||
|
(defstruct Corrupt [id i32])
|
||||||
|
(defstruct Late [id i32])
|
||||||
|
|
||||||
|
(defvar log i64)
|
||||||
|
(defvar frame Allocator)
|
||||||
|
|
||||||
|
(defn note [n i64] () (set log (+ (* log 10) n)))
|
||||||
|
|
||||||
|
;;; Two frames below any handler-case here, each with a defer, so an unwind has
|
||||||
|
;;; something to cross and leaves a mark saying it crossed it — and says in
|
||||||
|
;;; which order it crossed them (§5: innermost first).
|
||||||
|
(defn inner [n i32] i32
|
||||||
|
(defer (note 1))
|
||||||
|
(error (Missing {.id n}))
|
||||||
|
0)
|
||||||
|
|
||||||
|
(defn middle [n i32] i32
|
||||||
|
(defer (note 2))
|
||||||
|
(+ (inner n) 1))
|
||||||
|
|
||||||
|
;;; Nothing signals: the body's own value stands, which is the case a form that
|
||||||
|
;;; only ever answers its clauses would quietly get wrong.
|
||||||
|
(defn quiet [n i32] i32
|
||||||
|
(handler-case (+ n 1)
|
||||||
|
[(Missing [c] -1)]))
|
||||||
|
|
||||||
|
;;; Caught, and the clause reads a local of the function that established the
|
||||||
|
;;; form. That is the whole difference from handler-bind, whose clause is
|
||||||
|
;;; lifted into a function of its own and can see no such thing.
|
||||||
|
(defn caught [n i32] i32
|
||||||
|
(let [bonus 100]
|
||||||
|
(handler-case (middle n)
|
||||||
|
[(Missing [c] (+ bonus (.id c)))])))
|
||||||
|
|
||||||
|
;;; Two clauses, and the one whose type was signalled is the one that runs.
|
||||||
|
(defn raise [k i32] i32
|
||||||
|
(cond
|
||||||
|
(= k 0) (error (Missing {.id 1}))
|
||||||
|
(= k 1) (error (Corrupt {.id 2}))
|
||||||
|
:else (error (Late {.id 3}))))
|
||||||
|
|
||||||
|
(defn two [k i32] i32
|
||||||
|
(handler-case (raise k)
|
||||||
|
[(Missing [c] (+ 100 (.id c)))
|
||||||
|
(Corrupt [c] (+ 200 (.id c)))]))
|
||||||
|
|
||||||
|
;;; A condition no clause lists installs no frame that matches it, so this form
|
||||||
|
;;; never sees it and it goes on outward unchanged. The outer handler-bind is
|
||||||
|
;;; what proves it arrived, and the 7 is what proves signal still returned ()
|
||||||
|
;;; and the body carried on from where it was.
|
||||||
|
(defn unmatched [n i32] i32
|
||||||
|
(handler-case
|
||||||
|
(do (signal (Corrupt {.id n}))
|
||||||
|
7)
|
||||||
|
[(Missing [c] -1)]))
|
||||||
|
|
||||||
|
;;; A handler-case inside a handler-bind. The Corrupt goes out to the
|
||||||
|
;;; handler-bind, which returns normally, so the body carries on; the Missing
|
||||||
|
;;; that follows unwinds to the handler-case in between.
|
||||||
|
(defn hc-in-hb [n i32] i32
|
||||||
|
(handler-bind [(Corrupt [c] (note 4))]
|
||||||
|
(handler-case
|
||||||
|
(do (signal (Corrupt {.id n}))
|
||||||
|
(error (Missing {.id n}))
|
||||||
|
0)
|
||||||
|
[(Missing [c] (.id c))])))
|
||||||
|
|
||||||
|
;;; And the other way round. The inner handler-bind is on the path the unwind
|
||||||
|
;;; takes, so its frame has to come off as the transfer passes through it —
|
||||||
|
;;; which is the same landing pad a restart transfer already uses.
|
||||||
|
(defn hb-in-hc [n i32] i32
|
||||||
|
(handler-case
|
||||||
|
(handler-bind [(Corrupt [c] (note 5))]
|
||||||
|
(do (signal (Corrupt {.id n}))
|
||||||
|
(error (Missing {.id n}))
|
||||||
|
0))
|
||||||
|
[(Missing [c] (* 2 (.id c)))]))
|
||||||
|
|
||||||
|
;;; A clause that signals. It runs with its own handler-case's frames already
|
||||||
|
;;; off the stack, so this Missing must not be caught here — that would be an
|
||||||
|
;;; unbounded loop rather than a wrong number. It has to leave this function,
|
||||||
|
;;; running the defer below on the way, and land further out.
|
||||||
|
(defn arm-signals [n i32] i32
|
||||||
|
(defer (note 3))
|
||||||
|
(handler-case (middle n)
|
||||||
|
[(Missing [c] (do (signal (Missing {.id 99}))
|
||||||
|
(.id c)))]))
|
||||||
|
|
||||||
|
(defn arm-caught-outside [n i32] i32
|
||||||
|
(handler-case (arm-signals n)
|
||||||
|
[(Missing [c] (+ 5000 (.id c)))]))
|
||||||
|
|
||||||
|
;;; A clause that returns. A clause runs at the form, in the function that
|
||||||
|
;;; wrote it, so a [return] there is an ordinary return from *this* function —
|
||||||
|
;;; not the refusal the body gets, where the frames are still standing. It has
|
||||||
|
;;; to run this function's own defer on the way out, after the two the unwind
|
||||||
|
;;; already ran, and it has to leave nothing on the handler stack: main signals
|
||||||
|
;;; once more afterwards with nothing listening, which would be a call into a
|
||||||
|
;;; frame that has gone if anything leaked.
|
||||||
|
(defn return-from-clause [n i32] i32
|
||||||
|
(defer (note 9))
|
||||||
|
(handler-case (middle n)
|
||||||
|
[(Missing [c] (return (+ 700 (.id c))))])
|
||||||
|
0)
|
||||||
|
|
||||||
|
;;; A handler-case inside a defer. A defer is itself the cleanup an unwind
|
||||||
|
;;; runs, so establishing frames in one has to work like establishing them
|
||||||
|
;;; anywhere — what a defer may not do is start a transfer that leaves it, and
|
||||||
|
;;; this one begins and ends inside. Two defers, so the order is pinned here
|
||||||
|
;;; too: the handler-case one is innermost and notes first.
|
||||||
|
(defn hc-in-defer [n i32] i32
|
||||||
|
(defer (note 8))
|
||||||
|
(defer (note (i64 (handler-case (error (Corrupt {.id n}))
|
||||||
|
[(Corrupt [d] (.id d))]))))
|
||||||
|
n)
|
||||||
|
|
||||||
|
;;; Two handler-cases written inside handler-bind clauses. Such a clause is
|
||||||
|
;;; lifted into a function of its own, so both of these mint their made-up
|
||||||
|
;;; restart name out of one shared bucket rather than out of a function's —
|
||||||
|
;;; which is exactly where two forms landing on the same name would show, the
|
||||||
|
;;; inner shadowing the outer wherever their extents overlapped. Each notes the
|
||||||
|
;;; id of the condition *it* caught, so the trace says they are two names.
|
||||||
|
(defn in-clause-a [] ()
|
||||||
|
(handler-bind
|
||||||
|
[(Late [c]
|
||||||
|
(note (i64 (handler-case (error (Corrupt {.id 6}))
|
||||||
|
[(Corrupt [d] (.id d))]))))]
|
||||||
|
(signal (Late {.id 0}))))
|
||||||
|
|
||||||
|
(defn in-clause-b [] ()
|
||||||
|
(handler-bind
|
||||||
|
[(Late [c]
|
||||||
|
(note (i64 (handler-case (error (Corrupt {.id 7}))
|
||||||
|
[(Corrupt [d] (.id d))]))))]
|
||||||
|
(signal (Late {.id 0}))))
|
||||||
|
|
||||||
|
;;; A with-allocator on the way out. It rebinds the context allocator for its
|
||||||
|
;;; extent and the transfer passes straight through it, so the restore has to
|
||||||
|
;;; happen on that path as well as on the normal one — otherwise the clause,
|
||||||
|
;;; and everything after the whole form, would be allocating out of a region
|
||||||
|
;;; nobody else knows about. main destroys the arena before it allocates
|
||||||
|
;;; again, so a context left pointing at it would not be a wrong number.
|
||||||
|
(defn scoped [n i32] i32
|
||||||
|
(handler-case
|
||||||
|
(with-allocator frame
|
||||||
|
(let [v (vec-new i32)]
|
||||||
|
(push v 1)
|
||||||
|
(error (Missing {.id n}))
|
||||||
|
(i32 (len v))))
|
||||||
|
[(Missing [c] (+ 20 (.id c)))]))
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
;; Normal completion.
|
||||||
|
(print (quiet 41)) (println "")
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; Caught, with both defers between the signal and the form having run, and
|
||||||
|
;; the trace saying inner's ran before middle's.
|
||||||
|
(print (caught 5)) (println "")
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; The arm that matches is the arm that runs.
|
||||||
|
(print (two 0)) (println "")
|
||||||
|
(print (two 1)) (println "")
|
||||||
|
|
||||||
|
;; Unmatched: past this form and out to a handler-bind around it, and the
|
||||||
|
;; body's own value still stands.
|
||||||
|
(handler-bind [(Corrupt [c] (note 6))]
|
||||||
|
(print (unmatched 3)) (println ""))
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; The two nestings. Neither signals from under [middle], so what each adds
|
||||||
|
;; to the trace is its own handler-bind's clause and nothing else.
|
||||||
|
(print (hc-in-hb 6)) (println "")
|
||||||
|
(print (hb-in-hc 8)) (println "")
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; A clause that signals, caught by the handler-case outside it. Two unwinds,
|
||||||
|
;; so both defers under [middle] run and then the one in [arm-signals] does.
|
||||||
|
(print (arm-caught-outside 4)) (println "")
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; A clause that returns, and then a signal nothing is listening for. The
|
||||||
|
;; second is the leak check: it must be the no-op §2 says it is.
|
||||||
|
(print (return-from-clause 5)) (println "")
|
||||||
|
(print log) (println "")
|
||||||
|
(signal (Missing {.id 0}))
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; A handler-case established inside a defer.
|
||||||
|
(print (hc-in-defer 4)) (println "")
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; Two forms out of the one bucket of made-up names, each answering its own.
|
||||||
|
(in-clause-a)
|
||||||
|
(in-clause-b)
|
||||||
|
(print log) (println "")
|
||||||
|
|
||||||
|
;; And the allocator scope. The arena is destroyed straight after, so the
|
||||||
|
;; heap allocation below is only possible if the context was put back.
|
||||||
|
(set frame (arena-new 4096))
|
||||||
|
(print (scoped 3)) (println "")
|
||||||
|
(arena-destroy frame)
|
||||||
|
(let [h (vec-new i32)]
|
||||||
|
(push h 9)
|
||||||
|
(print (len h)) (println "")
|
||||||
|
(free h))
|
||||||
|
0)
|
||||||
@ -448,6 +448,37 @@ let () =
|
|||||||
in
|
in
|
||||||
restart_mismatch ();
|
restart_mismatch ();
|
||||||
restart_mismatch ~opt:"-O0" ();
|
restart_mismatch ~opt:"-O0" ();
|
||||||
|
(* handler-case, the unwinding half of the pair. It is a handler-bind whose
|
||||||
|
clause invokes a restart the form established around itself, so what
|
||||||
|
these rows are really asserting is that the composition holds together
|
||||||
|
at the places an unwind can come apart: the defers between the signal
|
||||||
|
and the form, a condition nobody listed carrying on outward with the
|
||||||
|
body still running after it, a handler-bind on the path out having its
|
||||||
|
frame taken off as the transfer passes through, and a clause that
|
||||||
|
signals landing outside the form it belongs to rather than back in it.
|
||||||
|
A clause that returns, which is an ordinary return from the function
|
||||||
|
that wrote the form and must leave the handler stack empty behind it.
|
||||||
|
And two of them written inside handler-bind clauses, which is where the
|
||||||
|
made-up restart names come out of one shared bucket and where two forms
|
||||||
|
minting the same one would show.
|
||||||
|
At -O0 as well, because the guard after every call is control flow the
|
||||||
|
optimiser would otherwise launder, and under --x86, where the transfer
|
||||||
|
exit and the with-allocator restore share one epilogue and there is no
|
||||||
|
second copy to forget.
|
||||||
|
|
||||||
|
The long numbers are cleanup.flan's digit trace and not a running sum,
|
||||||
|
which is what makes them worth comparing: a sum commutes, so defers run
|
||||||
|
outermost-first would total the same, and a trace that says 12 where the
|
||||||
|
order is wrong says 21. *)
|
||||||
|
let handler_case_out =
|
||||||
|
"42\n0\n105\n12\n101\n202\n7\n126\n6\n16\n12645\n5099\n12645123\n705\n\
|
||||||
|
12645123129\n12645123129\n4\n1264512312948\n126451231294867\n23\n1\n"
|
||||||
|
in
|
||||||
|
outputs "handler-case" "programs/handler-case.flan" handler_case_out;
|
||||||
|
outputs ~opt:"-O0" "handler-case, -O0" "programs/handler-case.flan"
|
||||||
|
handler_case_out;
|
||||||
|
outputs ~x86:true "handler-case, --x86" "programs/handler-case.flan"
|
||||||
|
handler_case_out;
|
||||||
(* The other way a transfer starts is the break loop, which chooses a
|
(* 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
|
restart by position and has nothing to fill parameters in with. It
|
||||||
reaches the clause through the same channel an invoke-restart writes, so
|
reaches the clause through the same channel an invoke-restart writes, so
|
||||||
|
|||||||
@ -1653,10 +1653,94 @@ let () =
|
|||||||
(fun (name, src) ->
|
(fun (name, src) ->
|
||||||
rejects_check (name ^ " is still unimplemented") src
|
rejects_check (name ^ " is still unimplemented") src
|
||||||
~needle:"not implemented yet")
|
~needle:"not implemented yet")
|
||||||
[ "handler-case", "(defn f [] () (handler-case 1))";
|
[ "find-restart", "(defn f [] () (find-restart 'skip))";
|
||||||
"find-restart", "(defn f [] () (find-restart 'skip))";
|
|
||||||
"compute-restarts", "(defn f [] () (compute-restarts))" ];
|
"compute-restarts", "(defn f [] () (compute-restarts))" ];
|
||||||
|
|
||||||
|
(* ── handler-case ──────────────────────────────────────────────── *)
|
||||||
|
|
||||||
|
(* The unwinding handler. It is built out of a handler-bind and a
|
||||||
|
restart-case, so most of what could go wrong is already pinned where
|
||||||
|
those two are; what is asserted here is the surface it puts in front of
|
||||||
|
them and the one rule that is its own — every clause and the body agree
|
||||||
|
on a type, which is the type of the whole form. *)
|
||||||
|
let boom =
|
||||||
|
"(defstruct Boom [id i32])\n(defstruct Dud [id i32])\n"
|
||||||
|
in
|
||||||
|
accepts "handler-case"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (.id c))]))");
|
||||||
|
accepts "handler-case with several clauses"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (.id c)) \
|
||||||
|
(Dud [c] (+ 1 (.id c)))]))");
|
||||||
|
(* The whole difference from handler-bind: a clause runs at the form, in the
|
||||||
|
function that wrote it, so it sees that function's locals. The same body
|
||||||
|
under a handler-bind is refused by name. *)
|
||||||
|
accepts "a handler-case clause sees the establishing function's locals"
|
||||||
|
(boom ^ "(defn f [] i32 (let [n 1] (handler-case 0 [(Boom [c] n)])))");
|
||||||
|
rejects_check "a handler-bind clause still cannot"
|
||||||
|
(boom ^ "(defn f [] i32 (let [n 1] (handler-bind [(Boom [c] (set n 2))] 0)))")
|
||||||
|
~needle:"a handler cannot see n: it is a local of the enclosing function";
|
||||||
|
(* Nothing static refuses a condition no clause lists: it installs no frame
|
||||||
|
that matches, so it goes past untouched and the body carries on. *)
|
||||||
|
accepts "a condition no clause lists"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case (do (signal (Dud {.id 1})) 7) \
|
||||||
|
[(Boom [c] 0)]))");
|
||||||
|
(* The typing rule, and it fails where an [if] with disagreeing arms fails:
|
||||||
|
at the form that does not fit, saying what was wanted and what was
|
||||||
|
found. *)
|
||||||
|
rejects_check "a handler-case clause that disagrees with the body"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] \"no\")]))")
|
||||||
|
~needle:"expected i32, found string";
|
||||||
|
rejects_check "two handler-case clauses that disagree"
|
||||||
|
(boom ^ "(defn f [] () (println (handler-case 1 [(Boom [c] 2) \
|
||||||
|
(Dud [c] \"no\")])))")
|
||||||
|
~needle:"expected i32, found string";
|
||||||
|
(* Two clauses for one condition type: the first would take every one of
|
||||||
|
them and the second could never run. *)
|
||||||
|
rejects_check "one condition type twice"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] 2) (Boom [c] 3)]))")
|
||||||
|
~needle:"handles Boom twice";
|
||||||
|
rejects_check "a handler-case clause on something that is not a struct"
|
||||||
|
"(defn f [] i32 (handler-case 1 [(i32 [c] 2)]))"
|
||||||
|
~needle:"a handler matches a struct type";
|
||||||
|
(* The frames are established around the body and taken off after it, so an
|
||||||
|
early exit out of the middle would leave them on the stack — and the
|
||||||
|
refusal names the form the reader wrote rather than the handler-bind
|
||||||
|
underneath it. *)
|
||||||
|
rejects_check "return inside a handler-case body"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case (return 1) [(Boom [c] 2)]))")
|
||||||
|
~needle:"not allowed inside handler-case";
|
||||||
|
(* A clause is the other side of that rule and not an exception to it. It
|
||||||
|
runs at the form, in the function that wrote it, with the frames already
|
||||||
|
off the stack — so a [return] there is an ordinary return and there is
|
||||||
|
nothing left for it to strand. *)
|
||||||
|
accepts "return inside a handler-case clause"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (return 2))]))");
|
||||||
|
(* A defer is not, though, and for the reason every nested form is refused
|
||||||
|
one: it is copied onto every exit path of the *function*, so a defer
|
||||||
|
written where it looks scoped to the clause would run whether the clause
|
||||||
|
did or not. The same answer a restart-case clause gets. *)
|
||||||
|
rejects_check "defer inside a handler-case clause"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (defer (println \"\")) 2)]))")
|
||||||
|
~needle:"defer is not allowed inside a nested form";
|
||||||
|
(* The other way round works. A defer is the cleanup an unwind runs, so
|
||||||
|
establishing frames inside one is ordinary — what a defer may not do is
|
||||||
|
start a transfer that leaves it, and a handler-case begins and ends its
|
||||||
|
own. *)
|
||||||
|
accepts "handler-case inside a defer"
|
||||||
|
(boom ^ "(defn f [] i32 (defer (println (handler-case 1 [(Boom [c] 2)]))) 0)");
|
||||||
|
(* The shape. The clauses go in a vector after the body, which is the
|
||||||
|
opposite of handler-bind's order, so a form written the other way round
|
||||||
|
has to say so rather than parse as something else. *)
|
||||||
|
rejects_check "handler-case with no clauses"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 []))")
|
||||||
|
~needle:"at least one clause";
|
||||||
|
rejects_check "handler-case written the handler-bind way round"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case [(Boom [c] 2)] 1))")
|
||||||
|
~needle:"handler-case is (handler-case body";
|
||||||
|
rejects_check "a handler-case clause that binds nothing"
|
||||||
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [] 2)]))")
|
||||||
|
~needle:"a handler-case clause is (Type [name] body ...)";
|
||||||
|
|
||||||
(* ── Destructuring ─────────────────────────────────────────────── *)
|
(* ── Destructuring ─────────────────────────────────────────────── *)
|
||||||
|
|
||||||
(* A pattern is desugared in [Parse] into the bindings and field accesses that
|
(* A pattern is desugared in [Parse] into the bindings and field accesses that
|
||||||
|
|||||||
@ -141,6 +141,14 @@ let corpus =
|
|||||||
same directory; the new C here is three more path buffers, which is
|
same directory; the new C here is three more path buffers, which is
|
||||||
exactly what this tool is for. *)
|
exactly what this tool is for. *)
|
||||||
"programs/files.flan", [];
|
"programs/files.flan", [];
|
||||||
|
(* The unwinding handler, and here for the frames rather than for the heap:
|
||||||
|
every path it takes leaves a function through the transfer exit, where
|
||||||
|
a handler frame or a restart frame left on its stack is a pointer into
|
||||||
|
an alloca that has gone. An output comparison cannot see that until
|
||||||
|
something later calls through it; ASan sees it at the store. The
|
||||||
|
with-allocator case is the one that reaches the heap — the region it
|
||||||
|
rebound is released after the unwind has carried a value out of it. *)
|
||||||
|
"programs/handler-case.flan", [];
|
||||||
(* The JSON reader, which is the corpus's densest allocator: every string
|
(* The JSON reader, which is the corpus's densest allocator: every string
|
||||||
in the document is a (Vec u8) grown a byte at a time and then handed
|
in the document is a (Vec u8) grown a byte at a time and then handed
|
||||||
out as a view of its own block, and the block is never freed because
|
out as a view of its own block, and the block is never freed because
|
||||||
|
|||||||
@ -2013,7 +2013,6 @@ name, with the milestone it belongs to, and the tests assert on the reason.</p>
|
|||||||
<tr><td>a bare lowercase type name</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
<tr><td>a bare lowercase type name</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
|
||||||
<tr><td><code>errdefer</code></td><td>errdefer is not implemented yet (see the build sequence in plan.org)</td></tr>
|
<tr><td><code>errdefer</code></td><td>errdefer is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||||
<tr><td><code>await</code></td><td>await is not implemented yet (see the build sequence in plan.org)</td></tr>
|
<tr><td><code>await</code></td><td>await is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||||
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet (see the build sequence in plan.org)</td></tr>
|
|
||||||
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet (see the build sequence in plan.org)</td></tr>
|
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet (see the build sequence in plan.org)</td></tr>
|
||||||
<tr><td>a union as a <code>declare</code> parameter</td><td>a parameter of g is U, which cannot cross to C directly — pass (Ptr U) and let the shim read it</td></tr>
|
<tr><td>a union as a <code>declare</code> parameter</td><td>a parameter of g is U, which cannot cross to C directly — pass (Ptr U) and let the shim read it</td></tr>
|
||||||
<tr><td>a user-written allocator</td><td>a user-written allocator is not implemented yet, and a defn's name in that position …</td></tr>
|
<tr><td>a user-written allocator</td><td>a user-written allocator is not implemented yet, and a defn's name in that position …</td></tr>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user