flan/test/programs/defer-let.flan
Joseph Ferano e041b2f26c A let has the function's extent, so a defer may be written in one
defer is a compile-time construct: the cleanup is copied into every exit
path of the function. That is why a loop body and a branch are refused —
a loop body's would fire once at function exit rather than once per
iteration, and a branch would have to express "maybe registered", which
a form copied into every exit path or into none cannot say.

A let is neither. It is not a frame here: its bindings are function slots
like any other and nothing is released at scope exit, so a let at the top
level of a function body has exactly the function's extent and a defer
written in it always registers. It was refused for a reason that does not
apply to it. A let nested inside such a let has the same extent and the
same permission; a let inside a while or an if has the loop's or the
arm's, and inherits the refusal.

The permission is granted again before every form of a body, never once
around the body: check withdraws it as it starts, so granting it once
would let the first defer through and refuse the second — and two
resources acquired in one let is the case this exists for. defer-let.flan
covers that one specifically, along with nesting, interleaved
registration order across the let boundary, and an early return.

The two refusals that stay now name what blocks them.
2026-09-12 14:54:52 +07:00

73 lines
2.5 KiB
Plaintext

;;;; defer in a let whose extent is the function body.
;;;;
;;;; A let is not a frame here: its bindings are function slots like any other,
;;;; and nothing is released at scope exit (spec-memory.md, "When storage is
;;;; released"). So a let at the top level of a function body has exactly the
;;;; function's extent, a defer written in it always registers, and it is as
;;;; safe as one written at the top level. A let nested inside such a let has
;;;; the same extent and the same permission.
;;;;
;;;; The numbers differ per failure, so a wrong answer names its own cause.
(defvar order i64)
(defn note [n i32] (set order (+ (* order 10) (i64 n))))
;;; One defer in a let, with the acquisition it is paired with above it. This
;;; is the shape the relaxation exists for: acquire, defer the release beside
;;; it, then use it.
(defn one [] i64
(let [a 1]
(defer (note a))
(note 9))
order)
;;; Two resources in one let. This is the case a flag granted once per block
;;; instead of once per form gets wrong: the first defer registers and the
;;; second is refused.
(defn two []
(let [a 1 b 2]
(defer (note a))
(defer (note b))
(note 9)))
;;; A nested let still has the function's extent, so a defer in it registers
;;; too — and it registers *later* than the outer one, so it runs first.
(defn nested []
(let [a 1]
(defer (note a))
(let [b 2]
(defer (note b))
(note 9))))
;;; Registration order is one order across the boundary: a defer at the top
;;; level and a defer inside a let interleave by where they are written, not by
;;; which construct they are in. Written 1, 2, 3; run 3, 2, 1.
(defn mixed []
(defer (note 1))
(let [x 2]
(defer (note x))
(defer (note 3))
(note 9)))
;;; An early return runs them too, and innermost-first, exactly as it does for
;;; a defer at the top level. The let's binding is still live when the defer
;;; reads it, because the slot is the function's.
(defn early [] i64
(let [a 1]
(defer (note a))
(let [b 2]
(defer (note b))
(return 7)))
0)
(defn main [] i32
(set order 0) (print (one)) (println "") ; 9 — read before the defer runs
(set order 0) (one) (print order) (println "") ; 91
(set order 0) (two) (print order) (println "") ; 921 — reverse of writing
(set order 0) (nested)(print order) (println "") ; 921
(set order 0) (mixed) (print order) (println "") ; 9321
(set order 0) (print (early)) (println "") ; 7
(print order) (println "") ; 21
0)