A return computes its value before it runs its defers

This commit is contained in:
Joseph Ferano 2026-09-25 10:19:09 +07:00
parent d006a8b480
commit dd160aa31b
6 changed files with 80 additions and 16 deletions

View File

@ -339,12 +339,12 @@ a =defer= in one always registers. A loop body and a branch are still refused by
name: =defer= is a compile-time construct with the cleanup copied into every exit
path, so "maybe registered" is not expressible.
** TODO A return runs its defers before it computes its value
=(return v)= is lowered as =Do (defers @ [Return v])= (=lib/check.ml= near 3474),
so a defer that changes what =v= reads changes the answer, and =(return x)= and
falling off the end with =x= disagree. All backends agree with each other. The
value is computed first and the defers run after, the order Odin, Go and Zig
use.
** DONE A return runs its defers before it computes its value
CLOSED: [2026-09-25]
=(return v)= computes =v= into a slot, then runs the defers registered so far,
then returns the slot — the order falling off the end already had, and Odin's, Go's
and Zig's. One lowering in =Check=, so every backend has it. A value of type
=Never= is still returned directly, since nothing after it runs.
** DONE edn reads into a struct and answers a dynamic value
CLOSED: [2026-09-17]

View File

@ -19,7 +19,8 @@ assignable, which makes the generated step its only writer. **Amended** by the s
**`defer`** is recognised in `check_fn` and nowhere else, because that is the only place that knows a form is at the top
level of a function body. Each one is checked in place, then registered on the context; it emits nothing where it
stands. Function exit runs them innermost-first, and an explicit `return` runs the ones registered *above* it — a defer
written below a return has not executed yet and must not fire. A trap runs none of them, which follows from the
written below a return has not executed yet and must not fire. Both compute the returned value into a slot first and
run the defers after it, Odin's, Go's and Zig's order, so a defer that changes a returned local does not change the answer. A trap runs none of them, which follows from the
bounds-check shape (`noreturn` then `unreachable`) rather than being a separate decision. **Amended** once a bounds
failure became a signal: an *answered* one leaves through the unwind path and runs them like any other transfer, an
unanswered one still runs none. See "An index out of range is a condition" at the foot of this file.

View File

@ -3535,12 +3535,34 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
None
| Some v -> Some (check ctx ~want:ctx.ret v)
in
(* Whatever has been deferred *so far* runs first: a defer written below
this return has not executed yet and must not fire. *)
let r = mk loc Types.Never (Tast.Return v) in
(match ctx.defers with
| [] -> r
| ds -> mk loc Types.Never (Tast.Do (ds @ [ r ])))
(* The value is computed first, then whatever has been deferred *so far*
runs, then the function returns — the order the fall-off-the-end path
in [check_fn] has, so [(return x)] and a last form [x] agree. A defer
written below this return has not executed yet and must not fire. *)
(match ctx.defers, v with
| [], _ -> mk loc Types.Never (Tast.Return v)
| ds, Some (value : Tast.expr)
when not (Types.equal value.Tast.ty Types.Never
|| Types.equal value.Tast.ty Types.Unit) ->
let s = fresh_slot ctx value.Tast.ty in
let r =
mk loc Types.Never
(Tast.Return (Some (mk loc value.Tast.ty (Tast.Local s))))
in
mk loc Types.Never (Tast.Let ([ (s, value) ], ds @ [ r ]))
(* A unit value has nothing to keep, and is still evaluated first. *)
| ds, Some value when Types.equal value.Tast.ty Types.Unit ->
mk loc Types.Never
(Tast.Do
((value :: ds)
@ [ mk loc Types.Never (Tast.Return (Some (unit_at loc))) ]))
(* A value that never arrives is computed first too, and the defers
after it are unreachable: a trap runs none, and a transfer out of it
runs the function's [fdefers]. *)
| _, Some _ -> mk loc Types.Never (Tast.Return v)
| ds, None ->
mk loc Types.Never
(Tast.Do (ds @ [ mk loc Types.Never (Tast.Return None) ])))
(* (set (at target i) x) against a dyn target — a dyn vec from (vec-new
dyn), or a typed container's own view (M2 item 3) — is a call and not a
place: [flan_dyn_set_at] tag-checks [x]'s dyn tag against what the vec

View File

@ -0,0 +1,32 @@
;;;; A return computes its value first and then runs the defers registered
;;;; so far, so (return x) and a last form x answer the same thing even when
;;;; a defer changes x.
(defstruct P [a i32 b i32])
(defn early [] i32
(let [x 1]
(defer (set x 2))
(return x)))
(defn fall [] i32
(let [x 1]
(defer (set x 2))
x))
(defn agg [flag bool] P
(let [p (P {.a 1 .b 1})]
(defer (set p (P {.a 9 .b 9})) (println "deferred"))
(when flag (return p))
(P {.a 5 .b 5})))
(defn unit [] ()
(defer (println "second"))
(return (println "first")))
(defn main [] i32
(println (early)) ; 1
(println (fall)) ; 1
(println (.a (agg true))) ; deferred, then 1
(println (.a (agg false))) ; deferred, then 5
(unit) ; first, then second
0)

View File

@ -529,6 +529,14 @@ let () =
outputs "a u64 constant in decimal" "programs/u64-decimal.flan" u64_out;
outputs ~x86:true "a u64 constant in decimal, x86"
"programs/u64-decimal.flan" u64_out;
(* A return computes its value before it runs the defers. *)
let rd_out = "1\n1\ndeferred\n1\ndeferred\n5\nfirst\nsecond\n" in
outputs "a return computes its value before its defers"
"programs/return-defer.flan" rd_out;
outputs ~opt:"-O0" "a return computes its value before its defers, -O0"
"programs/return-defer.flan" rd_out;
outputs ~x86:true "a return computes its value before its defers, x86"
"programs/return-defer.flan" rd_out;
(* Constant arithmetic folds before a bounded variable checks it. *)
let fold_out = "10\n0\n7.5\n7\n" in
outputs "constant arithmetic at a bounded variable"

View File

@ -1107,9 +1107,10 @@ not found</code></pre>
<h2 id="defer">defer</h2>
<p>A <code>defer</code> runs at function exit, innermost first. An explicit
<code>return</code> runs the ones registered above it — a defer written below a return
has not executed yet and must not fire.</p>
<p>A <code>defer</code> runs at function exit, innermost first, after the value the
function returns has been computed. An explicit <code>return</code> runs the ones
registered above it — a defer written below a return has not executed yet and must
not fire.</p>
<pre><code>(defn work [n i32] i32
(defer (println "second"))