flan/test/programs/conditions.flan
Joseph Ferano 5ce8e7a68e handler-bind and signal, which alter no control flow
spec-conditions.md §1 and §2 and nothing else, because those two are worth
having alone: signal returns Unit whatever it finds, a handler that returns
normally leaves the signalling function to carry on, and with nothing matching
it is a no-op. So none of §6's transfer machinery exists yet and no signature
changed - which is the whole reason to do this step first.

The runtime is a linked list. Establishing a handler is two stores and a push
onto a frame on the establishing function's own stack, and signal with an empty
stack is a null check, which is what §2 asks for. Popping is by frame rather
than by count, so restoring what this one displaced is right even if something
below it left the stack out of step.

A condition's type is a hash of its name and not an index: an index would shift
the moment a struct were added, and every handler a running program had already
pushed would match the wrong type. The condition crosses as a pointer, since a
handler runs while the signalling frame is alive and there is nothing to copy -
but what the clause binds is the condition itself, the pointer being a hidden
parameter and the name a slot loaded from it, so a handler passing c to
something expecting the struct is not handed an address.

A clause is lifted into a function of its own, because a handler runs from
wherever the signal was and cannot be a branch in the function that wrote it.
That gives two refusals, both by the house rule. A handler cannot see the
establishing function's locals - that is a closure with an explicit
environment, so a reference to one is refused for that reason rather than
reported as an unknown name. And return inside a handler-bind body is refused,
since the frames are popped on the way out and an early exit would leave them
pointing into a function that has gone.

Settled in advance for the next step: in a dev build every function is
transfer-transparent, because a cell can hold anything and the honest answer to
what it can call is anything. Same bargain as the indirect call, and it means
redefinition acquires no new refusal class. Still open is whether the
discriminated result is returned by value or through an out-parameter.
2026-09-11 07:27:50 +07:00

46 lines
1.7 KiB
Plaintext

;;;; handler-bind and signal — spec-conditions.md §1 and §2.
;;;;
;;;; The accumulation case, which is what makes these two worth having on their
;;;; own: signal returns Unit, a handler that returns normally leaves the
;;;; signalling function to carry on, and with nothing matching signal is a
;;;; no-op. No control flow is altered, so none of the transfer machinery
;;;; restart-case needs exists yet.
(defstruct AssetMissing [id i32])
(defstruct Corrupt [id i32])
(defvar seen i64)
(defvar other i64)
;;; Signals twice and keeps going both times — that is the whole of §1.
(defn load-all []
(signal (AssetMissing {:id 1}))
(signal (AssetMissing {:id 2}))
(signal (Corrupt {:id 3})))
(defn main [] i32
;; No handler: a no-op, not an abort and not a message (§2).
(load-all)
(print-i64 seen) (newline) ; 0
(handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))]
(load-all))
(print-i64 seen) (newline) ; 1 + 2 = 3
;; Two clauses, and only the matching one runs for each condition.
(handler-bind [(AssetMissing [c] (set seen (+ seen 10)))
(Corrupt [c] (set other (+ other (i64 (.id c)))))]
(load-all))
(print-i64 seen) (newline) ; 3 + 20 = 23
(print-i64 other) (newline) ; 3
;; Nesting: the inner frame does not displace the outer one, so both run.
(handler-bind [(Corrupt [c] (set other (+ other 100)))]
(handler-bind [(Corrupt [c] (set other (+ other 1000)))]
(signal (Corrupt {:id 0}))))
(print-i64 other) (newline) ; 3 + 1000 + 100 = 1103
;; And the stack is back to what it was: no handler, no effect.
(load-all)
(print-i64 other) (newline) ; 1103
0)