A return computes its value before it runs its defers
This commit is contained in:
parent
d006a8b480
commit
dd160aa31b
12
TODO.org
12
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
|
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]
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
34
lib/check.ml
34
lib/check.ml
@ -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
|
||||||
|
|||||||
32
test/programs/return-defer.flan
Normal file
32
test/programs/return-defer.flan
Normal 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)
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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"))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user