flan/test/programs/restarts.flan
Joseph Ferano 468dab6e4c Restarts take parameters, and the check for them is where it has to be
spec-conditions.md §3's remaining half: a clause binds parameters, an
invoke-restart supplies them, and what a restart takes is compared at run
time because a restart is found by name on a dynamic stack — neither end
of the transfer can see the other.

The parameters live in a buffer the restart-case owns, not the invoker's
frame. A clause runs after every frame between the two has returned (§5),
so anything on the invoking side is gone by then; the invoker stores into
the target frame while both are still alive, which is the one moment they
are.

The frame carries the parameter count and a hash of how the types are
spelled, and every frame carries them whether it takes parameters or not:
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
makes a 32-bit collision between two different signatures harmless — and
the spelling itself rides along so that a mismatch can say what was
wanted and what was given, which neither end alone knows.

The arguments are evaluated into slots before the invoke node rather than
hanging off it. An argument that transfers on its own is then guarded
before anything aims the channel, and a call written in an argument is on
the ordinary walk Reach and Load already do — a node they treat as a leaf
would have dropped the function and failed to link.

The other way a transfer starts is the break loop, which chooses by
position and has nothing to fill parameters in with. It reaches a clause
through the same channel, so nothing downstream could tell the two apart:
the frame is pushed with the buffer marked unfilled and a clause with
parameters checks that mark before reading it. Refused with the reason
rather than run on values no one supplied.

runtime/flan_rt.c gains two message functions and nothing else; the
restart frame's first four fields, which are the ones C declares, do not
move.
2026-09-12 10:46:24 +07:00

155 lines
6.9 KiB
Plaintext

;;;; restart-case and invoke-restart — spec-conditions.md §3 to §6.
;;;;
;;;; 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).
;;;;
;;;; §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)
;;; The signalling end. Two frames below the restart-case, so the transfer has
;;; something to cross.
(defn load [n i32] i32
(signal (AssetMissing {:id n}))
100)
;;; §5: this defer runs whether the call below returns or transfers, and it
;;; runs before the clause body starts.
(defn middle [n i32] i32
(defer (set log (+ log 1)))
(+ (load n) 1))
;;; The spec's load-texture shape (§1): a restart-case in value position, whose
;;; fall-through has to produce the type too.
(defn fetch [n i32] i32
(restart-case (middle n)
(use-placeholder [] -1)
(retry [] 7)))
;;; §4: an inner restart-case shadows an outer one offering the same name, and
;;; the outer one is reached again once the inner has been left.
(defn nested [n i32] i32
(restart-case
(+ (restart-case (middle n)
(use-placeholder [] 10))
1000)
(use-placeholder [] 20)))
;;; §2's diverging variant. Same lookup, but a handler that returns normally
;;; has not answered it: only a transfer gets past, so the fall-through the
;;; signalling version needs does not exist here.
(defn strict [n i32] i32
(restart-case
(do (error (AssetMissing {:id n}))
;; unreachable — error is Never, so nothing after it runs
0)
(use-placeholder [] -2)))
;;; 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)))
(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))
: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
;; A handler that transfers: the clause's value is the restart-case's.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
(print (fetch 2)) (println "")) ; -1
(print log) (println "") ; 2 — the defer ran
(handler-bind [(AssetMissing [c] (invoke-restart 'retry))]
(print (fetch 3)) (println "")) ; 7
;; §4: the innermost frame offering the name wins, and the clause yields to
;; *its* own continuation — so the +1000 written around the inner
;; restart-case still runs, and the outer clause never does.
(handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
(print (nested 4)) (println "")) ; 1010
;; A handler that returns normally transfers nothing: §1's accumulation case
;; still works, and the fall-through stands.
(handler-bind [(AssetMissing [c] (set log (+ log 100)))]
(print (fetch 5)) (println "")) ; 101
;; The handler runs at the signal, which is inside the call the defer
;; belongs to, so its +100 lands before that defer's +1.
(print log) (println "") ; 4 + 100 + 1 = 105
;; error, answered by a transfer. Unanswered it stops the program, which is
;; 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)