An aggregate that reads its own destination sees the old value, on x86 too
x86 built a settling right-hand side straight into its destination, so (set p (P {.a (.b p) .b (.a p)})) wrote .a and read it back for .b. assign now also asks whether the value reads storage the destination lies in, and goes through the temporary when it does. The global-initialiser entry was already closed by startup_plan's Set; the unrooted-temporary entry turns out to be a both-backends rooting gap and stays open for a decision.
This commit is contained in:
parent
5557594f31
commit
9194f918fc
38
TODO.org
38
TODO.org
@ -1030,22 +1030,32 @@ is built into a frame temporary and copied over now, unless lowering is bound to
|
||||
reach the end. Separately, the transfer exit zeroed an aggregate return value,
|
||||
which for an aggregate is the caller's storage; it zeroes scalars only.
|
||||
|
||||
** TODO A global initialiser still builds an aggregate straight into its destination on x86
|
||||
The statement-level assignment goes through a frame temporary now, so the
|
||||
half-write is closed where a program can observe it. The release build's
|
||||
globals-init path was not converted and still writes in place.
|
||||
** DONE A global initialiser builds an aggregate through the same temporary on x86
|
||||
CLOSED: [2026-09-25]
|
||||
Already true when the entry was written. Every computed initialiser, release
|
||||
build included, reaches =emit_globals_init= as =Emit.startup_plan='s
|
||||
=(set g init)= and so goes through =assign=. The only initialisers lowered
|
||||
straight into their symbol are =Tast.const_init= ones, which neither transfer
|
||||
nor read anything. Nothing to change.
|
||||
|
||||
** TODO An aggregate built in place can read its own destination
|
||||
=(set p (P {.a (.b p) .b (.a p)}))= answers =2 1= under LLVM and =2 2= under
|
||||
=--x86=: lowering asks whether it can transfer and not whether it can alias. The
|
||||
fix has a shape already — the same temporary, chosen by an aliasing question.
|
||||
Whether the two become one predicate or two is the lane to decide.
|
||||
** DONE An aggregate built in place never reads its own destination
|
||||
CLOSED: [2026-09-25]
|
||||
=assign= builds in place only when the right-hand side settles *and* reads no
|
||||
storage the destination lies in; otherwise it goes through the temporary. Two
|
||||
predicates, not one: =settles= is about leaving part-way, =reads= about a value
|
||||
reading itself, and a read through a pointer counts as reading everything.
|
||||
=test/programs/self-read.flan= runs on both backends.
|
||||
|
||||
** TODO The aggregate temporary is an unrooted buffer while it is filled
|
||||
No root table names it. Harmless only while a dyn field in a struct was refused,
|
||||
and per-type descriptors have since lifted that. Whoever relies on a dyn field
|
||||
reaching this path has to root the buffer, or a collection running inside the
|
||||
construction will not see what has been built so far.
|
||||
** TODO A dyn value read out of a place is unrooted until it is stored, on both backends
|
||||
First filed as the x86 aggregate temporary being unrooted. It is wider than
|
||||
that: a dyn loaded from a place and held while a later sibling allocates is
|
||||
unrooted on LLVM -O0 as well, in an aggregate literal or an argument list alike.
|
||||
=(pick (.d other) (do (set other (T {.n 0})) (churn)))= prints a different
|
||||
object's contents on both backends. =Emit.root_plan= roots dyn call results
|
||||
only. Decision for the author: root such intermediates on both backends (a
|
||||
=root_plan= change, plus LLVM spilling them to rooted allocas), or declare the
|
||||
shape unsupported. Rooting only the x86 temporary would fork the backends and
|
||||
leave the argument case open.
|
||||
|
||||
** TODO Marking through a descriptor an x86 reload module emitted
|
||||
The module links and runs. What is not proved is a collection running while a live
|
||||
|
||||
@ -6104,6 +6104,14 @@ Scalars were never affected, on either backend, and `half-write.flan`'s last row
|
||||
being read as one: a scalar's store is a single instruction and it happens after the check for a transfer, so a call
|
||||
that signalled never reaches it.
|
||||
|
||||
**Settling is not enough on its own.** A right-hand side that reads its own destination sees whatever has been
|
||||
written so far: `(set p (P {.a (.b p) .b (.a p)}))` built in place writes `.a` and then reads it back for `.b`,
|
||||
answering `2 2` where LLVM answers `2 1`. So `assign` asks a second question, `X86.reads`: whether the value reads
|
||||
storage the destination lies in, where the destination is named as one local, one global, or anywhere at all when it is
|
||||
reached through a pointer. A read through a pointer counts as reading everything. The two questions stay separate
|
||||
predicates because they are about different things, and in-place construction needs a no from both.
|
||||
`test/programs/self-read.flan` crosses the destinations with that self-read on both backends.
|
||||
|
||||
## Ghost text finds its anchor in the buffer, not in the table
|
||||
|
||||
`M-x flan-watch-ghost-mode` paints each watched value inline, after the line holding the call that wrote it, as an
|
||||
|
||||
89
lib/x86.ml
89
lib/x86.ml
@ -1677,6 +1677,82 @@ and settles_place (p : Tast.place) =
|
||||
(* An index is bounds-checked, and the check signals. *)
|
||||
| Tast.Pindex _ -> false
|
||||
|
||||
(* The storage a destination lies in, as far as it can be named without
|
||||
running anything: one local's slot, one global, or somewhere behind a
|
||||
pointer, which could be any of them.
|
||||
|
||||
Building an aggregate in its destination needs a second answer besides
|
||||
[settles]. A right-hand side that reads the destination sees the fields
|
||||
already written: [(set p (P {.a (.b p) .b (.a p)}))] writes [.a] and then
|
||||
reads it back as the new [.b]. LLVM builds the value before it stores any
|
||||
of it, so the read sees the old [p]. The two questions stay two predicates
|
||||
because they are about different things — one about leaving part-way, the
|
||||
other about the value reading itself — and [assign] asks both. *)
|
||||
type root = Rlocal of int | Rglobal of string | Rany
|
||||
|
||||
let rec expr_root (e : Tast.expr) =
|
||||
match e.Tast.e with
|
||||
| Tast.Local i -> Rlocal i
|
||||
| Tast.Global n -> Rglobal n
|
||||
| Tast.Field (x, _) | Tast.CaseField (x, _, _) -> through x
|
||||
| Tast.Prim (Tast.At, a :: _ :: _) -> through a
|
||||
| _ -> Rany
|
||||
|
||||
(* A field or an element is in its container's storage unless the container
|
||||
is reached through a pointer or is a slice, both of which are a pointer. *)
|
||||
and through (x : Tast.expr) =
|
||||
match x.Tast.ty with
|
||||
| Types.Ptr _ | Types.Slice _ | Types.String -> Rany
|
||||
| _ -> expr_root x
|
||||
|
||||
let place_root (p : Tast.place) =
|
||||
match p with
|
||||
| Tast.Plocal i -> Rlocal i
|
||||
| Tast.Pglobal n -> Rglobal n
|
||||
| Tast.Pfield (x, _) | Tast.Pindex (x, _) -> through x
|
||||
| Tast.Pderef _ -> Rany
|
||||
|
||||
let overlaps a b =
|
||||
match a, b with
|
||||
| Rany, _ | _, Rany -> true
|
||||
| Rlocal i, Rlocal j -> i = j
|
||||
| Rglobal m, Rglobal n -> String.equal m n
|
||||
| _ -> false
|
||||
|
||||
(* Whether evaluating [e] can read storage in [root]. Only asked of an
|
||||
expression that [settles], so the shapes are the ones listed there; a
|
||||
shape this does not recognise answers yes. A read through a pointer can
|
||||
reach anything. [Addr] is counted as a read of its place, which it is not,
|
||||
because being wrong that way costs a copy. *)
|
||||
let rec reads root (e : Tast.expr) =
|
||||
match e.Tast.e with
|
||||
| Tast.Int _ | Tast.Float _ | Tast.Bool _ | Tast.Str _ | Tast.Unit
|
||||
| Tast.Zero _ | Tast.Uninit _ | Tast.None_ | Tast.FnAddr _ -> false
|
||||
| Tast.Local i -> overlaps root (Rlocal i)
|
||||
| Tast.Global n -> overlaps root (Rglobal n)
|
||||
| Tast.Deref _ -> true
|
||||
| Tast.Field (x, _) | Tast.CaseField (x, _, _) ->
|
||||
(match x.Tast.ty with Types.Ptr _ -> true | _ -> false) || reads root x
|
||||
| Tast.Some_ x -> reads root x
|
||||
| Tast.Make (_, es) | Tast.MakeCase (_, _, es) | Tast.Arr es | Tast.Do es
|
||||
| Tast.Prim (_, es) -> List.exists (reads root) es
|
||||
| Tast.If (c, a, b) -> reads root c || reads root a || reads root b
|
||||
| Tast.Addr p -> reads_place root p
|
||||
| _ -> true
|
||||
|
||||
and reads_place root (p : Tast.place) =
|
||||
match p with
|
||||
| Tast.Plocal i -> overlaps root (Rlocal i)
|
||||
| Tast.Pglobal n -> overlaps root (Rglobal n)
|
||||
| Tast.Pderef _ -> true
|
||||
| Tast.Pfield (x, _) ->
|
||||
(match x.Tast.ty with Types.Ptr _ -> true | _ -> false) || reads root x
|
||||
| Tast.Pindex (x, is) ->
|
||||
(match x.Tast.ty with
|
||||
| Types.Ptr _ | Types.Slice _ | Types.String -> true
|
||||
| _ -> false)
|
||||
|| reads root x || List.exists (reads root) is
|
||||
|
||||
let rec lower f (e : Tast.expr) (dst : loc) : unit =
|
||||
(* The one hook the line table needs, and it is here rather than at
|
||||
statement granularity on purpose: the same recursion that lowers a nested
|
||||
@ -1817,7 +1893,8 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
nowhere else. *)
|
||||
scoped f (fun () ->
|
||||
let d = Lf f.slots.(slot) in
|
||||
if f.loops = [] then lower f v d else assign f ~dst:d v);
|
||||
if f.loops = [] then lower f v d
|
||||
else assign f ~root:(Rlocal slot) ~dst:d v);
|
||||
bind_slot f slot)
|
||||
bs;
|
||||
block f body dst t
|
||||
@ -1862,7 +1939,7 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
| None -> unsupported "continue %d outside a loop" n)
|
||||
| Tast.Set (p, v) ->
|
||||
let l = place f p in
|
||||
scoped f (fun () -> assign f ~dst:l v)
|
||||
scoped f (fun () -> assign f ~root:(place_root p) ~dst:l v)
|
||||
| Tast.Make (sn, xs) ->
|
||||
let offs = field_offsets f sn in
|
||||
List.iteri
|
||||
@ -2386,11 +2463,15 @@ and block f body dst t =
|
||||
instruction and it happens after the transfer guard, so a call that
|
||||
signalled never reaches it.
|
||||
|
||||
The same temporary answers a right-hand side that reads its own
|
||||
destination, which [reads] asks of [root]: built in place, the fields
|
||||
written first would be what the later ones read.
|
||||
|
||||
[dst] is worked out by the caller and outlives this: [scoped] reclaims the
|
||||
temporary, not the destination. *)
|
||||
and assign f ~(dst : loc) (v : Tast.expr) : unit =
|
||||
and assign f ~(root : root) ~(dst : loc) (v : Tast.expr) : unit =
|
||||
let t = v.Tast.ty in
|
||||
if (not (is_agg t)) || settles v then lower f v dst
|
||||
if (not (is_agg t)) || (settles v && not (reads root v)) then lower f v dst
|
||||
else begin
|
||||
let o = Lf (tmp f t) in
|
||||
lower f v o;
|
||||
|
||||
62
test/programs/self-read.flan
Normal file
62
test/programs/self-read.flan
Normal file
@ -0,0 +1,62 @@
|
||||
;;;; An aggregate whose value reads the place it is assigned to sees the old
|
||||
;;;; value of that place, in every field.
|
||||
;;;;
|
||||
;;;; (set p (P {.a (.b p) .b (.a p)})) swaps the two fields. It does only if
|
||||
;;;; the whole right-hand side is read before any of it is stored. A backend
|
||||
;;;; that builds the struct straight into [p] writes [.a] first, and the read
|
||||
;;;; of [(.a p)] for [.b] then sees the new value: the answer comes back 2 2
|
||||
;;;; rather than 2 1.
|
||||
;;;;
|
||||
;;;; Each row is a different destination: a global, a field of a global, a
|
||||
;;;; local, a field of a local, a place behind a pointer, a variable read
|
||||
;;;; through a pointer that points at it, and a local rebound inside a loop
|
||||
;;;; whose previous turn the new value reads. The last row reads a different
|
||||
;;;; variable, which is the case still built in place.
|
||||
|
||||
(defstruct P [a i32 b i32])
|
||||
(defstruct Q [tag i32 inner P])
|
||||
|
||||
(defonce g P (P {.a 1 .b 2}))
|
||||
(defonce gq Q (Q {.tag 0 .inner (P {.a 1 .b 2})}))
|
||||
|
||||
(defn show [p P] ()
|
||||
(print (.a p)) (print " ") (print (.b p)) (print "\n"))
|
||||
|
||||
(defn main [] ()
|
||||
;; A global.
|
||||
(set g (P {.a (.b g) .b (.a g)}))
|
||||
(show g)
|
||||
;; A field of a global.
|
||||
(set (.inner gq) (P {.a (.b (.inner gq)) .b (.a (.inner gq))}))
|
||||
(show (.inner gq))
|
||||
(let [p (P {.a 1 .b 2})
|
||||
q (Q {.tag 0 .inner (P {.a 1 .b 2})})
|
||||
r (P {.a 1 .b 2})
|
||||
s (P {.a 1 .b 2})
|
||||
t (P {.a 1 .b 2})
|
||||
other (P {.a 3 .b 4})]
|
||||
;; A local.
|
||||
(set p (P {.a (.b p) .b (.a p)}))
|
||||
(show p)
|
||||
;; A field of a local.
|
||||
(set (.inner q) (P {.a (.b (.inner q)) .b (.a (.inner q))}))
|
||||
(show (.inner q))
|
||||
;; A place behind a pointer, reading the variable it points at.
|
||||
(let [pr (addr r)]
|
||||
(set (deref pr) (P {.a (.b r) .b (.a r)}))
|
||||
(show r))
|
||||
;; A variable, reading itself through a pointer.
|
||||
(let [ps (addr s)]
|
||||
(set s (P {.a (.b (deref ps)) .b (.a (deref ps))}))
|
||||
(show s))
|
||||
;; A local rebound in a loop, reading its own previous turn.
|
||||
(let [pt (addr t)
|
||||
i 0]
|
||||
(while (< i 2)
|
||||
(let [u (P {.a (.b (deref pt)) .b (.a (deref pt))})]
|
||||
(show u)
|
||||
(set pt (addr u)))
|
||||
(set i (+ i 1))))
|
||||
;; No self-read.
|
||||
(set p (P {.a (.b other) .b (.a other)}))
|
||||
(show p)))
|
||||
@ -2720,6 +2720,20 @@ let () =
|
||||
outputs ~x86:true
|
||||
"an assignment signalled through leaves the old value, --x86"
|
||||
"programs/half-write.flan" half_write_out;
|
||||
(* The other half of building in place: a value that reads its own
|
||||
destination has to see the old value in every field. x86 built
|
||||
[(set p (P {.a (.b p) .b (.a p)}))] into [p] and answered 2 2. *)
|
||||
let self_read_out =
|
||||
"2 1\n2 1\n2 1\n2 1\n2 1\n2 1\n2 1\n1 2\n4 3\n"
|
||||
in
|
||||
outputs "an aggregate that reads its destination sees the old value"
|
||||
"programs/self-read.flan" self_read_out;
|
||||
outputs ~opt:"-O0"
|
||||
"an aggregate that reads its destination sees the old value, -O0"
|
||||
"programs/self-read.flan" self_read_out;
|
||||
outputs ~x86:true
|
||||
"an aggregate that reads its destination sees the old value, --x86"
|
||||
"programs/self-read.flan" self_read_out;
|
||||
|
||||
(* ── Packages: the link follows the program ────────────────────────
|
||||
A package's C and linker arguments used to come with the import,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user