into refuses to copy elements that own storage as they stand, and names (map clone) where clone takes the element
This commit is contained in:
parent
e2aa0196cf
commit
5b8c4a68fa
106
lib/check.ml
106
lib/check.ml
@ -2429,6 +2429,49 @@ and const_steps ro (ty : Types.t) n =
|
|||||||
let const_copy env (e : Types.t) =
|
let const_copy env (e : Types.t) =
|
||||||
if owning env e || holds_dyn env e then None else Some "(clone v)"
|
if owning env e || holds_dyn env e then None else Some "(clone v)"
|
||||||
|
|
||||||
|
(* Whether (clone x) accepts a value of this type — the same arms the clone
|
||||||
|
builtin takes: a Vec or a Map whose elements own nothing, or a slice whose
|
||||||
|
elements neither own storage nor hold a dyn. *)
|
||||||
|
let clone_accepts env (t : Types.t) =
|
||||||
|
match t with
|
||||||
|
| Types.Vec _ | Types.Map _ -> not (region_only env t)
|
||||||
|
| Types.Slice (_, e) -> not (owning env e || holds_dyn env e)
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
(* The end of clone's refusal for a container of owning elements. Pushing
|
||||||
|
the elements themselves into a second container would copy their headers
|
||||||
|
and share their blocks, so the advice is a copy of each element where
|
||||||
|
clone takes one, and otherwise that there is no copy to make. *)
|
||||||
|
let insert_copies env (t : Types.t) =
|
||||||
|
let elem =
|
||||||
|
match t with
|
||||||
|
| Types.Vec e | Types.Slice (_, e) | Types.Map (_, e) -> Some e
|
||||||
|
| _ -> None
|
||||||
|
in
|
||||||
|
match elem with
|
||||||
|
| Some e when clone_accepts env e ->
|
||||||
|
"Build a second container and push a (clone x) of each element into it"
|
||||||
|
| Some e ->
|
||||||
|
Printf.sprintf
|
||||||
|
"Nothing copies what a %s owns either, so read the elements where they \
|
||||||
|
are"
|
||||||
|
(Types.to_string e)
|
||||||
|
| None -> "Build a second container and insert into it"
|
||||||
|
|
||||||
|
(* A call written back out as source, for a fix that has to repeat what the
|
||||||
|
reader wrote: names, integers and calls of those. Anything else is [None]
|
||||||
|
and the caller says the fix in words. *)
|
||||||
|
let rec spell_form (a : Ast.expr) =
|
||||||
|
match a.Ast.e with
|
||||||
|
| Ast.Var v -> Some v
|
||||||
|
| Ast.Int n -> Some (Int64.to_string n)
|
||||||
|
| Ast.UInt (_, s) -> Some s
|
||||||
|
| Ast.Call (f, args) ->
|
||||||
|
let parts = List.map spell_form (f :: args) in
|
||||||
|
if List.mem None parts then None
|
||||||
|
else Some ("(" ^ String.concat " " (List.filter_map Fun.id parts) ^ ")")
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
(* A store through a read-only view: a [[const T]] or a (Ptr const T). *)
|
(* A store through a read-only view: a [[const T]] or a (Ptr const T). *)
|
||||||
let refuse_const_place env loc (view : Types.t) =
|
let refuse_const_place env loc (view : Types.t) =
|
||||||
match view with
|
match view with
|
||||||
@ -9136,6 +9179,57 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
|||||||
fail loc
|
fail loc
|
||||||
"free takes an owning container — a Vec or a Map — found %s"
|
"free takes an owning container — a Vec or a Map — found %s"
|
||||||
(Types.to_string other))
|
(Types.to_string other))
|
||||||
|
(* Emitted by the prelude's [into] when no (map f) is in the chain, so that
|
||||||
|
every element pushed is a source element as it stands. A push copies an
|
||||||
|
element's header, and for an element that owns storage the copy and the
|
||||||
|
source then share one block: growing an element through either side
|
||||||
|
reallocates it and frees the block the other still points at. That is a
|
||||||
|
use after free the program never wrote, under a name that promised a
|
||||||
|
copy, so it is refused here. A bare (push w (at v 0)) is not: it copies a
|
||||||
|
header in plain sight, the Odin contract every container follows.
|
||||||
|
Arguments are the source, then the destination and the transforms as
|
||||||
|
written — those two only to be spelled back in the fix, never checked. *)
|
||||||
|
| "into-copies-elements" ->
|
||||||
|
(match args with
|
||||||
|
| src :: dst :: transforms ->
|
||||||
|
let s = check ctx src in
|
||||||
|
let elem =
|
||||||
|
match s.Tast.ty with
|
||||||
|
| Types.Vec e | Types.Slice (_, e) | Types.Array (_, e) -> Some e
|
||||||
|
| _ -> None
|
||||||
|
in
|
||||||
|
(match elem with
|
||||||
|
| Some e when owning ctx.env e ->
|
||||||
|
let v = spell_arg "v" src in
|
||||||
|
let et = Types.to_string e in
|
||||||
|
let fix =
|
||||||
|
if clone_accepts ctx.env e then
|
||||||
|
match spell_form dst, List.map spell_form transforms with
|
||||||
|
| Some d, ts when not (List.mem None ts) ->
|
||||||
|
Printf.sprintf
|
||||||
|
"Add (map clone) to the chain, which copies what each \
|
||||||
|
element owns: (into %s)"
|
||||||
|
(String.concat " "
|
||||||
|
((v :: d :: List.filter_map Fun.id ts) @ [ "(map clone)" ]))
|
||||||
|
| _ ->
|
||||||
|
"Add (map clone) to the chain, which copies what each element \
|
||||||
|
owns"
|
||||||
|
else
|
||||||
|
Printf.sprintf
|
||||||
|
"Nothing copies what a %s owns, so no copy of %s can stand on \
|
||||||
|
its own: read the elements where they are, or build each new \
|
||||||
|
element and push that"
|
||||||
|
et v
|
||||||
|
in
|
||||||
|
Loc.failk "check/into-shares-elements" src.Ast.loc
|
||||||
|
"into copies each element of %s as it stands, and an element of \
|
||||||
|
%s is a %s, which owns storage — the copy would share each \
|
||||||
|
element's block with %s, and growing either one frees the block \
|
||||||
|
the other points at. %s"
|
||||||
|
v v et v fix
|
||||||
|
| _ -> ());
|
||||||
|
expect ctx loc ~want (mk loc Types.Unit Tast.Unit)
|
||||||
|
| _ -> expect ctx loc ~want (mk loc Types.Unit Tast.Unit))
|
||||||
(* (clone v) uses the current allocator, (clone v a) names one. A deep,
|
(* (clone v) uses the current allocator, (clone v a) names one. A deep,
|
||||||
independent copy: spec-memory.md's "copying is always explicit". *)
|
independent copy: spec-memory.md's "copying is always explicit". *)
|
||||||
| "clone" ->
|
| "clone" ->
|
||||||
@ -9171,9 +9265,9 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
|||||||
| (Types.Vec _ | Types.Map _) when region_only ctx.env target.Tast.ty ->
|
| (Types.Vec _ | Types.Map _) when region_only ctx.env target.Tast.ty ->
|
||||||
fail loc
|
fail loc
|
||||||
"%s cannot be cloned — its elements own storage, and nothing here \
|
"%s cannot be cloned — its elements own storage, and nothing here \
|
||||||
can walk one to copy what it owns. Build a second container and \
|
can walk one to copy what it owns. %s"
|
||||||
insert into it"
|
|
||||||
(Types.to_string target.Tast.ty)
|
(Types.to_string target.Tast.ty)
|
||||||
|
(insert_copies ctx.env target.Tast.ty)
|
||||||
(* A slice's elements, copied into a block from the allocator and
|
(* A slice's elements, copied into a block from the allocator and
|
||||||
answered as a slice over it — what (bytes s) does for a string's
|
answered as a slice over it — what (bytes s) does for a string's
|
||||||
bytes, and the same lowering. The same refusal as a Vec's, for the
|
bytes, and the same lowering. The same refusal as a Vec's, for the
|
||||||
@ -9181,9 +9275,9 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
|||||||
| Types.Slice (_, elem) when owning ctx.env elem ->
|
| Types.Slice (_, elem) when owning ctx.env elem ->
|
||||||
fail loc
|
fail loc
|
||||||
"%s cannot be cloned — its elements own storage, and nothing here \
|
"%s cannot be cloned — its elements own storage, and nothing here \
|
||||||
can walk one to copy what it owns. Build a container and insert \
|
can walk one to copy what it owns. %s"
|
||||||
into it"
|
|
||||||
(Types.to_string target.Tast.ty)
|
(Types.to_string target.Tast.ty)
|
||||||
|
(insert_copies ctx.env target.Tast.ty)
|
||||||
(* The copy is a block from an allocator, which the collector does not
|
(* The copy is a block from an allocator, which the collector does not
|
||||||
walk, so a dyn in it would be a root nothing marks. *)
|
walk, so a dyn in it would be a root nothing marks. *)
|
||||||
| Types.Slice (_, elem) when holds_dyn ctx.env elem ->
|
| Types.Slice (_, elem) when holds_dyn ctx.env elem ->
|
||||||
@ -11708,6 +11802,10 @@ let builtins : (string * string * string) list =
|
|||||||
allocator's free-all or destroy. Refused for elements that own \
|
allocator's free-all or destroy. Refused for elements that own \
|
||||||
storage: a bytewise copy would alias the original's blocks under a \
|
storage: a bytewise copy would alias the original's blocks under a \
|
||||||
name promising otherwise.");
|
name promising otherwise.");
|
||||||
|
("into-copies-elements", "into-copies-elements [src dst transform...] ()",
|
||||||
|
"What into writes when its chain has no (map f). Refuses a source whose \
|
||||||
|
elements own storage, because pushing them as they stand would share \
|
||||||
|
their blocks. Not meant to be written by hand.");
|
||||||
|
|
||||||
(* (Map K V) *)
|
(* (Map K V) *)
|
||||||
("map-new", "map-new [K? V? Allocator?] (Map K V)",
|
("map-new", "map-new [K? V? Allocator?] (Map K V)",
|
||||||
|
|||||||
@ -2371,6 +2371,15 @@ let source = {flan|
|
|||||||
(form-sym? head "filter") (recur (- k 1) `(when (~f ~x) ~body))
|
(form-sym? head "filter") (recur (- k 1) `(when (~f ~x) ~body))
|
||||||
:else `(into-transform-is-map-or-filter ~t))))))))
|
:else `(into-transform-is-map-or-filter ~t))))))))
|
||||||
|
|
||||||
|
;; Whether any transform in the chain is a (map f).
|
||||||
|
(defn into-maps? [ts [Form]] bool
|
||||||
|
(loop [k 0]
|
||||||
|
(cond
|
||||||
|
(= k (length ts)) false
|
||||||
|
(let [items (form-items (at ts k))]
|
||||||
|
(and (> (length items) 0) (form-sym? (at items 0) "map"))) true
|
||||||
|
:else (recur (+ k 1)))))
|
||||||
|
|
||||||
;; The items of a list form, and the empty slice for anything else — a
|
;; The items of a list form, and the empty slice for anything else — a
|
||||||
;; non-list transform falls into the arity complaint above rather than needing
|
;; non-list transform falls into the arity complaint above rather than needing
|
||||||
;; a case of its own.
|
;; a case of its own.
|
||||||
@ -2423,10 +2432,19 @@ let source = {flan|
|
|||||||
named? (form-is-sym? from)
|
named? (form-is-sym? from)
|
||||||
src (if named? from (gensym))
|
src (if named? from (gensym))
|
||||||
bind (if named? (form-nil) (form-pair src from))
|
bind (if named? (form-nil) (form-pair src from))
|
||||||
|
;; With no (map f) in the chain every element pushed is a source
|
||||||
|
;; element as it stands, which copies only its header: the checker
|
||||||
|
;; refuses that for an element that owns storage. The destination and
|
||||||
|
;; the transforms ride along unevaluated, to be written back in the fix.
|
||||||
|
shares (if (into-maps? (form-rest args 2))
|
||||||
|
(form-nil)
|
||||||
|
(form-cons `(into-copies-elements ~src ~(at args 1) ~@(form-rest args 2))
|
||||||
|
(form-nil)))
|
||||||
dst (gensym)
|
dst (gensym)
|
||||||
x (gensym)
|
x (gensym)
|
||||||
i (gensym)]
|
i (gensym)]
|
||||||
`(let [~dst ~(at args 1) ~@bind]
|
`(let [~dst ~(at args 1) ~@bind]
|
||||||
|
~@shares
|
||||||
(dotimes [~i (length ~src)]
|
(dotimes [~i (length ~src)]
|
||||||
(let [~x (at ~src ~i)]
|
(let [~x (at ~src ~i)]
|
||||||
~(into-wrap (form-rest args 2) dst x)))
|
~(into-wrap (form-rest args 2) dst x)))
|
||||||
|
|||||||
22
test/programs/into-owning.flan
Normal file
22
test/programs/into-owning.flan
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
;;;; into over elements that own storage. Without a (map f) in the chain the
|
||||||
|
;;;; elements are pushed as they stand, which copies their headers and shares
|
||||||
|
;;;; their blocks — so that is refused, and (map clone) is the copy that
|
||||||
|
;;;; compiles. The outer Vecs are in an arena, as a container of owning
|
||||||
|
;;;; elements has to be; the inner ones are on the heap, where growing one
|
||||||
|
;;;; through a shared header would free the block the other still points at.
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
(let [a (arena-new 65536)
|
||||||
|
v (vec-new (Vec i32) a)]
|
||||||
|
(push v (vec-new i32))
|
||||||
|
(push (at v 0) 1)
|
||||||
|
(let [w (into v (vec-new (Vec i32) a) (map clone))]
|
||||||
|
(dotimes [i 100] (push (at w 0) i))
|
||||||
|
(println (at (at v 0) 0)) ; 1
|
||||||
|
(println (length (at v 0))) ; 1
|
||||||
|
(println (length (at w 0))) ; 101
|
||||||
|
(println (at (at w 0) 100)) ; 99
|
||||||
|
(free (at w 0)))
|
||||||
|
(free (at v 0))
|
||||||
|
(arena-destroy a)
|
||||||
|
0))
|
||||||
@ -613,6 +613,11 @@ let () =
|
|||||||
source transformed in two orders, which have to differ. *)
|
source transformed in two orders, which have to differ. *)
|
||||||
outputs "into" "programs/into.flan"
|
outputs "into" "programs/into.flan"
|
||||||
"7 8 9 \n2 4 6 8 10 12 \n4 8 12 \n21\n6 2 4 \n3 1 2 \n3 4 \n2 4 \n1\n";
|
"7 8 9 \n2 4 6 8 10 12 \n4 8 12 \n21\n6 2 4 \n3 1 2 \n3 4 \n2 4 \n1\n";
|
||||||
|
(* The fix into's refusal names for owning elements, (map clone): the
|
||||||
|
copy's inner Vec grows on the heap and the source's is untouched. *)
|
||||||
|
outputs "into with (map clone)" "programs/into-owning.flan" "1\n1\n101\n99\n";
|
||||||
|
outputs ~x86:true "into with (map clone), --x86" "programs/into-owning.flan"
|
||||||
|
"1\n1\n101\n99\n";
|
||||||
(* The prelude's slice algorithms. Every assertion here is over an input a
|
(* The prelude's slice algorithms. Every assertion here is over an input a
|
||||||
wrong implementation fails: unsorted with duplicates, negatives and an
|
wrong implementation fails: unsorted with duplicates, negatives and an
|
||||||
odd length; a reverse-sorted slice; and a sort of a subslice whose
|
odd length; a reverse-sorted slice; and a sort of a subslice whose
|
||||||
|
|||||||
@ -3188,6 +3188,34 @@ let () =
|
|||||||
rejects_check "clone on a slice of owning elements"
|
rejects_check "clone on a slice of owning elements"
|
||||||
"(defn f [v [(Vec i32)]] i32 (length (clone v)))"
|
"(defn f [v [(Vec i32)]] i32 (length (clone v)))"
|
||||||
~needle:"[(Vec i32)] cannot be cloned";
|
~needle:"[(Vec i32)] cannot be cloned";
|
||||||
|
(* into with no (map f) pushes the source's elements as they stand, which
|
||||||
|
for an owning element shares its block — pushing through the copy then
|
||||||
|
frees the source's. The fix it names is programs/into-owning.flan. *)
|
||||||
|
rejects_check "into copying owning elements, refused at the source"
|
||||||
|
"(defn f [a Allocator] i32\n\
|
||||||
|
\ (let [v (vec-new (Vec i32) a)\n\
|
||||||
|
\ w (into v (vec-new (Vec i32) a) (filter nonempty?))] (length w)))\n\
|
||||||
|
(defn nonempty? [x (Vec i32)] bool (> (length x) 0))"
|
||||||
|
~needle:"into copies each element of v as it stands, and an element of v \
|
||||||
|
is a (Vec i32), which owns storage — the copy would share each \
|
||||||
|
element's block with v, and growing either one frees the block \
|
||||||
|
the other points at. Add (map clone) to the chain, which copies \
|
||||||
|
what each element owns: (into v (vec-new (Vec i32) a) (filter \
|
||||||
|
nonempty?) (map clone))";
|
||||||
|
accepts "into with (map clone) over owning elements"
|
||||||
|
"(defn f [a Allocator] i32\n\
|
||||||
|
\ (let [v (vec-new (Vec i32) a)\n\
|
||||||
|
\ w (into v (vec-new (Vec i32) a) (map clone))] (length w)))";
|
||||||
|
accepts "into of plain elements is untouched"
|
||||||
|
"(defn f [v [i32]] () (let [w (into v (vec-new i32))] (free w)))";
|
||||||
|
rejects_check "into copying elements clone cannot copy, explained"
|
||||||
|
"(defstruct B [xs (Vec i32)])\n\
|
||||||
|
(defn f [v [B] a Allocator] i32 (let [w (into v (vec-new B a))] (length w)))"
|
||||||
|
~needle:"Nothing copies what a B owns, so no copy of v can stand on its \
|
||||||
|
own";
|
||||||
|
rejects_check "clone's refusal names a clone of each element"
|
||||||
|
"(defn f [v [(Vec i32)]] i32 (length (clone v)))"
|
||||||
|
~needle:"push a (clone x) of each element into it";
|
||||||
accepts "clone on a slice, with and without an allocator"
|
accepts "clone on a slice, with and without an allocator"
|
||||||
"(defn f [v [f64] a Allocator] i32 (+ (length (clone v)) (length (clone v a))))";
|
"(defn f [v [f64] a Allocator] i32 (+ (length (clone v)) (length (clone v a))))";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user