The survey was counting the defers, not watching them
Every mark it left was an addition, and addition commutes, so a backend that ran the defers outermost-first produced byte-identical output and the row that was supposed to be watching the order could not have told. The claim was in the comments and not in the numbers. cleanup.flan already had the device for this — a shift rather than a sum — so the log here is a digit trace now, and the two frames under a catch read 12 where a wrong order reads 21. Rewriting the trace made room for the three behaviours that worked and nothing pinned. A return inside a clause is an ordinary return from the function that wrote the form, because that is where a clause runs: it leaves through the function's own exit, runs the defer registered there after the two the unwind already ran, and leaves the handler stack empty behind it, which the bare signal that follows in main is the check on. A defer inside a clause is refused for the reason every nested form is refused one. And a handler-case inside a defer works, because a defer may not start a transfer that leaves it and this one begins and ends its own. The program is registered with the sanitizers, where the interesting failure is not the heap but a handler or restart frame left on a stack pointing into an alloca that has gone — an output comparison cannot see that until something much later calls through it. It is clean; it was also leaking sixteen bytes out of the vector main allocates to prove the allocator context came back, which is the test's own litter and is freed now. docs/PORTING.md ranked handler-case as one site handler-bind covers. It still is one site, and handler-bind still covers it, but it is no longer the closer translation: a catch block is assumed everywhere it is written to see the locals around it, and only the clause that runs at the form does. The handler clauses are lifted left to right rather than by List.map, whose order is unspecified. Each lift names itself after the count already on the list, so an order nobody chose would number the clauses of one handler-bind differently between builds, and those names go into a redefinition module.
This commit is contained in:
parent
a251dca67f
commit
e3565b30e8
@ -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`
|
||||
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
|
||||
"log it and carry on", which is what a `handler-bind` clause returning normally does.
|
||||
`engine.clj`'s `reload-config!` is `(try … (catch Exception e (println e)))`. The ranking
|
||||
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
|
||||
|
||||
@ -642,9 +655,11 @@ not compete for the same slot.
|
||||
|
||||
**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`
|
||||
(neither host uses that discipline), `handler-case` (one site, `handler-bind` covers it),
|
||||
`loop`/`recur` and tail calls (nothing recurses), user-written allocators, structural
|
||||
typing.
|
||||
(neither host uses that discipline), `loop`/`recur` and tail calls (nothing recurses),
|
||||
user-written allocators, structural typing. `handler-case` was on this list for the same
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -2407,7 +2407,13 @@ and check_fn ctx ~want loc (params : string list) body =
|
||||
that has gone. *)
|
||||
and check_handler_bind ctx ?want ?(what = "handler-bind") loc clauses body =
|
||||
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) ->
|
||||
let ty = resolve ctx.env c.Ast.hty in
|
||||
let name =
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
;;;; 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])
|
||||
@ -21,15 +27,18 @@
|
||||
(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 (§5).
|
||||
;;; 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 (set log (+ log 1)))
|
||||
(defer (note 1))
|
||||
(error (Missing {.id n}))
|
||||
0)
|
||||
|
||||
(defn middle [n i32] i32
|
||||
(defer (set log (+ log 10)))
|
||||
(defer (note 2))
|
||||
(+ (inner n) 1))
|
||||
|
||||
;;; Nothing signals: the body's own value stands, which is the case a form that
|
||||
@ -72,7 +81,7 @@
|
||||
;;; 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] (set log (+ log 1000)))]
|
||||
(handler-bind [(Corrupt [c] (note 4))]
|
||||
(handler-case
|
||||
(do (signal (Corrupt {.id n}))
|
||||
(error (Missing {.id n}))
|
||||
@ -84,7 +93,7 @@
|
||||
;;; which is the same landing pad a restart transfer already uses.
|
||||
(defn hb-in-hc [n i32] i32
|
||||
(handler-case
|
||||
(handler-bind [(Corrupt [c] (set log (+ log 1)))]
|
||||
(handler-bind [(Corrupt [c] (note 5))]
|
||||
(do (signal (Corrupt {.id n}))
|
||||
(error (Missing {.id n}))
|
||||
0))
|
||||
@ -95,7 +104,7 @@
|
||||
;;; 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 (set log (+ log 100)))
|
||||
(defer (note 3))
|
||||
(handler-case (middle n)
|
||||
[(Missing [c] (do (signal (Missing {.id 99}))
|
||||
(.id c)))]))
|
||||
@ -104,26 +113,48 @@
|
||||
(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 answers
|
||||
;;; its own condition, so the total says they are different names.
|
||||
;;; 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]
|
||||
(set log
|
||||
(+ log (i64 (handler-case (error (Corrupt {.id 3}))
|
||||
[(Corrupt [d] (.id d))])))))]
|
||||
(note (i64 (handler-case (error (Corrupt {.id 6}))
|
||||
[(Corrupt [d] (.id d))]))))]
|
||||
(signal (Late {.id 0}))))
|
||||
|
||||
(defn in-clause-b [] ()
|
||||
(handler-bind
|
||||
[(Late [c]
|
||||
(set log
|
||||
(+ log (i64 (handler-case (error (Corrupt {.id 40}))
|
||||
[(Corrupt [d] (.id d))])))))]
|
||||
(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
|
||||
@ -143,46 +174,58 @@
|
||||
|
||||
(defn main [] i32
|
||||
;; Normal completion.
|
||||
(print (quiet 41)) (println "") ; 42
|
||||
(print log) (println "") ; 0 — nothing unwound
|
||||
(print (quiet 41)) (println "")
|
||||
(print log) (println "")
|
||||
|
||||
;; Caught, with both defers between the signal and the form having run.
|
||||
(print (caught 5)) (println "") ; 105
|
||||
(print log) (println "") ; 11
|
||||
;; 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 "") ; 101
|
||||
(print (two 1)) (println "") ; 202
|
||||
(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] (set log (+ log 10000)))]
|
||||
(print (unmatched 3)) (println "")) ; 7
|
||||
(print log) (println "") ; 10011
|
||||
(handler-bind [(Corrupt [c] (note 6))]
|
||||
(print (unmatched 3)) (println ""))
|
||||
(print log) (println "")
|
||||
|
||||
;; The two nestings. Neither signals from under [middle], so neither moves
|
||||
;; the defer count: what each adds is its own handler-bind's clause.
|
||||
(print (hc-in-hb 6)) (println "") ; 6
|
||||
(print log) (println "") ; 10011 + 1000 = 11011
|
||||
(print (hb-in-hc 8)) (println "") ; 16
|
||||
(print log) (println "") ; 11011 + 1 = 11012
|
||||
;; 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 "") ; 5099
|
||||
(print log) (println "") ; 11012 + 11 + 100 = 11123
|
||||
(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 "") ; 11123 + 3 + 40 = 11166
|
||||
(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 "") ; 23
|
||||
(print (scoped 3)) (println "")
|
||||
(arena-destroy frame)
|
||||
(let [h (vec-new i32)]
|
||||
(push h 9)
|
||||
(print (len h)) (println "")) ; 1
|
||||
(print (len h)) (println "")
|
||||
(free h))
|
||||
0)
|
||||
|
||||
@ -456,16 +456,23 @@ let () =
|
||||
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. *)
|
||||
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\n11\n101\n202\n7\n10011\n6\n11011\n16\n11012\n5099\n11123\n\
|
||||
11166\n23\n1\n"
|
||||
"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"
|
||||
|
||||
@ -1709,6 +1709,25 @@ let () =
|
||||
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. *)
|
||||
|
||||
@ -141,6 +141,14 @@ let corpus =
|
||||
same directory; the new C here is three more path buffers, which is
|
||||
exactly what this tool is for. *)
|
||||
"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
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user