From d4def945a918ff2053aa7d61fc0b939e0904e5e8 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 18:30:25 +0700 Subject: [PATCH] An array is a value you can write, not a place you have to fill first DISCUSS.org's "need a value-producing array constructor": the author wanted grid filled with 255 as part of its declaration and could not write it. (array n T) produces the zeroed array only, and dotimes is Unit, so it can mutate a place that already exists but cannot be the initialiser expression -- which has to produce the whole value in one go. The grid was declared zeroed and filled in main instead. Two forms, both expressions, both any rank: (array-fill [rows cols] 255) every element that value (array-gen [rows cols] cell) every element (cell i j) Spelled apart rather than one form dispatching on the third element's type, because an array *of* function values is a thing to want and one form would have to decide whether (array-fill [4] f) meant four copies of f or four calls of it. The dimensions are read in Parse, and that is the whole reason they are recognised there: handed through as an ordinary call, [rows cols] is an array literal of two names, and where those names are defconsts it is a perfectly good two-element array of integers -- the wrong reading, and a silent one. Read in Parse they are the same len the [n T] type spelling takes, resolved by the same array_len, with one extra condition of their own: the fill counts in i32 like every index in the language, so a dimension no i32 can reach has no loop that could end. The lowering is a loop over a slot, not an aggregate. Tast.Arr is the node the backends have and both build it element by element from a list as long as the array; a fill of [600 [800 u8]] is half a million elements and there is no list to be had. So these bind the array to a slot, zero it, run one While per dimension writing through Set of a Pindex, and answer with the slot -- While, Set and Pindex, which is the argument check_loop already makes for recur. Nothing new reaches a backend and all three get the form with no edit. The value stays value-like: the slot is the form's own, and the Local at the end copies out the way any array-typed expression does. Row-major is pinned, not incidental: the first dimension is the outermost loop, and a generator that counts observes it. The fill value and the generator value are each bound once before any loop starts, so (array-fill [n] (next-id)) is one call and n copies of its answer. What falls out for the defvar the note was written about, and neither half is a carve-out: (defvar grid [rows [cols u8]] (array-fill [rows cols] 255)) is the spelling that works -- a typed global with a computed initialiser, which is the startup-lifted path with the init-once guard that defvar already had, so the fill runs once and the value survives a re-run like any other computed one. The three-element spelling means what the 2026-09-20 rule says it means: not a type, so a dyn global, and a typed fixed array crosses into dyn only as a view of storage that outlives the view. A freshly built array is a temporary, so it is refused -- by the element rule where the elements are themselves an array, by the lifetime rule where they are one of the three scalars a view carries. Both refusals are the ones any other temporary gets. The type an array-fill builds never goes through resolve, so resolve's own guard is asked again where it is built: a fixed array of function values would be zeroed, and a zeroed function value is a null pointer. --- lib/ast.ml | 26 +++++ lib/check.ml | 175 ++++++++++++++++++++++++++++++++++ lib/load.ml | 32 +++++-- lib/parse.ml | 30 ++++++ test/programs/array-fill.flan | 86 +++++++++++++++++ test/programs/dev-rerun.flan | 13 +++ test/test_acceptance.ml | 23 +++++ test/test_dev.ml | 13 ++- test/test_flan.ml | 112 ++++++++++++++++++++++ 9 files changed, 502 insertions(+), 8 deletions(-) create mode 100644 test/programs/array-fill.flan diff --git a/lib/ast.ml b/lib/ast.ml index 44430a5..3105f57 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -92,6 +92,28 @@ and expr_kind = fails on an unknown name. This is that position's answer, and it says what it does rather than looking like a vector of two things. *) | ArrayOf of texpr (* the whole array type, built by Parse *) + (* (array-fill [r c] v) and (array-gen [r c] f) — a fixed array of any rank + as an *expression*, which is what [ArrayOf] and [dotimes] between them + could not be: [ArrayOf] produces the zeroed value only, and [dotimes] is + Unit and can only mutate a place that already exists. These produce the + whole value, so they compose where a bracket literal does. + + The dimensions are in brackets and are [len]s, not expressions, for the + reason the brackets are read at all: in expression position [[rows cols]] + is an array *literal* of two names, and where those names are defconsts + it would quietly type-check as one. So the form is recognised in [Parse] + and the brackets are read with the same [len] the [n T] type spelling + uses — an integer or a compile-time constant's name, and nothing else. + + Two forms rather than one with a dispatch on the third element's type: an + array *of function values* is a thing one may want, and a single form + would have to decide whether [(array-fill [4] f)] meant four copies of + [f] or four calls of it. Spelled apart, neither reading is ever in doubt. + + [ArrayGen]'s expression is a function value taking one index per + dimension; [ArrayFill]'s is the element value itself, evaluated once. *) + | ArrayFill of len list * expr + | ArrayGen of len list * expr (* These bind names or alter control flow, so none of them can be a call. *) | Fn of string list * expr list (* (fn [x y] ...) — non-escaping *) | Dotimes of string option * string * expr * expr list (* (dotimes :o [i n] ...) *) @@ -368,6 +390,10 @@ let map_children f (e : expr) : expr = | Bare fs -> Bare (List.map (fun (n, v) -> (n, ex v)) fs) | MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs) | Arr es -> Arr (List.map ex es) + (* Not leaves: the fill value and the generator are ordinary + subexpressions. The dimensions are [len]s and hold none. *) + | ArrayFill (ds, v) -> ArrayFill (ds, ex v) + | ArrayGen (ds, f) -> ArrayGen (ds, ex f) | Fn (ps, es) -> Fn (ps, List.map ex es) | Dotimes (l, n, c, es) -> Dotimes (l, n, ex c, List.map ex es) | Defer es -> Defer (List.map ex es) diff --git a/lib/check.ml b/lib/check.ml index 02c6fce..fe3d4f1 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -2573,6 +2573,8 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr = | Ast.ArrayOf t -> let ty = resolve ctx.env t in expect ctx loc ~want (mk loc ty (Tast.Zero ty)) + | Ast.ArrayFill (dims, v) -> check_array_fill ctx ~want loc dims v + | Ast.ArrayGen (dims, f) -> check_array_gen ctx ~want loc dims f | Ast.Match (scrutinee, arms) -> check_match ctx ~tail ?want loc scrutinee arms | Ast.Call (head, args) -> check_call ctx ~want loc head args | Ast.Unwrap (Ast.Usome, v) -> @@ -4107,6 +4109,179 @@ and check_arr ctx ~want loc items = an array literal does not satisfy a slice expectation. *) expect ctx loc ~want (mk loc (Types.Array (n, elem)) (Tast.Arr items)) +(* ── (array-fill [r c] v) and (array-gen [r c] f) ────────────────────── + + DISCUSS.org's "need a value-producing array constructor". [(array 4 T)] is + the zeroed array and [dotimes] is Unit, so between them there was no way to + write "an array of these" as an *expression* — which is what a defvar + initialiser has to be. These are that expression, at any rank. + + **The lowering, and why it is not an aggregate value.** [Tast.Arr] is the + one the backends already have, and both build it element by element from a + list that is as long as the array: an [insertvalue] chain on LLVM, a store + per element on x86. A fill of [[600 800 u8]] is half a million elements and + there is no list to be had. So these lower to a *loop over a slot*: bind the + array to a slot, zero it, run one loop per dimension writing each element + through [Tast.Set] of a [Pindex], and answer with the slot. Nothing new + reaches a backend — it is [While], [Set] and [Pindex], which is the same + argument [check_loop] makes for [recur] — and both backends get the form + with no edit, the js one included. + + The value stays value-like for all that: the slot is the form's own, nothing + else can name it, and the [Local] at the end is copied out exactly as any + other array-typed expression is. In a [defvar] initialiser the copy is the + store into the global that the startup function does; in a [let] it is the + binding's own store. An in-place fill of the *destination*, skipping the + temporary, would be the faster lowering and is deliberately not what this + does — the destination is not a thing an expression may know about, and + [mem2reg] plus the store-to-load forwarding both backends already get is + where that cost goes. + + The slot is zeroed before the loops rather than left [Uninit]. An element + type of [dyn] is the reason it has to be: between the binding and the + store that overwrites it the collector may run, and it would read whatever + the frame happened to hold as a dyn word. The double write is the price and + it is one memset. + + **Row-major, pinned.** The first dimension is the outermost loop, so + [[i][j]] runs with [j] fastest. A generator that prints, or counts, or + appends, observes that order, so it is a promise: this is the order, not + the order the nesting happened to come out in. + + **Evaluated once.** The fill value and the generator *value* are each bound + to a slot before any loop starts, so [(array-fill [n] (next-id))] is one + call and n copies of its answer — not n calls. A generator's *body*, of + course, runs once per element; that is what it is for. *) + +(* The dimensions, resolved by the same rule the [n T] type spelling uses — + [array_len] is literally that rule — with the one extra condition this form + has and the type spelling does not: the fill counts in i32, because every + index in the language is an i32, so a dimension that does not fit one has no + loop that could reach its end. *) +and array_dims ctx loc (dims : Ast.len list) = + List.map + (fun d -> + let n = array_len ctx.env loc d in + if n < 0L || Int64.compare n 2147483647L > 0 then + fail loc + "%Ld is not a dimension a fill can count to: an index in this \ + language is an i32, and so is the loop that writes the elements" + n; + n) + dims + +(* [r c] and an element type make [r [c T]], outermost first. *) +and array_of_dims ns elem = + List.fold_right (fun n t -> Types.Array (n, t)) ns elem + +(* The element type an annotation asks for, peeled one [Array] per dimension. + [None] where the annotation is not an array of at least this rank: the + mismatch is then [expect]'s to report against the whole type, which is the + message that names both shapes rather than one of their leaves. *) +and array_elem_want rank want = + if rank = 0 then want + else + match want with + | Some (Types.Array (_, t)) -> array_elem_want (rank - 1) (Some t) + | _ -> None + +(* The shared lowering. [pre] is bound before any loop runs — that is what + "evaluated once" means — and [element] is handed the index locals, in + dimension order, to build the value one element takes. *) +and array_build ctx loc ns elem ~pre ~element = + let aty = array_of_dims ns elem in + let arr = fresh_slot ctx aty in + let arrv = mk loc aty (Tast.Local arr) in + let islots = List.map (fun _ -> fresh_slot ctx index_ty) ns in + let ivals = List.map (fun s -> mk loc index_ty (Tast.Local s)) islots in + let zero = mk loc index_ty (Tast.Int (0L, Types.I32)) in + let one = mk loc index_ty (Tast.Int (1L, Types.I32)) in + let store = + mk loc Types.Unit (Tast.Set (Tast.Pindex (arrv, ivals), element ivals)) + in + (* One [Let] and one [While] per dimension, the first dimension outermost. + The counter is bound *inside* the enclosing loop's body so that it is + re-zeroed on every pass of it, and the increment is the latch for the + reason [check_dotimes] gives. These loops carry no [break] and no + [continue], which is the condition [tast.ml] puts on a [While] the + checker invents. *) + let rec nest ns islots = + match ns, islots with + | [], [] -> store + | n :: ns, i :: islots -> + let iv = mk loc index_ty (Tast.Local i) in + let limit = mk loc index_ty (Tast.Int (n, Types.I32)) in + let cond = mk loc Types.Bool (Tast.Prim (Tast.Lt, [ iv; limit ])) in + let step = + mk loc Types.Unit + (Tast.Set (Tast.Plocal i, + mk loc index_ty (Tast.Prim (Tast.Add, [ iv; one ])))) + in + let loop = + mk loc Types.Unit (Tast.While (cond, [ nest ns islots ], [ step ])) + in + mk loc Types.Unit (Tast.Let ([ (i, zero) ], [ loop ])) + | _, _ -> fail loc "array fill: one counter per dimension" + in + mk loc aty + (Tast.Let (pre @ [ (arr, mk loc aty (Tast.Zero aty)) ], + [ nest ns islots; arrv ])) + +and check_array_fill ctx ~want loc dims v = + let ns = array_dims ctx loc dims in + let elem_want = array_elem_want (List.length ns) want in + (* The annotation's element type is the [want] the value is checked against, + so a disagreement is reported at the value, in the ordinary + expected/found words, rather than as a whole-array mismatch a line up. *) + let v = check ctx ?want:elem_want v in + let elem = match elem_want with Some t -> t | None -> v.Tast.ty in + (* [resolve] refuses a fixed array of function values, because the elements + this form does not write would be zeroed and a zeroed function value is a + null pointer. The type is built here without going through [resolve], so + the same guard has to be asked here. *) + no_zeroed_fn loc "a fixed array's element" elem; + let vs = fresh_slot ctx elem in + let vv = mk loc elem (Tast.Local vs) in + expect ctx loc ~want + (array_build ctx loc ns elem ~pre:[ (vs, v) ] ~element:(fun _ -> vv)) + +and check_array_gen ctx ~want loc dims f = + let ns = array_dims ctx loc dims in + let rank = List.length ns in + let f = check ctx f in + let plural n = if n = 1 then "" else "s" in + let elem = + match f.Tast.ty with + | Types.Fn (ps, r) -> + let got = List.length ps in + if got <> rank then + fail f.Tast.loc + "this array-gen has %d dimension%s, so its generator is called with \ + %d index%s — and this one takes %d argument%s" + rank (plural rank) rank + (if rank = 1 then "" else "es") got (plural got); + List.iteri + (fun k p -> + if not (Types.equal p index_ty) then + fail f.Tast.loc + "an index is an i32, and this generator's argument %d is %s" + (k + 1) (Types.to_string p)) + ps; + r + | other -> + fail f.Tast.loc + "array-gen's second element is a function value, called once per \ + element with one i32 index per dimension, and this is %s — for one \ + value repeated, write array-fill" + (Types.to_string other) + in + no_zeroed_fn loc "a fixed array's element" elem; + let fs = fresh_slot ctx f.Tast.ty in + let fv = mk loc f.Tast.ty (Tast.Local fs) in + expect ctx loc ~want + (array_build ctx loc ns elem ~pre:[ (fs, f) ] + ~element:(fun idxs -> mk loc elem (Tast.CallPtr (fv, idxs)))) + and check_match ctx ?(tail = false) ?want loc scrutinee arms = let s = check ctx scrutinee in (* What the arms are alternatives over. An [Option] is a two-case data type diff --git a/lib/load.ml b/lib/load.ml index 1d495ec..456e1d7 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -160,6 +160,15 @@ let qualify_name owned alias bound n = (* The type names the package itself declares. Only these are rewritten: a reference to [i32] or to [Ptr] must survive untouched. *) +(* An array length written as a name is an ordinary compile-time constant of + the package, so it is qualified like any other reference to one. Shared by + the [Tarray] below and by [array-fill]/[array-gen], whose dimensions are the + same [len] in expression position. *) +let rename_len owned alias (l : Ast.len) : Ast.len = + match l with + | Ast.Lname n when List.mem n owned -> Ast.Lname (qualify alias n) + | l -> l + let rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr = let k = match t.Ast.t with @@ -169,12 +178,7 @@ let rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr = (* The length too: [rows] in [[rows [cols u32]]] is an ordinary compile-time constant of the package, not part of the type syntax. *) | Ast.Tarray (l, e) -> - let l = - match l with - | Ast.Lname n when List.mem n owned -> Ast.Lname (qualify alias n) - | l -> l - in - Ast.Tarray (l, rename_texpr owned alias e) + Ast.Tarray (rename_len owned alias l, rename_texpr owned alias e) | Ast.Tmap (k, v) -> Ast.Tmap (rename_texpr owned alias k, rename_texpr owned alias v) | Ast.Tapp (n, args) -> @@ -279,6 +283,12 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr = Ast.MapLit (tag, List.map (fun (k, v) -> (go k, go v)) kvs) | Ast.Arr items -> Ast.Arr (gos items) | Ast.ArrayOf t -> Ast.ArrayOf (rename_texpr owned alias t) + (* The dimensions too, for the reason [rename_texpr] gives about the one + inside [Tarray]: a dimension written as a name is an ordinary + compile-time constant of the package and has to be qualified like any + other reference to it. *) + | Ast.ArrayFill (ds, v) -> Ast.ArrayFill (List.map (rename_len owned alias) ds, go v) + | Ast.ArrayGen (ds, v) -> Ast.ArrayGen (List.map (rename_len owned alias) ds, go v) | Ast.Fn (ps, body) -> Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body) | Ast.Dotimes (l, i, n, body) -> @@ -744,6 +754,16 @@ let rec expr_uses acc (e : Ast.expr) = | Ast.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs | Ast.Arr items -> gos items | Ast.ArrayOf t -> texpr_uses acc t + (* A dimension written as a name is a use of that constant, exactly as it is + inside [Tarray]. *) + | Ast.ArrayFill (ds, v) | Ast.ArrayGen (ds, v) -> + List.iter + (fun (l : Ast.len) -> + match l with + | Ast.Lname n -> acc := (n, e.Ast.loc) :: !acc + | Ast.Lint _ -> ()) + ds; + go v | Ast.Fn (_, body) -> gos body | Ast.Dotimes (_, _, n, body) -> go n; gos body | Ast.Defer body -> gos body diff --git a/lib/parse.ml b/lib/parse.ml index e4f450a..3f7e964 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -437,6 +437,36 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = "array is (array COUNT TYPE), as in (array 4 rl/Vector2) — a zeroed \ fixed array of COUNT of them") + (* ── (array-fill [r c] v) and (array-gen [r c] f) ────────────────── + The two value-producing array constructors, and the reason they are + recognised here rather than reaching Check as ordinary calls: the + dimensions are in brackets, and a bracket in expression position is an + array literal. [(array-fill [rows cols] 255)] handed through as a call + would arrive with an [Arr] of two [Var]s as its first argument, which + where [rows] and [cols] are defconsts is a perfectly good two-element + array of integers — the wrong reading, and a silent one. Read here, the + brackets are [len]s: the same integer-or-constant's-name the [n T] type + spelling takes, refused by [len] when they are anything else. *) + | Sym (("array-fill" | "array-gen") as which) -> + let usage () = + fail f + "%s is (%s [n ...] %s) — the dimensions in brackets, each an integer \ + or a compile-time constant's name, and %s" + which which + (if which = "array-fill" then "value" else "f") + (if which = "array-fill" then + "the value every element takes" + else + "a function taking one i32 index per dimension") + in + (match args with + | [ { v = Vec (_ :: _ as ds); _ }; v ] -> + let ds = List.map len ds in + let v = expr v in + mk (if which = "array-fill" then Ast.ArrayFill (ds, v) + else Ast.ArrayGen (ds, v)) + | _ -> usage ()) + | Sym "match" -> (match args with | scrutinee :: rest -> mk (Ast.Match (expr scrutinee, arms f rest)) diff --git a/test/programs/array-fill.flan b/test/programs/array-fill.flan new file mode 100644 index 0000000..f42aa2b --- /dev/null +++ b/test/programs/array-fill.flan @@ -0,0 +1,86 @@ +;;;; (array-fill [r c] v) and (array-gen [r c] f) — a fixed array as a value. +;;;; +;;;; DISCUSS.org's "need a value-producing array constructor": (array n T) is +;;;; the zeroed array and dotimes is Unit, so neither could be the initialiser +;;;; expression of a declaration. These are expressions, so they compose where +;;;; a bracket literal does — including as a defvar's initialiser, which is the +;;;; line the note was written about. +;;;; +;;;; The dimensions are in brackets and are the same compile-time lengths the +;;;; [n T] type spelling takes: an integer or a constant's name. + +(defconst rows 3) +(defconst cols 4) + +;; The line from the note. A typed declaration with a computed initialiser, +;; which is the startup-lifted path a defvar already had. +(defvar grid [rows [cols u8]] (array-fill [rows cols] 255)) + +;; One index per dimension, i32 each, and the return type is the element type. +(defn cell [r i32 c i32] i32 (+ (* r 100) c)) + +(defn one [i i32] i32 (* i i)) + +;; Row-major order is pinned, so a generator that counts observes it: this one +;; is called once per element and answers the call number, so the array it +;; fills is 0 1 2 ... in the order the elements are written. +(defvar ticks i32) + +(defn tick [r i32 c i32] i32 + (set ticks (+ ticks 1)) + (- ticks 1)) + +(defn main [] i32 + ;; Rank 1. + (let [a (array-fill [5] 7)] + (print (at a 0)) (print " ") (print (at a 4)) (println "")) ; 7 7 + + ;; Rank 2, and the element type is the fill value's. + (let [b (array-fill [2 3] (f32 1.5))] + (print (at b 1 2)) (println "")) ; 1.5 + + ;; Rank 3. + (let [c (array-fill [2 2 2] -1)] + (print (at c 0 0 0)) (print " ") (print (at c 1 1 1)) (println "")) ; -1 -1 + + ;; A dimension may be a constant's name, exactly as in [rows [cols u8]]. + (let [d (array-fill [rows cols] 1)] + (print (at d 2 3)) (println "")) ; 1 + + ;; The generator, rank 1: element i is i*i. + (let [g (array-gen [5] one)] + (print (at g 0)) (print " ") (print (at g 3)) (print " ") + (print (at g 4)) (println "")) ; 0 9 16 + + ;; The generator, rank 2. Element [i][j] is i*100+j, which pins the index + ;; arguments: the first is the outer index and the second the inner one, and + ;; a form that passed them the other way round would print 1 and 300 here. + (let [h (array-gen [rows cols] cell)] + (print (at h 0 0)) (print " ") (print (at h 0 1)) (print " ") + (print (at h 1 0)) (print " ") (print (at h 2 3)) (println "")) ; 0 1 100 203 + + ;; Row-major, pinned. [tick] answers the call number, so the element that + ;; was written first holds 0 — and with four columns, [1][0] is the fifth. + (let [t (array-gen [rows cols] tick)] + (print (at t 0 0)) (print " ") (print (at t 0 1)) (print " ") + (print (at t 1 0)) (print " ") (print (at t 2 3)) (println "")) ; 0 1 4 11 + + ;; The defvar from the top: 255 everywhere, read back as an i32 so the + ;; printed value is the number and not a byte. + (print (i32 (at grid 0 0))) (print " ") + (print (i32 (at grid 2 3))) (println "") ; 255 255 + + ;; An array value copies, which is what makes this a value and not a view: + ;; writing through the copy leaves the global alone. + (let [copy grid] + (set (at copy 0 0) (u8 1)) + (print (i32 (at copy 0 0))) (print " ") + (print (i32 (at grid 0 0))) (println "")) ; 1 255 + + ;; A zero dimension is an array with no elements, and the loop that fills it + ;; runs no passes. Nothing to read, so the claim is that it compiles and the + ;; program carries on. + (let [e (array-fill [0] 9)] + (println "empty ok")) + + 0) diff --git a/test/programs/dev-rerun.flan b/test/programs/dev-rerun.flan index f2445b5..f1ff710 100644 --- a/test/programs/dev-rerun.flan +++ b/test/programs/dev-rerun.flan @@ -46,6 +46,14 @@ ;; the line that would count 1, 1, 1, 1. (defvar tally 0) +;; A typed array with a computed initialiser: (array-fill ...) is an +;; expression, so it is lifted into the startup function and guarded there +;; exactly as [counter]'s call is. If it were not — if a fill re-ran on every +;; entry into main — this would count 251, 251, 251, 251 instead of climbing, +;; which is [counter]'s own failure in the one shape that only an array can +;; have. +(defvar grid [2 [3 u8]] (array-fill [2 3] 250)) + ;; The guard flags the fix adds are the compiler's own globals, and they used ;; to be spelled [.init-once.] — a name a program can write, since [.] ;; is an ordinary symbol constituent. This one is exactly the old spelling of @@ -62,10 +70,15 @@ (set .init-once.counter (+ .init-once.counter 1)) (put state :runs (+ (get state :runs) 1)) (set tally (+ tally 1)) + (set (at grid 0 0) (u8 (+ (i32 (at grid 0 0)) 1))) (print "counter ") (print counter) (println "") (print "zeroed ") (print zeroed) (println "") (print "runs ") (print (get state :runs)) (println "") (print "tally ") (print tally) (println "") + (print "grid ") (print (i32 (at grid 0 0))) (println "") + ;; The element the run never writes, which says the fill ran at all: 250 + ;; on every run, and 0 if the initialiser had been skipped outright. + (print "grid-far ") (print (i32 (at grid 1 2))) (println "") (print "base ") (print base) (println "") ;; Long enough for a client to be served, short enough to park well inside ;; any watchdog — dev-macro.flan's clock, for its reason. diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index f882fa7..7a46772 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -384,6 +384,29 @@ let () = (* (array COUNT TYPE). Every line of it is a [let] binding, which is the one position with no type slot and the whole reason the form exists. *) outputs "array constructor" "programs/array-ctor.flan" "4\n0\n7\n9\n4\n"; + (* (array-fill [r c] v) and (array-gen [r c] f), DISCUSS.org's + value-producing array constructor. Three of these lines are load-bearing + beyond "it prints something". "0 1 100 203" pins the index arguments: + element [i][j] is i*100+j, so a generator handed its indices the other + way round prints 1 and 300 there. "0 1 4 11" pins the *order*: the + generator answers the call number, so with four columns the element at + [1][0] being 4 is row-major, written as a promise rather than as + whatever the nesting happened to do. And "1 255" is the value semantics + — a copy written through leaves the global alone. + + Three rows, because the fill is a loop over a slot rather than an + aggregate literal and each backend builds that loop itself: the -O0 row + is the one where nothing has been folded away, and the x86 row is the + dev backend that emits the stores by hand. *) + (let fill_out = + "7 7\n1.5\n-1 -1\n1\n0 9 16\n0 1 100 203\n0 1 4 11\n255 255\n1 255\n\ + empty ok\n" + in + outputs "array-fill and array-gen" "programs/array-fill.flan" fill_out; + outputs ~opt:"-O0" "array-fill and array-gen, -O0" + "programs/array-fill.flan" fill_out; + outputs ~x86:true "array-fill and array-gen, --x86" + "programs/array-fill.flan" fill_out); (* break and continue. The dotimes/continue case is the one that fails by hanging rather than by printing the wrong thing — the step is the loop's latch, and folded onto the body a continue would jump past it — so the diff --git a/test/test_dev.ml b/test/test_dev.ml index e8cfbb2..25b9fc6 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -4742,10 +4742,11 @@ let () = initialiser every *other* time would pass a single re-run. [programs/dev-rerun.flan] prints one line per case per run, and the - whole assertion is the fourth run's five lines: [counter] computed and + whole assertion is the fourth run's lines: [counter] computed and incremented four times, [zeroed] uncomputed and incremented four times, a computed dyn map whose contents were mutated four times, a dyn global - written the three-element way and incremented four times, and a + written the three-element way and incremented four times, a typed array + filled once by a computed (array-fill ...) and written four times, and a [defconst] that no run can have changed. *) let rsock = tmp "rerun.sock" and rout = tmp "rerun.out" in (try Sys.remove rsock with Sys_error _ -> ()); @@ -4802,6 +4803,14 @@ let () = guard because it *is* the same declaration by the time anything downstream sees it. *) "tally 4"; + (* A typed array with a computed initialiser — (array-fill ...), + which is an expression and so is lifted into the startup function + like any other computed one. 250 filled once and incremented four + times; a fill that re-ran would print 251 every run. *) + "grid 254"; + (* And the element no run writes, which separates "the guard held" + from "the initialiser never ran": 250, not 0. *) + "grid-far 250"; (* And a [defconst], which no run can have changed. *) "base 40" ] in diff --git a/test/test_flan.ml b/test/test_flan.ml index f946955..a436d6e 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -697,6 +697,26 @@ let () = ~needle:"expected a type"; parse_rejects "array with a non-constant count" "(defn f [] () (array (+ 1 1) f32))" ~needle:"an array length is an integer or a constant's name"; + (* The dimensions are read in [Parse], and this is the whole reason: without + the bracket being read here it would arrive as an ordinary argument, and + an [Arr] of two names is a perfectly good array literal wherever those + names are constants. So the bracket is required and its contents are + [len]s, refused by the same message [4 f32] gets. *) + parse_rejects "array-fill wants its dimensions in brackets" + "(defn f [] () (array-fill 3 0))" + ~needle:"array-fill is (array-fill [n ...] value)"; + parse_rejects "array-fill wants a fill value" + "(defn f [] () (array-fill [3]))" + ~needle:"array-fill is (array-fill [n ...] value)"; + parse_rejects "array-fill has no rank zero" + "(defn f [] () (array-fill [] 0))" + ~needle:"array-fill is (array-fill [n ...] value)"; + parse_rejects "an array-fill dimension is a length, not an expression" + "(defn f [] () (array-fill [(+ 1 1)] 0))" + ~needle:"an array length is an integer or a constant's name"; + parse_rejects "array-gen says its own name in its usage" + "(defn f [] () (array-gen 3 g))" + ~needle:"array-gen is (array-gen [n ...] f)"; (* ── The corpus parses ─────────────────────────────────────────── *) List.iter @@ -897,6 +917,16 @@ let () = infers "array constructor" "(array 4 f32)" "[4 f32]"; infers "array of a struct" "(array 2 i32)" "[2 i32]"; infers "array of an array" "(array 2 [3 u8])" "[2 [3 u8]]"; + (* (array-fill [r c] v): the same type at any rank, with the element type + taken from the fill value. Unlike [array] above this one is a value and + not a zero, which is what lets it be a defvar's initialiser — see + programs/array-fill.flan for what it puts in the elements. *) + infers "array-fill, rank 1" "(array-fill [5] 7)" "[5 i32]"; + infers "array-fill, rank 2" "(array-fill [2 3] 0.5)" "[2 [3 f64]]"; + infers "array-fill, rank 3" "(array-fill [2 3 4] true)" "[2 [3 [4 bool]]]"; + (* A zero dimension is a legal array with no elements, and the fill loop + runs no passes over it. *) + infers "array-fill of nothing" "(array-fill [0] 1)" "[0 i32]"; infers "bytes of a string" "(bytes \"hi\")" "[u8]"; infers "len is i32" "(len (bytes \"hi\"))" "i32"; infers "slice of a slice" "(slice (bytes \"hi\") 0 1)" "[u8]"; @@ -2157,6 +2187,88 @@ let () = "(defvar score i64 1) (defvar total scor) (defn f [] ())" ~needle:"Nothing named scor is declared as either — did you mean score?"; + (* ── (array-fill ...) and (array-gen ...) as initialisers ────────── + DISCUSS.org's "need a value-producing array constructor" wanted + [(defvar grid (array-fill [rows cols] 255))] — the grid filled as part of + its declaration rather than in a mutation step after it. What falls out of + the rules already settled, and it is not a carve-out either way: + + The four-element spelling is the one that works. It is a typed global with + a computed initialiser, which is the startup-lifted path a defvar already + had, and the value it stores is an ordinary fixed array. + + The three-element spelling does not mean this, and could not. A defvar + whose third element is not a type is a *dyn* global by the 2026-09-20 + rule, and a typed fixed array crosses into dyn only as a view of storage + that outlives the view. A freshly built array is a temporary, so the view + lifetime guard refuses it — and where the elements are an array rather + than one of the three scalar widths a view carries, the element refusal + gets there first. Both refusals are the ones any other temporary gets; + neither was written for this form. *) + defvar_reading "a typed array-fill global is computed, not zeroed" + "(defconst rows 2) (defconst cols 3)\n\ + (defvar grid [rows [cols u8]] (array-fill [rows cols] 255))\n\ + (defn f [] u8 (at grid 0 0))" + "grid" ~ty:"[2 [3 u8]]" ~zeroed:false; + rejects_check "a three-element array-fill defvar is the dyn reading" + "(defvar xs (array-fill [3] (i64 1))) (defn f [] ())" + ~needle:"does not cross into dyn as a view here"; + rejects_check "and its element type is asked about first" + "(defvar grid (array-fill [2 3] 255)) (defn f [] ())" + ~needle:"does not cross into dyn yet"; + (* A defconst is not a second path to it: its value is what the linker + writes into the image, and a fill is a loop. *) + rejects_check "array-fill is not a constant's value" + "(defconst g [2 u8] (array-fill [2] (u8 1))) (defn f [] ())" + ~needle:"a constant's value must be a compile-time constant"; + + (* The element type the annotation asks for is the one the fill value is + checked against, so the disagreement is reported at the value. *) + rejects_check "the annotation and the fill value must agree" + "(defvar g [2 [3 u8]] (array-fill [2 3] (f32 1.0))) (defn f [] ())" + ~needle:"expected u8, found f32"; + rejects_check "the annotation's shape has to be the fill's shape" + "(defvar g [2 u8] (array-fill [3] (u8 1))) (defn f [] ())" + ~needle:"expected [2 u8], found [3 u8]"; + (* A dimension is the same compile-time length [n T] takes, and a local is + not one. The refusal is [array_len]'s own, which is what "the same rule" + means here. *) + rejects_check "a dimension is a compile-time constant" + "(defn f [] i32 (let [n 3 a (array-fill [n] 0)] 0))" + ~needle:"is not a compile-time integer constant"; + (* The one condition this form has that the [n T] type spelling does not: + the fill counts in i32 like every other index, so a dimension no i32 can + reach has no loop that could end. Written as a literal, because a + [defconst] that big is refused as an i32 constant before it is ever a + dimension. *) + rejects_check "a dimension has to fit an i32 index" + "(defn f [] i32 (let [a (array-fill [3000000000] 0)] 0))" + ~needle:"is not a dimension a fill can count to"; + + (* The generator. Its type decides the element type, its arity has to be the + rank, and its arguments are indices. *) + accepts "array-gen takes a named function" + "(defn cell [r i32 c i32] i32 (+ (* r 100) c))\n\ + (defvar grid [2 [3 i32]] (array-gen [2 3] cell))\n\ + (defn f [] i32 (at grid 1 2))"; + rejects_check "array-gen's second element is a function" + "(defn f [] i32 (let [a (array-gen [3] 7)] 0))" + ~needle:"array-gen's second element is a function value"; + rejects_check "the generator takes one argument per dimension" + "(defn g [i i32 j i32] i32 0) (defn f [] i32 (let [a (array-gen [3] g)] 0))" + ~needle:"this array-gen has 1 dimension, so its generator is called with \ + 1 index — and this one takes 2 arguments"; + rejects_check "the generator's arguments are i32 indices" + "(defn g [i i64] i32 0) (defn f [] i32 (let [a (array-gen [3] g)] 0))" + ~needle:"an index is an i32, and this generator's argument 1 is i64"; + (* [resolve] refuses a fixed array of function values — a zeroed one would + be a null pointer — and the type these forms build never goes through + [resolve], so the guard is asked again where the type is built. *) + rejects_check "an array of function values is refused here too" + "(defn h [x i32] i32 x) (defn g [i i32] (Fn [i32] i32) h)\n\ + (defn f [] i32 (let [a (array-gen [2] g)] 0))" + ~needle:"a fixed array's element cannot be (Fn [i32] i32)"; + (* ── Computed global initialisers ────────────────────────────────── The order they run in is the compiler's to choose, so a global written above the one it reads is fine... *)