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.
This commit is contained in:
Joseph Ferano 2026-09-20 18:30:25 +07:00
parent e807986622
commit d4def945a9
9 changed files with 502 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.<name>] — 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.

View File

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

View File

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

View File

@ -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... *)