diff --git a/TODO.org b/TODO.org index 0395fa88..1ffa5367 100644 --- a/TODO.org +++ b/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 diff --git a/lib/check.ml b/lib/check.ml index 569f9221..57eca899 100644 --- a/lib/check.ml +++ b/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)", diff --git a/lib/emit.ml b/lib/emit.ml index 18f1a15f..3aa80f0d 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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 diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 35e50e56..a55c89a6 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -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; } diff --git a/test/programs/clone-slice.flan b/test/programs/clone-slice.flan new file mode 100644 index 00000000..a7aa0b9e --- /dev/null +++ b/test/programs/clone-slice.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 0d0fdd1f..0c48ab7d 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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 diff --git a/test/test_flan.ml b/test/test_flan.ml index d7857883..f9bf1b1f 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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