(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:
Joseph Ferano 2026-09-25 11:40:42 +07:00
parent c6f0dd5450
commit cb83014715
7 changed files with 115 additions and 59 deletions

View File

@ -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

View File

@ -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)",

View File

@ -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

View File

@ -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;
}

View 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)

View File

@ -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

View File

@ -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