A dyn value held beside a sibling that can allocate is rooted until it is used, on every backend
This commit is contained in:
commit
795ba945d9
26
TODO.org
26
TODO.org
@ -1075,16 +1075,12 @@ 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 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.
|
||||
** DONE The aggregate temporary is an unrooted buffer while it is filled
|
||||
CLOSED: [2026-09-25]
|
||||
The buffer stays unrooted. Every dyn word written into it while a sibling field
|
||||
may allocate is also in a root slot of its own, pinned as that field was
|
||||
computed, so the collector sees what has been built so far without a root for
|
||||
the buffer.
|
||||
|
||||
** 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
|
||||
@ -1265,6 +1261,16 @@ A struct or a condition holding a =dyn= carries a descriptor naming the byte
|
||||
offsets the collector must follow, emitted by both backends and by both
|
||||
redefinition emitters. The stopgap refusal on a dyn field in a struct is lifted.
|
||||
|
||||
** DONE A dyn read out of a place is rooted while a sibling operand runs
|
||||
CLOSED: [2026-09-25]
|
||||
An operand holding a dyn word — a call's, runtime call's or primitive's
|
||||
argument, a struct or array literal's element, the temporary array an =at= or
|
||||
=slice= indexes — is spilled into a pushed root slot as soon as it is computed,
|
||||
whenever another operand in the same list does not settle. Same
|
||||
explicit-slot root stack on both backends and on wasm32; rules out anything that
|
||||
scans the native stack or registers. See docs/BUILT.md, "Operands held beside a
|
||||
sibling".
|
||||
|
||||
** DONE A redefined defclass migrates its instances lazily
|
||||
CLOSED: [2026-09-20]
|
||||
CLHS 4.3.6 minus the user hook. Nothing is enumerated and no heap is walked — the
|
||||
|
||||
@ -7074,3 +7074,74 @@ default x86 and `--llvm`. `programs/dev-rerun.flan` keeps the other half.
|
||||
|
||||
The `defconst` there is an `f64`, deliberately: the checker folds *integer* constants on the way in, and a folded one
|
||||
is refused by name rather than published, so an `i64` would have pinned the refusal instead of the store.
|
||||
|
||||
## How a dyn value is rooted, and operands held beside a sibling
|
||||
|
||||
The collector in `runtime/flan_dyn.c` is precise and finds its roots by address. `gc_mark_all` reads two things: the
|
||||
root stack, which is an array of addresses pushed through `flan_dyn_root_push` (a bare dyn word) and
|
||||
`flan_dyn_root_push_desc` (an aggregate, with the descriptor naming its dyn offsets), and a ring of the last 64
|
||||
allocations. It never scans the machine stack or registers. That is what lets the same scheme run on wasm32, where a
|
||||
program cannot read its own stack: every root is a slot the compiled code told the runtime about.
|
||||
|
||||
What gets a slot is decided once per function by `Emit.root_plan`, and both backends read that one plan rather than
|
||||
each deriving it. Before this section's change it named three kinds of root: every frame slot whose type holds a dyn
|
||||
word, the result of every call or runtime call that answers one (spilled into its own slot the instant it returns,
|
||||
because the callee popped its roots on the way out), and the few non-place expressions whose *address* is handed to
|
||||
something that can allocate. All the slots are made, zeroed and pushed at entry and popped by count at every exit.
|
||||
|
||||
### Operands held beside a sibling
|
||||
|
||||
A dyn read out of a place is rooted by the place, and only for as long as the place still holds it. In
|
||||
|
||||
```
|
||||
(set (.d dst) (pick (.d other) (do (set other (T {.n 0})) (churn))))
|
||||
```
|
||||
|
||||
the first argument is loaded, the second overwrites `other` and allocates, and between the two the only copy of the
|
||||
first is a register or a scratch temporary that no root names. A collection there freed it: the program printed a
|
||||
freed block's contents on LLVM, LLVM `-O0`, `--x86` and wasm32 alike, and valgrind showed `gc_mark_all` reading a
|
||||
block `gc_sweep` had freed.
|
||||
|
||||
The positions that hold a value while more code runs are the ordered operand lists — a call's arguments, a runtime
|
||||
call's, a primitive's, a struct or array literal's fields. The positions outside a list each have one operand and
|
||||
nothing beside it to run: a `let` stores it into a rooted slot, a `set` into its place (both backends compute the
|
||||
place before the value), a branch tests it, a `return` hands it back. So `Emit.held_operands` looks only at those
|
||||
lists, and pins an operand when:
|
||||
|
||||
- its type holds a dyn word, as a bare dyn or through a descriptor;
|
||||
- nothing roots it already — a call's result has its own slot from the rule above;
|
||||
- some *other* operand in the same list does not settle.
|
||||
|
||||
`Emit.settles` is the closed list of expressions that cannot reach a call, a signal, a check, a store or a return — the
|
||||
same question the x86 backend asks before building an aggregate in place. A sibling that settles can neither allocate
|
||||
nor overwrite a place, so a value beside it is safe where it is. Asking about every other operand, not only the later
|
||||
ones, means the plan does not depend on the order a backend evaluates operands in.
|
||||
|
||||
Some operands are taken by address rather than by value: the array `at` and `slice` index, and an array handed to the
|
||||
runtime. What such an operand holds is whatever its address was taken of. A place needs nothing, being rooted where it
|
||||
was declared. Anything else — `(at [(.d other) (.d other)] (clobber-idx))`, or the same array reached through a field
|
||||
of a struct literal — is copied into a temporary first, and that temporary sits unrooted while the index runs.
|
||||
`addr_base` walks down through fields and elements to that temporary, and it is the node pinned.
|
||||
|
||||
A dyn inside a `defdata` payload and an `(Option dyn)` are both refused before code generation, so a case literal and
|
||||
`some` are not positions this covers today.
|
||||
|
||||
A pinned operand is spilled into a root slot as soon as it is computed, in `Emit.value` and in `X86.lower`, which is
|
||||
the same move a call's result gets and uses the same slots. The pinned nodes are keyed by physical identity. The
|
||||
checker occasionally shares one node between two positions, and a node pinned in one position is then spilled from
|
||||
both, so the count of slots is taken per visit of a pinned node rather than per position that asked for it —
|
||||
otherwise the supply runs short and the backends fall back to an unrooted temporary. The acceptance suite's
|
||||
`%dx`/`%ax` check sees that fallback on LLVM; it caught exactly this during the change.
|
||||
|
||||
The x86 backend's aggregate temporary, which builds a struct literal field by field, needs no root of its own for the
|
||||
same reason: every dyn word written into it while a sibling field may allocate is also in a pinned slot.
|
||||
|
||||
What it costs is a store per pinned operand and a slot per pin in the entry block. Over `dyn-class.flan`, `dyn-map.flan`
|
||||
and `edn-read.flan` the emitted root pushes went up by roughly a quarter to a half. Typed code is unaffected: nothing
|
||||
without a dyn word in it is ever pinned.
|
||||
|
||||
`programs/dyn-held-operand.flan` is the fixture, ten cases on LLVM, `-O0`, `--x86` and wasm32: a call's argument, a
|
||||
struct literal's field, an array literal's element, a runtime call's argument (dyn `=`), a local overwritten by the
|
||||
sibling, a whole struct passed by value, a struct nested in a literal, a call through a function value, and an array
|
||||
literal indexed while the index runs, directly and through a struct literal's field. Without the pins every line
|
||||
prints freed memory.
|
||||
|
||||
223
lib/emit.ml
223
lib/emit.ml
@ -954,6 +954,10 @@ type f = {
|
||||
within a pool never matters. See [root_plan] for why this is by type and
|
||||
not a single positional list. *)
|
||||
mutable aroot_ns : (string * string list) list;
|
||||
(* The operands [root_plan] found held beside a sibling that may allocate,
|
||||
by physical identity. [value] spills each of them into a root slot the
|
||||
moment it is computed. See [held_operands]. *)
|
||||
mutable pins : Tast.expr list;
|
||||
(* Where a dev build records each slot's address, so that a stopped frame's
|
||||
locals can be read. [None] in a release build and in a function with no
|
||||
named slot at all. Only *named* slots are recorded: a slot the compiler
|
||||
@ -1087,6 +1091,93 @@ let label f name =
|
||||
count below is still the number the epilogue takes off; what changed is that
|
||||
the plan has to say, per entry, which kind it is. *)
|
||||
|
||||
(* Whether lowering this expression into a destination is bound to reach the
|
||||
end of it.
|
||||
|
||||
Asked first by the x86 backend. Nothing there holds a value in a register
|
||||
across a statement, so an aggregate is built *in the destination*: field by field, element by
|
||||
element, each one stored as it is computed. That is the cheapest thing that
|
||||
works, and it works right up until the middle of the construction leaves.
|
||||
A condition signalled while the third of four elements is being computed
|
||||
transfers out of the assignment with two elements written and two not, and
|
||||
what is left behind is a variable that is half its old value and half its
|
||||
new one. The LLVM backend cannot have that shape — it builds the whole
|
||||
aggregate as one value and stores it once — and where the two disagree,
|
||||
the x86 one is the one that is wrong.
|
||||
|
||||
So an x86 assignment whose right-hand side might leave part-way builds into a
|
||||
frame temporary and copies the finished value over in a single block move.
|
||||
The copy is not free, which is what this question is for: it buys nothing
|
||||
for [(set grid [1 2 3 4])], where nothing between the first store and the
|
||||
last can go anywhere at all.
|
||||
|
||||
The answer is yes only for shapes spelled out below, none of which can
|
||||
reach a call, a signal, a bounds or arithmetic check, or a return. Anything
|
||||
else answers no and pays for the copy — a node added to the IR later
|
||||
included, since being wrong this way costs a block copy and being wrong the
|
||||
other way costs a corrupted variable.
|
||||
|
||||
[root_plan] asks the same question for a different reason: a sibling
|
||||
operand that settles can neither allocate nor overwrite a place, so a dyn
|
||||
value held beside it needs no root of its own. See [held_operands]. *)
|
||||
let settled_prim (p : Tast.prim) =
|
||||
match p with
|
||||
(* Arithmetic that cannot fail. Division and remainder are absent on
|
||||
purpose: both are checked, and a check signals. *)
|
||||
| Tast.Add | Tast.Sub | Tast.Mul
|
||||
| Tast.Eq | Tast.Ne | Tast.Lt | Tast.Le | Tast.Gt | Tast.Ge | Tast.Not
|
||||
| Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr
|
||||
(* Questions about a value's shape, answered from the layout tables. *)
|
||||
| Tast.Len | Tast.SizeOf _ | Tast.AlignOf _ | Tast.AddrOf -> true
|
||||
(* Everything else reaches C, signals, or both: an index and a slice are
|
||||
bounds-checked and [Rt] is a call by definition. [Cast] is not here
|
||||
because it is only sometimes checked; see [cast_checks]. *)
|
||||
| _ -> false
|
||||
|
||||
(* Which conversions can signal, and it is one of them. [cvttsd2si] answers a
|
||||
fixed "integer indefinite" for a value out of range, which is a number
|
||||
rather than an answer, so a float narrowed to an integer is tested against
|
||||
the destination's range first and the failure signals ArithError — see
|
||||
[cast], which is where the test is emitted. Every other conversion is a
|
||||
move, a widen or one SSE instruction, and cannot go anywhere. That matters
|
||||
here because an array of the language's own literals is usually written
|
||||
[(u32 1)] and not [1], and refusing every cast would have made the most
|
||||
ordinary aggregate literal there is pay for a copy. *)
|
||||
let cast_checks (src : Types.t) (target : Types.t) =
|
||||
let concrete (t : Types.t) =
|
||||
match t with Types.Enum _ -> Types.Int Types.I32 | t -> t
|
||||
in
|
||||
match concrete src, concrete target with
|
||||
| Types.Float _, Types.Int _ -> true
|
||||
| _ -> false
|
||||
|
||||
let rec settles (e : Tast.expr) =
|
||||
match e.Tast.e with
|
||||
(* Values with no code in them to leave from. *)
|
||||
| Tast.Int _ | Tast.Float _ | Tast.Bool _ | Tast.Str _ | Tast.Unit
|
||||
| Tast.Zero _ | Tast.Uninit _ | Tast.Local _ | Tast.Global _ | Tast.None_
|
||||
| Tast.FnAddr _ -> true
|
||||
(* Building an aggregate, and running a sequence: as settled as the parts. *)
|
||||
| Tast.Make (_, es) | Tast.MakeCase (_, _, es) | Tast.Arr es | Tast.Do es ->
|
||||
List.for_all settles es
|
||||
| Tast.Some_ x | Tast.Field (x, _) | Tast.CaseField (x, _, _)
|
||||
| Tast.Deref x -> settles x
|
||||
| Tast.If (c, a, b) -> settles c && settles a && settles b
|
||||
| Tast.Addr p -> settles_place p
|
||||
| Tast.Prim (Tast.Cast target, [ a ]) ->
|
||||
(not (cast_checks a.Tast.ty target)) && settles a
|
||||
| Tast.Prim (p, es) -> settled_prim p && List.for_all settles es
|
||||
(* A call, a signal, an invoke, an unwrap that returns early, a loop, a
|
||||
[return], a [break] — and anything this list has not heard of. *)
|
||||
| _ -> false
|
||||
|
||||
and settles_place (p : Tast.place) =
|
||||
match p with
|
||||
| Tast.Plocal _ | Tast.Pglobal _ -> true
|
||||
| Tast.Pfield (x, _) | Tast.Pderef x -> settles x
|
||||
(* An index is bounds-checked, and the check signals. *)
|
||||
| Tast.Pindex _ -> false
|
||||
|
||||
(* Which expressions [addr] can answer without copying. Shared with [addr]
|
||||
itself rather than repeated, because the counter below has to make exactly
|
||||
the same call: a place has an address already and needs no root, and
|
||||
@ -1122,8 +1213,90 @@ type rootplan = {
|
||||
rslots : (int * Types.t) list; (* slot index and its type, in slot order *)
|
||||
rdyn : int;
|
||||
ragg : Types.t list; (* sorted, with multiplicity *)
|
||||
rpins : Tast.expr list; (* see [held_operands] *)
|
||||
}
|
||||
|
||||
(* The operands whose value is held while one of their siblings runs, and which
|
||||
therefore need a root of their own from the moment they are computed.
|
||||
|
||||
A dyn read out of a place — a local, a global, a field — is rooted only by
|
||||
the place, and only for as long as the place still holds it. In
|
||||
[(pick (.d other) (do (set other ...) (churn)))] the first argument is
|
||||
loaded, the second overwrites [other] and allocates, and between the two
|
||||
the only copy of the first is a register or a temporary no root points at,
|
||||
so a collection there frees it. Nothing about the place was wrong; what was
|
||||
missing is a root for the value between being produced and being consumed.
|
||||
|
||||
The positions that hold a value while more code runs are the ordered
|
||||
operand lists: a call's arguments, a runtime call's, a primitive's, a
|
||||
struct or array literal's fields. An operand taken by address — the array
|
||||
[at] and [slice] index, an array handed to the runtime — holds whatever its
|
||||
address was taken of, which is a temporary when that is not a place. The
|
||||
positions outside a list each have one operand and nothing beside it to
|
||||
run: a [let] stores it into a rooted slot, a [set] into its place (both
|
||||
backends compute the place first), a branch tests it, a [return] hands it
|
||||
back.
|
||||
|
||||
An operand is pinned when its type holds a dyn word, when nothing already
|
||||
roots it — a call's result and a dyn-producing runtime call's are spilled
|
||||
into their own slot where they are made — and when some *other* operand in
|
||||
the same list does not settle. A sibling that settles cannot allocate and
|
||||
cannot write a place, so a value beside it is safe where it is. Any other
|
||||
sibling, before or after, counts: which order the operands run in is then
|
||||
not a question this plan has to agree with a backend about.
|
||||
|
||||
The node itself is the key, by physical identity, and the backends spill
|
||||
its value after computing it — [value] in this file, [lower] in the other —
|
||||
exactly as they spill a call's result. For an operand taken by address the
|
||||
node pinned is the temporary under it, found by [addr_base]; a place under
|
||||
it needs nothing, and neither backend evaluates one through either hook. *)
|
||||
let held_operands m (e : Tast.expr) : Tast.expr list =
|
||||
let holds (x : Tast.expr) =
|
||||
(x.Tast.ty = Types.Dyn || dyn_offsets m x.Tast.ty <> [])
|
||||
&& (match x.Tast.e with
|
||||
| Tast.Call _ | Tast.CallPtr _ -> false
|
||||
| Tast.Prim (Tast.Rt _, _) -> x.Tast.ty <> Types.Dyn
|
||||
| Tast.Zero _ | Tast.Uninit _ | Tast.None_ | Tast.Unit -> false
|
||||
| _ -> true)
|
||||
in
|
||||
(* An operand taken by address holds the value its address is taken of.
|
||||
A place holds nothing new: its storage is rooted where it was declared.
|
||||
A field or an element of something else is an address into that
|
||||
something, and a value that is not a place is copied into a temporary —
|
||||
which is the value held, and the node both backends evaluate. *)
|
||||
let rec addr_base (x : Tast.expr) =
|
||||
match x.Tast.e with
|
||||
| Tast.Local _ | Tast.Global _ | Tast.Deref _ | Tast.CaseField _ -> None
|
||||
| Tast.Field (t, _) -> addr_base t
|
||||
| Tast.Prim (Tast.At, t :: _ :: _) -> addr_base t
|
||||
| _ -> Some x
|
||||
in
|
||||
(* [ops] is the list with, for each operand, whether it crosses by address. *)
|
||||
let pick ops =
|
||||
List.filter_map
|
||||
(fun ((x : Tast.expr), by_addr) ->
|
||||
let held = if by_addr then addr_base x else Some x in
|
||||
match held with
|
||||
| Some h when holds h
|
||||
&& List.exists
|
||||
(fun ((s : Tast.expr), _) -> s != x && not (settles s))
|
||||
ops -> Some h
|
||||
| _ -> None)
|
||||
ops
|
||||
in
|
||||
let by_value es = List.map (fun x -> (x, false)) es in
|
||||
let is_array (x : Tast.expr) =
|
||||
match x.Tast.ty with Types.Array _ -> true | _ -> false
|
||||
in
|
||||
match e.Tast.e with
|
||||
| Tast.Prim (Tast.Rt _, es) -> pick (List.map (fun x -> (x, is_array x)) es)
|
||||
| Tast.Prim ((Tast.At | Tast.Slice), t :: rest) ->
|
||||
pick ((t, true) :: by_value rest)
|
||||
| Tast.Prim (_, es) | Tast.Call (_, es) | Tast.Make (_, es)
|
||||
| Tast.MakeCase (_, _, es) | Tast.Arr es -> pick (by_value es)
|
||||
| Tast.CallPtr (c, es) -> pick (by_value (c :: es))
|
||||
| _ -> []
|
||||
|
||||
let root_plan m (fn : Tast.fn) : rootplan =
|
||||
let rslots = ref [] in
|
||||
Array.iteri
|
||||
@ -1133,7 +1306,24 @@ let root_plan m (fn : Tast.fn) : rootplan =
|
||||
fn.Tast.slots;
|
||||
let dyn = ref 0 and agg = ref [] in
|
||||
let want (t : Types.t) = t <> Types.Dyn && dyn_offsets m t <> [] in
|
||||
(* The pinned operands first, as a set of nodes. The checker shares a node
|
||||
between two positions now and then — the same [Local] read in two places
|
||||
— and the backends spill by identity, so a node pinned in one position is
|
||||
spilled in every position it is emitted from. The count below is
|
||||
therefore taken per visit of a pinned node, which is per emission, and
|
||||
not per position that asked for the pin. *)
|
||||
let pins = ref [] in
|
||||
let collect (e : Tast.expr) =
|
||||
List.iter
|
||||
(fun x -> if not (List.memq x !pins) then pins := x :: !pins)
|
||||
(held_operands m e)
|
||||
in
|
||||
List.iter (Tast.walk collect) fn.Tast.body;
|
||||
List.iter (Tast.walk collect) fn.Tast.fdefers;
|
||||
let count (e : Tast.expr) =
|
||||
if !pins <> [] && List.memq e !pins then begin
|
||||
if e.Tast.ty = Types.Dyn then incr dyn else agg := e.Tast.ty :: !agg
|
||||
end;
|
||||
match e.Tast.e with
|
||||
| Tast.Prim (Tast.Rt _, _) when e.Tast.ty = Types.Dyn -> incr dyn
|
||||
(* A Flan call answering a dyn, which wants the same slot a dyn-producing
|
||||
@ -1177,7 +1367,8 @@ let root_plan m (fn : Tast.fn) : rootplan =
|
||||
asm listing and an IR listing put the same value at the same depth. *)
|
||||
ragg =
|
||||
List.sort (fun a b -> String.compare (Types.to_string a) (Types.to_string b))
|
||||
!agg }
|
||||
!agg;
|
||||
rpins = !pins }
|
||||
|
||||
let dyn_roots m (fn : Tast.fn) =
|
||||
let p = root_plan m fn in
|
||||
@ -1770,14 +1961,25 @@ let fcmp_op = function
|
||||
a child -- the branch at the end of an [if], the store of a [set] -- are
|
||||
attributed to the parent and not to whatever ran last inside it. *)
|
||||
let rec value f (e : Tast.expr) : string =
|
||||
match f.dsub with
|
||||
| None -> value_at f e
|
||||
| Some _ ->
|
||||
let saved = f.dloc in
|
||||
at_loc f e.Tast.loc;
|
||||
let v = value_at f e in
|
||||
f.dloc <- saved;
|
||||
v
|
||||
let v =
|
||||
match f.dsub with
|
||||
| None -> value_at f e
|
||||
| Some _ ->
|
||||
let saved = f.dloc in
|
||||
at_loc f e.Tast.loc;
|
||||
let v = value_at f e in
|
||||
f.dloc <- saved;
|
||||
v
|
||||
in
|
||||
(* An operand held while a sibling may allocate: spilled into a root slot the
|
||||
instant it exists, the same move a call's result gets in [call_through].
|
||||
The value carries on being used as a register; the store is what the
|
||||
collector reads. *)
|
||||
if f.pins <> [] && List.memq e f.pins then begin
|
||||
if e.Tast.ty = Types.Dyn then ins f "store i64 %s, ptr %s" v (dyn_tmp f)
|
||||
else ins f "store %s %s, ptr %s" (ll e.Tast.ty) v (agg_tmp f e.Tast.ty)
|
||||
end;
|
||||
v
|
||||
|
||||
(* The [!DILocation] for a position, memoised: a loop body emits the same few
|
||||
lines over and over and each would otherwise make its own node. *)
|
||||
@ -3341,7 +3543,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
|
||||
pads = []; loops = []; unwind = "unwind"; unwound = false;
|
||||
defers = fn.Tast.fdefers;
|
||||
frame = None; slotv = None; snames = fn.Tast.snames;
|
||||
droots = 0; droot_ns = []; aroot_ns = [];
|
||||
droots = 0; droot_ns = []; aroot_ns = []; pins = [];
|
||||
dsub;
|
||||
dline = (if fn.Tast.floc.Loc.line = 0 then 1 else fn.Tast.floc.Loc.line);
|
||||
dloc = "";
|
||||
@ -3453,6 +3655,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
|
||||
List.iter (fun (ty, n) -> push n ty) atemps;
|
||||
f.droots <- nroots;
|
||||
f.droot_ns <- dtemps;
|
||||
f.pins <- plan.rpins;
|
||||
f.aroot_ns <-
|
||||
List.fold_left
|
||||
(fun acc (ty, n) ->
|
||||
|
||||
108
lib/x86.ml
108
lib/x86.ml
@ -708,6 +708,10 @@ type fnctx = {
|
||||
type is interchangeable, so a supply that drifted from the emission can
|
||||
only run out and never pair an address with another type's descriptor. *)
|
||||
mutable aroot_ns : (string * int list) list;
|
||||
(* [emit.ml]'s [pins]: the operands [Emit.root_plan] found held beside a
|
||||
sibling that may allocate, spilled by [lower] into a root slot as soon as
|
||||
each is computed. *)
|
||||
mutable pins : Tast.expr list;
|
||||
(* [fn.snames], carried so [bind_slot] can ask whether a slot has a name to
|
||||
show without the whole [Tast.fn] being threaded to every binding site. *)
|
||||
snames : string option array;
|
||||
@ -1594,88 +1598,8 @@ let emit_args f (args : arg list) =
|
||||
point is that the store has somewhere legal to go. *)
|
||||
let sink = Lf 0
|
||||
|
||||
(* Whether lowering this expression into a destination is bound to reach the
|
||||
end of it.
|
||||
|
||||
Nothing in this backend holds a value in a register across a statement, so
|
||||
an aggregate is built *in the destination*: field by field, element by
|
||||
element, each one stored as it is computed. That is the cheapest thing that
|
||||
works, and it works right up until the middle of the construction leaves.
|
||||
A condition signalled while the third of four elements is being computed
|
||||
transfers out of the assignment with two elements written and two not, and
|
||||
what is left behind is a variable that is half its old value and half its
|
||||
new one. The LLVM backend cannot have that shape — it builds the whole
|
||||
aggregate as one value and stores it once — and where the two disagree,
|
||||
this one is the one that is wrong.
|
||||
|
||||
So an assignment whose right-hand side might leave part-way builds into a
|
||||
frame temporary and copies the finished value over in a single block move.
|
||||
The copy is not free, which is what this question is for: it buys nothing
|
||||
for [(set grid [1 2 3 4])], where nothing between the first store and the
|
||||
last can go anywhere at all.
|
||||
|
||||
The answer is yes only for shapes spelled out below, none of which can
|
||||
reach a call, a signal, a bounds or arithmetic check, or a return. Anything
|
||||
else answers no and pays for the copy — a node added to the IR later
|
||||
included, since being wrong this way costs a block copy and being wrong the
|
||||
other way costs a corrupted variable. *)
|
||||
let settled_prim (p : Tast.prim) =
|
||||
match p with
|
||||
(* Arithmetic that cannot fail. Division and remainder are absent on
|
||||
purpose: both are checked, and a check signals. *)
|
||||
| Tast.Add | Tast.Sub | Tast.Mul
|
||||
| Tast.Eq | Tast.Ne | Tast.Lt | Tast.Le | Tast.Gt | Tast.Ge | Tast.Not
|
||||
| Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr
|
||||
(* Questions about a value's shape, answered from the layout tables. *)
|
||||
| Tast.Len | Tast.SizeOf _ | Tast.AlignOf _ | Tast.AddrOf -> true
|
||||
(* Everything else reaches C, signals, or both: an index and a slice are
|
||||
bounds-checked and [Rt] is a call by definition. [Cast] is not here
|
||||
because it is only sometimes checked; see [cast_checks]. *)
|
||||
| _ -> false
|
||||
|
||||
(* Which conversions can signal, and it is one of them. [cvttsd2si] answers a
|
||||
fixed "integer indefinite" for a value out of range, which is a number
|
||||
rather than an answer, so a float narrowed to an integer is tested against
|
||||
the destination's range first and the failure signals ArithError — see
|
||||
[cast], which is where the test is emitted. Every other conversion is a
|
||||
move, a widen or one SSE instruction, and cannot go anywhere. That matters
|
||||
here because an array of the language's own literals is usually written
|
||||
[(u32 1)] and not [1], and refusing every cast would have made the most
|
||||
ordinary aggregate literal there is pay for a copy. *)
|
||||
let cast_checks (src : Types.t) (target : Types.t) =
|
||||
let concrete (t : Types.t) =
|
||||
match t with Types.Enum _ -> Types.Int Types.I32 | t -> t
|
||||
in
|
||||
match concrete src, concrete target with
|
||||
| Types.Float _, Types.Int _ -> true
|
||||
| _ -> false
|
||||
|
||||
let rec settles (e : Tast.expr) =
|
||||
match e.Tast.e with
|
||||
(* Values with no code in them to leave from. *)
|
||||
| Tast.Int _ | Tast.Float _ | Tast.Bool _ | Tast.Str _ | Tast.Unit
|
||||
| Tast.Zero _ | Tast.Uninit _ | Tast.Local _ | Tast.Global _ | Tast.None_
|
||||
| Tast.FnAddr _ -> true
|
||||
(* Building an aggregate, and running a sequence: as settled as the parts. *)
|
||||
| Tast.Make (_, es) | Tast.MakeCase (_, _, es) | Tast.Arr es | Tast.Do es ->
|
||||
List.for_all settles es
|
||||
| Tast.Some_ x | Tast.Field (x, _) | Tast.CaseField (x, _, _)
|
||||
| Tast.Deref x -> settles x
|
||||
| Tast.If (c, a, b) -> settles c && settles a && settles b
|
||||
| Tast.Addr p -> settles_place p
|
||||
| Tast.Prim (Tast.Cast target, [ a ]) ->
|
||||
(not (cast_checks a.Tast.ty target)) && settles a
|
||||
| Tast.Prim (p, es) -> settled_prim p && List.for_all settles es
|
||||
(* A call, a signal, an invoke, an unwrap that returns early, a loop, a
|
||||
[return], a [break] — and anything this list has not heard of. *)
|
||||
| _ -> false
|
||||
|
||||
and settles_place (p : Tast.place) =
|
||||
match p with
|
||||
| Tast.Plocal _ | Tast.Pglobal _ -> true
|
||||
| Tast.Pfield (x, _) | Tast.Pderef x -> settles x
|
||||
(* An index is bounds-checked, and the check signals. *)
|
||||
| Tast.Pindex _ -> false
|
||||
(* [Emit.settles], whose comment gives this backend's reason for asking. *)
|
||||
let settles = Emit.settles
|
||||
|
||||
(* 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
|
||||
@ -1764,7 +1688,7 @@ let rec lower f (e : Tast.expr) (dst : loc) : unit =
|
||||
scoped f (fun () ->
|
||||
let o = tmp f e.Tast.ty in
|
||||
lower f e (Lf o))
|
||||
else if not f.ann then lower_at f e dst
|
||||
else if not f.ann then (lower_at f e dst; pin f e dst)
|
||||
else begin
|
||||
(* The second hook, and it hangs off the same recursion for the same
|
||||
reason: a heading queued here spans exactly the bytes this form and
|
||||
@ -1779,6 +1703,7 @@ let rec lower f (e : Tast.expr) (dst : loc) : unit =
|
||||
let s = annot f e in
|
||||
f.adepth <- d + 1;
|
||||
lower_at f e dst;
|
||||
pin f e dst;
|
||||
f.adepth <- d;
|
||||
set_ind f.b ind;
|
||||
match s with
|
||||
@ -1786,6 +1711,16 @@ let rec lower f (e : Tast.expr) (dst : loc) : unit =
|
||||
| None -> ()
|
||||
end
|
||||
|
||||
(* An operand held while a sibling may allocate, copied into a root slot the
|
||||
moment it has been lowered — [Emit.held_operands] for which ones and why.
|
||||
[dst] is not enough: it is usually a [scoped] temporary, or a field of an
|
||||
aggregate being built in one, and never a slot anything was pushed for. *)
|
||||
and pin f (e : Tast.expr) (dst : loc) =
|
||||
if f.pins <> [] && List.memq e f.pins then begin
|
||||
let o = if e.Tast.ty = Types.Dyn then dyn_tmp f else agg_tmp f e.Tast.ty in
|
||||
move f ~dst:(Lf o) ~src:dst e.Tast.ty
|
||||
end
|
||||
|
||||
and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
let t = e.Tast.ty in
|
||||
match e.Tast.e with
|
||||
@ -3761,7 +3696,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
{ b; md; fnname = fn.Tast.name; retlbl = "";
|
||||
fret = fn.Tast.ret; slots = Array.make nslots 0;
|
||||
xfer_off = 0; sret_off = 0; retval = 0;
|
||||
dframe = None; dslotv = None; droots = 0; droot_ns = []; aroot_ns = [];
|
||||
dframe = None; dslotv = None; droots = 0; droot_ns = []; aroot_ns = []; pins = [];
|
||||
snames = fn.Tast.snames;
|
||||
frame = 0; maxframe = 0; outgoing = 0;
|
||||
loops = []; pads = []; xfer_lbl = ""; unwound = false;
|
||||
@ -3876,6 +3811,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
droot_zero := zeros @ List.map (fun o -> (o, Types.Dyn)) dtemps @ atemps;
|
||||
f.droots <- nroots;
|
||||
f.droot_ns <- dtemps;
|
||||
f.pins <- plan.Emit.rpins;
|
||||
f.aroot_ns <-
|
||||
List.fold_left
|
||||
(fun acc (o, ty) ->
|
||||
@ -4479,7 +4415,7 @@ let emit_globals_init ?(cfi = false) ?(ann = false) ?body ~sym (md : Emit.m) ~ex
|
||||
let f =
|
||||
{ b; md; fnname = "<globals>"; retlbl = new_label () "ginit";
|
||||
fret = Types.Unit; slots = [||]; xfer_off = 0; sret_off = 0; retval = 0;
|
||||
dframe = None; dslotv = None; droots = 0; droot_ns = []; aroot_ns = [];
|
||||
dframe = None; dslotv = None; droots = 0; droot_ns = []; aroot_ns = []; pins = [];
|
||||
snames = [||];
|
||||
frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = [];
|
||||
xfer_lbl = ""; unwound = false;
|
||||
@ -5175,7 +5111,7 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
||||
let f =
|
||||
{ b = ib; md; fnname = "<install>"; retlbl = new_label () "install";
|
||||
fret = Types.Unit; slots = [||]; xfer_off = 0; sret_off = 0; retval = 0;
|
||||
dframe = None; dslotv = None; droots = 0; droot_ns = []; aroot_ns = [];
|
||||
dframe = None; dslotv = None; droots = 0; droot_ns = []; aroot_ns = []; pins = [];
|
||||
snames = [||];
|
||||
frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = [];
|
||||
xfer_lbl = ""; unwound = false;
|
||||
|
||||
104
test/programs/dyn-held-operand.flan
Normal file
104
test/programs/dyn-held-operand.flan
Normal file
@ -0,0 +1,104 @@
|
||||
;;;; A dyn value read out of a place, held while a sibling operand runs.
|
||||
;;;;
|
||||
;;;; Every line below reads the vector in [other] into an operand position —
|
||||
;;;; a call's argument, a struct literal's field, an array literal's element,
|
||||
;;;; a runtime call's argument, a function value's argument, the array an
|
||||
;;;; index is taken of — and then runs a
|
||||
;;;; sibling that overwrites [other] and allocates enough to collect. Once
|
||||
;;;; [other] is overwritten, the only reference to the vector is the operand
|
||||
;;;; being held, so unless that operand has a root of its own the collection
|
||||
;;;; frees it and the line prints whatever the freed block holds next.
|
||||
;;;;
|
||||
;;;; [churn] ends in an explicit collection, so each line is tested against a
|
||||
;;;; collection that certainly ran and not against the trigger's timing.
|
||||
|
||||
(defstruct T [d dyn n i64])
|
||||
(defstruct W [t T k i64])
|
||||
(defstruct A2 [xs [2 dyn]])
|
||||
(declare gc-collect [] () "flan_gc_collect")
|
||||
|
||||
(defonce other T)
|
||||
(defonce dst T)
|
||||
(defonce pair [2 dyn])
|
||||
(defonce w W)
|
||||
|
||||
(defn churn [] i64
|
||||
(let [i 0]
|
||||
(while (< i 2000)
|
||||
(let [v (vec-new dyn)] (push v i) (push v "junk"))
|
||||
(set i (+ i 1)))
|
||||
(gc-collect)
|
||||
7))
|
||||
|
||||
(defn kept [] dyn
|
||||
(let [v (vec-new dyn)] (push v "kept") (push v 42) v))
|
||||
|
||||
;;; A fresh vector in [other], old enough that it is no longer among the
|
||||
;;; collector's most recent allocations.
|
||||
(defn fill [] () (set (.d other) (kept)) (churn))
|
||||
|
||||
;;; Overwrite [other], then allocate and collect.
|
||||
(defn clobber [] i64 (set other (T {.n 0})) (churn))
|
||||
(defn clobber-dyn [] dyn (clobber) (vec-new dyn))
|
||||
(defn clobber-idx [] i32 (clobber) 0)
|
||||
|
||||
(defn pick [x dyn n i64] dyn x)
|
||||
(defn pick-t [t T n i64] dyn (.d t))
|
||||
|
||||
(defn show [label string v dyn] ()
|
||||
(churn)
|
||||
(println label (at v 0) (at v 1)))
|
||||
|
||||
(defn main [] ()
|
||||
;; A call's argument.
|
||||
(fill)
|
||||
(set (.d dst) (pick (.d other) (clobber)))
|
||||
(show "call:" (.d dst))
|
||||
|
||||
;; A struct literal's field.
|
||||
(fill)
|
||||
(set dst (T {.d (.d other) .n (clobber)}))
|
||||
(show "struct:" (.d dst))
|
||||
|
||||
;; An array literal's element.
|
||||
(fill)
|
||||
(set pair [(.d other) (clobber-dyn)])
|
||||
(show "array:" (at pair 0))
|
||||
|
||||
;; A runtime call's argument: dyn equality is a call into the runtime, and
|
||||
;; its first operand is held while the second is computed.
|
||||
(fill)
|
||||
(println "runtime:" (= (.d other) (do (clobber) (kept))))
|
||||
|
||||
;; A local, overwritten by the sibling rather than a global.
|
||||
(fill)
|
||||
(let [x (.d other)]
|
||||
(set (.d dst) (pick x (do (set x 0) (clobber))))
|
||||
(show "local:" (.d dst)))
|
||||
|
||||
;; A whole struct with a dyn field in it, passed by value.
|
||||
(fill)
|
||||
(set (.d dst) (pick-t other (clobber)))
|
||||
(show "aggregate:" (.d dst))
|
||||
|
||||
;; The same struct as a field of a struct literal.
|
||||
(fill)
|
||||
(set w (W {.t other .k (clobber)}))
|
||||
(show "nested:" (.d (.t w)))
|
||||
|
||||
;; Through a function value.
|
||||
(fill)
|
||||
(let [g pick]
|
||||
(set (.d dst) (g (.d other) (clobber)))
|
||||
(show "fn value:" (.d dst)))
|
||||
|
||||
;; An array literal indexed while the index runs: the array is a temporary
|
||||
;; taken by address, and the temporary is what is held.
|
||||
(fill)
|
||||
(set (.d dst) (at [(.d other) (.d other)] (clobber-idx)))
|
||||
(show "index:" (.d dst))
|
||||
|
||||
;; The same through a field of a struct literal.
|
||||
(fill)
|
||||
(set (.d dst) (at (.xs (A2 {.xs [(.d other) (.d other)]})) (clobber-idx)))
|
||||
(show "field index:" (.d dst)))
|
||||
@ -3231,6 +3231,14 @@ let () =
|
||||
refuses "a Vec crossing to C" "programs/vec-to-c.flan"
|
||||
"is a Vec, which owns its storage";
|
||||
|
||||
(* What [programs/dyn-held-operand.flan] prints, here rather than beside its
|
||||
three native rows further down because the wasm32 case below runs it
|
||||
too. *)
|
||||
let dyn_held_out =
|
||||
"call: kept 42\nstruct: kept 42\narray: kept 42\nruntime: true\n\
|
||||
local: kept 42\naggregate: kept 42\nnested: kept 42\n\
|
||||
fn value: kept 42\nindex: kept 42\nfield index: kept 42\n"
|
||||
in
|
||||
(* ── wasm32 ────────────────────────────────────────────────────────
|
||||
TODO.org, "The web target does not reach four things".
|
||||
|
||||
@ -3345,7 +3353,12 @@ let () =
|
||||
wasm_case "an imported package nothing calls, wasm32"
|
||||
"programs/pkg-unused.flan" "ok\n";
|
||||
wasm_case "an imported package nothing calls, wasm32, -O0"
|
||||
~opt:"-O0" "programs/pkg-unused.flan" "ok\n"));
|
||||
~opt:"-O0" "programs/pkg-unused.flan" "ok\n";
|
||||
(* The dyn roots, on the target that cannot scan its own stack: a
|
||||
value held beside a sibling that collects is rooted through the
|
||||
same explicit slots there as natively. *)
|
||||
wasm_case "dyn: an operand held while a sibling collects, wasm32"
|
||||
"programs/dyn-held-operand.flan" dyn_held_out));
|
||||
|
||||
(* The EDN tokenizer, and the struct reader written by hand against it
|
||||
(vendor/edn, test/programs/edn.flan). The expected output is a raw
|
||||
@ -4556,6 +4569,20 @@ level "1"
|
||||
"programs/dyn-struct.flan" dyn_struct_out;
|
||||
outputs ~x86:true "dyn: a struct with dyn fields, under collection, --x86"
|
||||
"programs/dyn-struct.flan" dyn_struct_out;
|
||||
(* A dyn read out of a place and held in an operand position while a
|
||||
sibling overwrites the place and collects: a call's argument, a struct
|
||||
and an array literal's element, a runtime call's argument, a local, a
|
||||
whole struct passed by value, a struct nested in a literal, a call
|
||||
through a function value, and an array literal indexed — directly and
|
||||
through a struct literal's field — while the index runs. [Emit.held_operands] roots each of them. With
|
||||
that function answering nothing, every line comes out as a freed
|
||||
block's contents and the runtime line as false, on all three rows. *)
|
||||
outputs "dyn: an operand held while a sibling collects"
|
||||
"programs/dyn-held-operand.flan" dyn_held_out;
|
||||
outputs ~opt:"-O0" "dyn: an operand held while a sibling collects, -O0"
|
||||
"programs/dyn-held-operand.flan" dyn_held_out;
|
||||
outputs ~x86:true "dyn: an operand held while a sibling collects, --x86"
|
||||
"programs/dyn-held-operand.flan" dyn_held_out;
|
||||
(* Two struct spellings the checker decides: a bare {.field v} typed by
|
||||
the position it stands in, and (Cell 1 2) positional. Three rows
|
||||
because that is the house rule, not because the backends could differ
|
||||
@ -4964,6 +4991,7 @@ level "1"
|
||||
"programs/dyn-struct.flan";
|
||||
"programs/dyn-global.flan"; "programs/dyn-boundary.flan";
|
||||
"programs/dyn-defer.flan"; "programs/dyn-view.flan";
|
||||
"programs/dyn-held-operand.flan";
|
||||
(* Included for the same reason every dyn program is, though this
|
||||
check cannot see the slot M2 item 4 actually added: [%dx]/[%ax]
|
||||
are the pool-ran-dry fallback for a temporary [root_plan] COUNTED
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user