(clone xs) copies any slice's elements into a block from the context allocator or a named one, through the lowering (bytes s) uses
This commit is contained in:
parent
c6f0dd5450
commit
cb83014715
6
TODO.org
6
TODO.org
@ -245,12 +245,6 @@ arrives at the push. A =Vec= a call returned is accepted where an array a call
|
||||
returned is refused: one dangles and one only leaks, and leaking is defined
|
||||
behaviour here.
|
||||
|
||||
** NEXT (clone slice) as the general spelling of what (bytes s) does
|
||||
Decided 2026-09-25: =(clone xs)= copies any slice into the context allocator, sharing =flan_bytes_dup='s lowering with =bytes=. The allocator's region answers who frees it.
|
||||
Not built because of the who-frees question =bytes= answers by leaning on
|
||||
free-all and arena-destroy. =flan_bytes_dup= is already the lowering, so if slices
|
||||
grow a =clone= the two should share it.
|
||||
|
||||
** DONE The count is length, and len is a name a program can have
|
||||
CLOSED: [2026-09-21]
|
||||
One arm in the checker and one row in the builtin table. A call to an undefined
|
||||
|
||||
112
lib/check.ml
112
lib/check.ml
@ -6880,6 +6880,51 @@ and allocator_arg ctx loc = function
|
||||
| [ a ] -> check ctx ~want:Types.Alloc a
|
||||
| _ -> fail loc "at most one allocator may be named here"
|
||||
|
||||
(* A copy of [src]'s elements — a string's bytes or a slice's elements — into
|
||||
a block from [a], answered as a slice over it: (bytes s), (clone xs) and the
|
||||
number conversions. The lowering mirrors [vec-new]: a hidden (Vec T) temp
|
||||
holds the block so the allocation registry can read its extent, the attempt
|
||||
sits under [alloc_guard] so a failure signals StorageExhausted with retry,
|
||||
and the answer is the [slice] of the whole of it. The slice carries no
|
||||
allocator, so nothing can [free] the block through it — it lives until its
|
||||
allocator's free-all or destroy.
|
||||
|
||||
The source is bound before the guard's loop, so a retry re-attempts the
|
||||
same copy rather than re-evaluating the expression that produced it. Same
|
||||
rule as [push]'s element. No [region_check]: that guard compares a Vec
|
||||
header being *stored* against the region it lands in, and the header here
|
||||
is a temp nothing stores. *)
|
||||
and dup_elems ctx loc elem (src : Tast.expr) (a : Tast.expr) =
|
||||
let sty = src.Tast.ty in
|
||||
let sv = fresh_slot ctx sty in
|
||||
let v = fresh_slot ctx (Types.Vec elem) in
|
||||
let out = fresh_slot ctx (Types.Slice elem) in
|
||||
let attempt =
|
||||
rt loc (Types.Int Types.I8) "flan_bytes_dup"
|
||||
[ mk loc (Types.Vec elem) (Tast.Local v); a;
|
||||
mk loc sty (Tast.Local sv); size_of loc elem; align_of loc elem;
|
||||
here loc ]
|
||||
in
|
||||
let fill =
|
||||
rt loc Types.Unit "flan_vec_as_slice"
|
||||
[ mk loc (Types.Vec elem) (Tast.Local v);
|
||||
addr_of loc (mk loc (Types.Slice elem) (Tast.Local out));
|
||||
mk loc index_ty (Tast.Int (0L, Types.I32));
|
||||
mk loc index_ty (Tast.Int (-1L, Types.I32));
|
||||
size_of loc elem; here loc ]
|
||||
in
|
||||
mk loc (Types.Slice elem)
|
||||
(Tast.Let
|
||||
([ (sv, src);
|
||||
(v, mk loc (Types.Vec elem) (Tast.Zero (Types.Vec elem)));
|
||||
(out, mk loc (Types.Slice elem) (Tast.Zero (Types.Slice elem))) ],
|
||||
[ with_note loc (alloc_guard ctx loc attempt)
|
||||
(reg_note loc "flan_dev_reg_note_vec"
|
||||
(mk loc (Types.Vec elem) (Tast.Local v))
|
||||
[ size_of loc elem ] elem);
|
||||
fill;
|
||||
mk loc (Types.Slice elem) (Tast.Local out) ]))
|
||||
|
||||
(* The address of an element, bounds-checked, with the allocator's epoch
|
||||
checked first. Both the value form [(at v i)] and the place form
|
||||
[(set (at v i) x)] come through here, so they cannot drift apart — which is
|
||||
@ -7764,6 +7809,18 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
can walk one to copy what it owns. Build a second container and \
|
||||
insert into it"
|
||||
(Types.to_string target.Tast.ty)
|
||||
(* 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
|
||||
bytes, and the same lowering. The same refusal as a Vec's, for the
|
||||
same reason: a copy of owning elements is a copy of their headers. *)
|
||||
| Types.Slice elem when owning ctx.env elem ->
|
||||
fail loc
|
||||
"%s cannot be cloned — its elements own storage, and nothing here \
|
||||
can walk one to copy what it owns. Build a container and insert \
|
||||
into it"
|
||||
(Types.to_string target.Tast.ty)
|
||||
| Types.Slice elem ->
|
||||
expect ctx loc ~want (dup_elems ctx loc elem target a)
|
||||
(* A map's clone reinserts rather than copying the block, because the
|
||||
seed is derived from the block's address — see flan_rt.c. That is
|
||||
the runtime's business; from here it is one more allocating call
|
||||
@ -8641,55 +8698,14 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
||||
spec-memory.md's frozen rule over every allocating operation. It used to
|
||||
be the zero-cost reinterpret above, and the author's in-place sort over
|
||||
(bytes "INSERTIONSORT") wrote into the string constant; "I would expect
|
||||
bytes to copy" is the ruling this implements.
|
||||
|
||||
The lowering mirrors [vec-new]: a hidden (Vec u8) temp holds the block so
|
||||
the allocation registry can read its extent, the attempt sits under
|
||||
[alloc_guard] so a failure signals StorageExhausted with retry, and the
|
||||
answer is the [slice] of the whole of it. The slice carries no
|
||||
allocator, so nothing can [free] this block through it — it lives until
|
||||
its allocator's free-all or destroy, which is the story every borrowed
|
||||
view already has and is written down in BUILT.md's surface table.
|
||||
|
||||
No [region_check]: that guard compares a Vec header being *stored* against
|
||||
the region it lands in, and the header here is a temp nothing stores. *)
|
||||
bytes to copy" is the ruling this implements. The lowering is
|
||||
[dup_elems], which (clone xs) shares for any slice. *)
|
||||
| "bytes" ->
|
||||
(match args with
|
||||
| s :: rest when List.length rest <= 1 ->
|
||||
let u8 = Types.Int Types.U8 in
|
||||
let s = check ctx ~want:Types.String s in
|
||||
let a = allocator_arg ctx loc rest in
|
||||
(* The string is bound before the guard's loop, so a retry re-attempts
|
||||
the same copy rather than re-evaluating the expression that produced
|
||||
the string. Same rule as [push]'s element. *)
|
||||
let sv = fresh_slot ctx Types.String in
|
||||
let v = fresh_slot ctx (Types.Vec u8) in
|
||||
let out = fresh_slot ctx (Types.Slice u8) in
|
||||
let attempt =
|
||||
rt loc (Types.Int Types.I8) "flan_bytes_dup"
|
||||
[ mk loc (Types.Vec u8) (Tast.Local v); a;
|
||||
mk loc Types.String (Tast.Local sv); here loc ]
|
||||
in
|
||||
let fill =
|
||||
rt loc Types.Unit "flan_vec_as_slice"
|
||||
[ mk loc (Types.Vec u8) (Tast.Local v);
|
||||
addr_of loc (mk loc (Types.Slice u8) (Tast.Local out));
|
||||
mk loc index_ty (Tast.Int (0L, Types.I32));
|
||||
mk loc index_ty (Tast.Int (-1L, Types.I32));
|
||||
size_of loc u8; here loc ]
|
||||
in
|
||||
expect ctx loc ~want
|
||||
(mk loc (Types.Slice u8)
|
||||
(Tast.Let
|
||||
([ (sv, s);
|
||||
(v, mk loc (Types.Vec u8) (Tast.Zero (Types.Vec u8)));
|
||||
(out, mk loc (Types.Slice u8) (Tast.Zero (Types.Slice u8))) ],
|
||||
[ with_note loc (alloc_guard ctx loc attempt)
|
||||
(reg_note loc "flan_dev_reg_note_vec"
|
||||
(mk loc (Types.Vec u8) (Tast.Local v))
|
||||
[ size_of loc u8 ] u8);
|
||||
fill;
|
||||
mk loc (Types.Slice u8) (Tast.Local out) ])))
|
||||
expect ctx loc ~want (dup_elems ctx loc (Types.Int Types.U8) s a)
|
||||
| _ -> fail loc "bytes is (bytes s) or (bytes s allocator)")
|
||||
|
||||
(* (string b): a [u8] seen as a string. The mirror of (bytes-view s),
|
||||
@ -10201,10 +10217,12 @@ let builtins : (string * string * string) list =
|
||||
"Releases the container's block. It does not recurse into elements that \
|
||||
own storage — such a container is refused here, and releasing its \
|
||||
region with free-all is the answer.");
|
||||
("clone", "clone [(Vec T)|(Map K V) Allocator?] (Vec T)|(Map K V)",
|
||||
("clone", "clone [(Vec T)|(Map K V)|[T] Allocator?] (Vec T)|(Map K V)|[T]",
|
||||
"A deep, independent copy, from the current allocator or one named. \
|
||||
Refused for a container whose elements own storage: a bytewise copy \
|
||||
would alias the original's blocks under a name promising otherwise.");
|
||||
A slice's copy is a slice over a new block, which lives until its \
|
||||
allocator's free-all or destroy. Refused for elements that own \
|
||||
storage: a bytewise copy would alias the original's blocks under a \
|
||||
name promising otherwise.");
|
||||
|
||||
(* (Map K V) *)
|
||||
("map-new", "map-new [K? V? Allocator?] (Map K V)",
|
||||
|
||||
@ -4442,7 +4442,7 @@ declare i8 @flan_vec_init(ptr, ptr, i64, i64, i64, ptr, i64)
|
||||
declare i8 @flan_vec_reserve(ptr, i64, i64, i64, ptr, i64)
|
||||
declare i8 @flan_vec_push(ptr, ptr, i64, i64, ptr, i64)
|
||||
declare i8 @flan_vec_clone(ptr, ptr, ptr, i64, i64, ptr, i64)
|
||||
declare i8 @flan_bytes_dup(ptr, ptr, ptr, i64, ptr, i64)
|
||||
declare i8 @flan_bytes_dup(ptr, ptr, ptr, i64, i64, i64, ptr, i64)
|
||||
declare i64 @flan_vec_len(ptr, ptr, i64)
|
||||
; These two take the transfer channel as well, because a Vec's bounds check is
|
||||
; inside the runtime rather than emitted here and (at v i) has to signal the
|
||||
|
||||
@ -1930,15 +1930,16 @@ void flan_vec_free(flan_vec *v, int64_t size, int64_t align,
|
||||
v->epoch = 0;
|
||||
}
|
||||
|
||||
/* (bytes s): a writable copy of a string's bytes, into a block the named
|
||||
/* (bytes s) and (clone xs): a copy of n elements, into a block the named
|
||||
* allocator owns. The header the compiler hands in is a hidden temp — the
|
||||
* caller's answer is a slice over the block — but it is a real Vec, so the
|
||||
* registry note, the epoch word and free-all's reclaim all work on it the way
|
||||
* they work on any Vec of u8. Element size and align are 1 by construction. */
|
||||
* they work on any Vec. (bytes s) passes a size and align of 1. */
|
||||
int8_t flan_bytes_dup(flan_vec *v, flan_allocator *a, const uint8_t *p,
|
||||
int64_t n, const uint8_t *loc, int64_t loclen) {
|
||||
if (!flan_vec_init(v, a, n, 1, 1, loc, loclen)) return 0;
|
||||
if (n > 0) memcpy(v->ptr, p, (size_t)n);
|
||||
int64_t n, int64_t size, int64_t align,
|
||||
const uint8_t *loc, int64_t loclen) {
|
||||
if (!flan_vec_init(v, a, n, size, align, loc, loclen)) return 0;
|
||||
if (n > 0) memcpy(v->ptr, p, (size_t)(n * size));
|
||||
v->len = n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
30
test/programs/clone-slice.flan
Normal file
30
test/programs/clone-slice.flan
Normal file
@ -0,0 +1,30 @@
|
||||
;;;; (clone xs) on a slice: the elements copied into a block from the context
|
||||
;;;; allocator, or from one named, and answered as a slice over it. Writing
|
||||
;;;; through the copy leaves the original alone, and the other way round.
|
||||
(defstruct P [x i32 y f64])
|
||||
|
||||
(defn show [xs [i32]] ()
|
||||
(dotimes [i (length xs)]
|
||||
(print (at xs i))
|
||||
(print " "))
|
||||
(println ""))
|
||||
|
||||
(defn main [] i32
|
||||
(let [arr [1 2 3 4 5]
|
||||
c (clone (slice arr 1 4))]
|
||||
(set (at c 0) 20)
|
||||
(set (at arr 2) 30)
|
||||
(show c)
|
||||
(show (slice arr)))
|
||||
;; From a Vec's view, into a named arena; wider elements than a byte.
|
||||
(let [a (arena-new 1024)
|
||||
v (vec-new P)]
|
||||
(push v (P {.x 1 .y 1.5}))
|
||||
(push v (P {.x 2 .y 2.5}))
|
||||
(let [c (clone (slice v) a)]
|
||||
(set (.x (at v 0)) 9)
|
||||
(println (.x (at c 0)) (.y (at c 1)) (length c))))
|
||||
;; An empty slice clones to an empty slice.
|
||||
(let [e (clone (slice (bytes-view "abc") 1 1))]
|
||||
(println (length e)))
|
||||
0)
|
||||
@ -888,6 +888,14 @@ let () =
|
||||
"programs/shim-literal.flan" shim_literal_out;
|
||||
outputs ~x86:true "a literal crosses to C uncopied, --x86"
|
||||
"programs/shim-literal.flan" shim_literal_out;
|
||||
(* (clone xs) on a slice: an independent copy, from the context or a named
|
||||
allocator, of elements a byte wide and wider. *)
|
||||
let clone_slice_out = "20 3 4 \n1 2 30 4 5 \n1 2.5 2\n0\n" in
|
||||
outputs "clone of a slice" "programs/clone-slice.flan" clone_slice_out;
|
||||
outputs ~opt:"-O0" "clone of a slice, -O0" "programs/clone-slice.flan"
|
||||
clone_slice_out;
|
||||
outputs ~x86:true "clone of a slice, --x86" "programs/clone-slice.flan"
|
||||
clone_slice_out;
|
||||
(* A slice taken before a push moved its Vec reads 0xDEADBEEF in a dev
|
||||
build and the old value in a release one. *)
|
||||
let poison = "programs/stale-slice-poison.flan" in
|
||||
|
||||
@ -2795,6 +2795,11 @@ let () =
|
||||
"(defdata V [Nil (L [xs (Vec V)])])\n\
|
||||
(defn f [v (Vec V)] () (let [c (clone v)] (free c)))"
|
||||
~needle:"cannot be cloned";
|
||||
rejects_check "clone on a slice of owning elements"
|
||||
"(defn f [v [(Vec i32)]] i32 (length (clone v)))"
|
||||
~needle:"[(Vec i32)] cannot be cloned";
|
||||
accepts "clone on a slice, with and without an allocator"
|
||||
"(defn f [v [f64] a Allocator] i32 (+ (length (clone v)) (length (clone v a))))";
|
||||
|
||||
(* ── A move-only global ─────────────────────────────────────────────
|
||||
Legal, started zeroed, and since the repeal of the flow analysis it is
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user