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 name: =defer= is a compile-time construct with the cleanup copied into every exit
path, so "maybe registered" is not expressible. path, so "maybe registered" is not expressible.
** TODO A return runs its defers before it computes its value ** DONE A return runs its defers before it computes its value
=(return v)= is lowered as =Do (defers @ [Return v])= (=lib/check.ml= near 3474), CLOSED: [2026-09-25]
so a defer that changes what =v= reads changes the answer, and =(return x)= and =(return v)= computes =v= into a slot, then runs the defers registered so far,
falling off the end with =x= disagree. All backends agree with each other. The then returns the slot — the order falling off the end already had, and Odin's, Go's
value is computed first and the defers run after, the order Odin, Go and Zig and Zig's. One lowering in =Check=, so every backend has it. A value of type
use. =Never= is still returned directly, since nothing after it runs.
** DONE edn reads into a struct and answers a dynamic value ** DONE edn reads into a struct and answers a dynamic value
CLOSED: [2026-09-17] 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 **`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 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 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 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 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. 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 None
| Some v -> Some (check ctx ~want:ctx.ret v) | Some v -> Some (check ctx ~want:ctx.ret v)
in in
(* Whatever has been deferred *so far* runs first: a defer written below (* The value is computed first, then whatever has been deferred *so far*
this return has not executed yet and must not fire. *) runs, then the function returns — the order the fall-off-the-end path
let r = mk loc Types.Never (Tast.Return v) in in [check_fn] has, so [(return x)] and a last form [x] agree. A defer
(match ctx.defers with written below this return has not executed yet and must not fire. *)
| [] -> r (match ctx.defers, v with
| ds -> mk loc Types.Never (Tast.Do (ds @ [ r ]))) | [], _ -> 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 (* (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 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 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 "a u64 constant in decimal" "programs/u64-decimal.flan" u64_out;
outputs ~x86:true "a u64 constant in decimal, x86" outputs ~x86:true "a u64 constant in decimal, x86"
"programs/u64-decimal.flan" u64_out; "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. *) (* Constant arithmetic folds before a bounded variable checks it. *)
let fold_out = "10\n0\n7.5\n7\n" in let fold_out = "10\n0\n7.5\n7\n" in
outputs "constant arithmetic at a bounded variable" outputs "constant arithmetic at a bounded variable"

View File

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