An aggregate that reads its own destination sees the old value on x86, as it does on LLVM
This commit is contained in:
commit
f83e803ef8
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,
|
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.
|
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
|
** DONE A global initialiser builds an aggregate through the same temporary on x86
|
||||||
The statement-level assignment goes through a frame temporary now, so the
|
CLOSED: [2026-09-25]
|
||||||
half-write is closed where a program can observe it. The release build's
|
Already true when the entry was written. Every computed initialiser, release
|
||||||
globals-init path was not converted and still writes in place.
|
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
|
** DONE An aggregate built in place never reads its own destination
|
||||||
=(set p (P {.a (.b p) .b (.a p)}))= answers =2 1= under LLVM and =2 2= under
|
CLOSED: [2026-09-25]
|
||||||
=--x86=: lowering asks whether it can transfer and not whether it can alias. The
|
=assign= builds in place only when the right-hand side settles *and* reads no
|
||||||
fix has a shape already — the same temporary, chosen by an aliasing question.
|
storage the destination lies in; otherwise it goes through the temporary. Two
|
||||||
Whether the two become one predicate or two is the lane to decide.
|
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
|
** TODO A dyn value read out of a place is unrooted until it is stored, on both backends
|
||||||
No root table names it. Harmless only while a dyn field in a struct was refused,
|
First filed as the x86 aggregate temporary being unrooted. It is wider than
|
||||||
and per-type descriptors have since lifted that. Whoever relies on a dyn field
|
that: a dyn loaded from a place and held while a later sibling allocates is
|
||||||
reaching this path has to root the buffer, or a collection running inside the
|
unrooted on LLVM -O0 as well, in an aggregate literal or an argument list alike.
|
||||||
construction will not see what has been built so far.
|
=(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
|
** 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
|
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
|
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.
|
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
|
## 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
|
`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. *)
|
(* An index is bounds-checked, and the check signals. *)
|
||||||
| Tast.Pindex _ -> false
|
| 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 =
|
let rec lower f (e : Tast.expr) (dst : loc) : unit =
|
||||||
(* The one hook the line table needs, and it is here rather than at
|
(* 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
|
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. *)
|
nowhere else. *)
|
||||||
scoped f (fun () ->
|
scoped f (fun () ->
|
||||||
let d = Lf f.slots.(slot) in
|
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)
|
bind_slot f slot)
|
||||||
bs;
|
bs;
|
||||||
block f body dst t
|
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)
|
| None -> unsupported "continue %d outside a loop" n)
|
||||||
| Tast.Set (p, v) ->
|
| Tast.Set (p, v) ->
|
||||||
let l = place f p in
|
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) ->
|
| Tast.Make (sn, xs) ->
|
||||||
let offs = field_offsets f sn in
|
let offs = field_offsets f sn in
|
||||||
List.iteri
|
List.iteri
|
||||||
@ -2386,11 +2463,15 @@ and block f body dst t =
|
|||||||
instruction and it happens after the transfer guard, so a call that
|
instruction and it happens after the transfer guard, so a call that
|
||||||
signalled never reaches it.
|
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
|
[dst] is worked out by the caller and outlives this: [scoped] reclaims the
|
||||||
temporary, not the destination. *)
|
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
|
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
|
else begin
|
||||||
let o = Lf (tmp f t) in
|
let o = Lf (tmp f t) in
|
||||||
lower f v o;
|
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
|
outputs ~x86:true
|
||||||
"an assignment signalled through leaves the old value, --x86"
|
"an assignment signalled through leaves the old value, --x86"
|
||||||
"programs/half-write.flan" half_write_out;
|
"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 ────────────────────────
|
(* ── Packages: the link follows the program ────────────────────────
|
||||||
A package's C and linker arguments used to come with the import,
|
A package's C and linker arguments used to come with the import,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user