From dd160aa31b06b3d5065326a5edab3e04509b9d6f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:19:09 +0700 Subject: [PATCH] A return computes its value before it runs its defers --- TODO.org | 12 ++++++------ docs/BUILT.md | 3 ++- lib/check.ml | 34 +++++++++++++++++++++++++++------ test/programs/return-defer.flan | 32 +++++++++++++++++++++++++++++++ test/test_acceptance.ml | 8 ++++++++ web/index.html | 7 ++++--- 6 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 test/programs/return-defer.flan diff --git a/TODO.org b/TODO.org index a9614cdd..6ab697d9 100644 --- a/TODO.org +++ b/TODO.org @@ -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] diff --git a/docs/BUILT.md b/docs/BUILT.md index 930c83da..c3dbc372 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -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. diff --git a/lib/check.ml b/lib/check.ml index 678c19db..2ddd068d 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 diff --git a/test/programs/return-defer.flan b/test/programs/return-defer.flan new file mode 100644 index 00000000..0909169b --- /dev/null +++ b/test/programs/return-defer.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 45947b76..2a1c6a28 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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" diff --git a/web/index.html b/web/index.html index 791be8c7..7584f36e 100644 --- a/web/index.html +++ b/web/index.html @@ -1107,9 +1107,10 @@ not found

defer

-

A defer runs at function exit, innermost first. An explicit -return runs the ones registered above it — a defer written below a return -has not executed yet and must not fire.

+

A defer runs at function exit, innermost first, after the value the +function returns has been computed. An explicit return runs the ones +registered above it — a defer written below a return has not executed yet and must +not fire.

(defn work [n i32] i32
   (defer (println "second"))