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 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. *) it does rather than looking like a vector of two things. *)
| ArrayOf of texpr (* the whole array type, built by Parse *) | 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. *) (* 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 *) | Fn of string list * expr list (* (fn [x y] ...) — non-escaping *)
| Dotimes of string option * string * expr * expr list (* (dotimes :o [i n] ...) *) | 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) | 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) | MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs)
| Arr es -> Arr (List.map ex es) | 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) | Fn (ps, es) -> Fn (ps, List.map ex es)
| Dotimes (l, n, c, es) -> Dotimes (l, n, ex c, 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) | 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 -> | Ast.ArrayOf t ->
let ty = resolve ctx.env t in let ty = resolve ctx.env t in
expect ctx loc ~want (mk loc ty (Tast.Zero ty)) 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.Match (scrutinee, arms) -> check_match ctx ~tail ?want loc scrutinee arms
| Ast.Call (head, args) -> check_call ctx ~want loc head args | Ast.Call (head, args) -> check_call ctx ~want loc head args
| Ast.Unwrap (Ast.Usome, v) -> | 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. *) an array literal does not satisfy a slice expectation. *)
expect ctx loc ~want (mk loc (Types.Array (n, elem)) (Tast.Arr items)) 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 = and check_match ctx ?(tail = false) ?want loc scrutinee arms =
let s = check ctx scrutinee in let s = check ctx scrutinee in
(* What the arms are alternatives over. An [Option] is a two-case data type (* 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 (* The type names the package itself declares. Only these are rewritten: a
reference to [i32] or to [Ptr] must survive untouched. *) 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 rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr =
let k = let k =
match t.Ast.t with 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 (* The length too: [rows] in [[rows [cols u32]]] is an ordinary
compile-time constant of the package, not part of the type syntax. *) compile-time constant of the package, not part of the type syntax. *)
| Ast.Tarray (l, e) -> | Ast.Tarray (l, e) ->
let l = Ast.Tarray (rename_len owned alias l, rename_texpr owned alias e)
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.Tmap (k, v) -> | Ast.Tmap (k, v) ->
Ast.Tmap (rename_texpr owned alias k, rename_texpr owned alias v) Ast.Tmap (rename_texpr owned alias k, rename_texpr owned alias v)
| Ast.Tapp (n, args) -> | 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.MapLit (tag, List.map (fun (k, v) -> (go k, go v)) kvs)
| Ast.Arr items -> Ast.Arr (gos items) | Ast.Arr items -> Ast.Arr (gos items)
| Ast.ArrayOf t -> Ast.ArrayOf (rename_texpr owned alias t) | 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, body) ->
Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body) Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body)
| Ast.Dotimes (l, i, n, 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.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs
| Ast.Arr items -> gos items | Ast.Arr items -> gos items
| Ast.ArrayOf t -> texpr_uses acc t | 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.Fn (_, body) -> gos body
| Ast.Dotimes (_, _, n, body) -> go n; gos body | Ast.Dotimes (_, _, n, body) -> go n; gos body
| Ast.Defer body -> 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 \ "array is (array COUNT TYPE), as in (array 4 rl/Vector2) — a zeroed \
fixed array of COUNT of them") 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" -> | Sym "match" ->
(match args with (match args with
| scrutinee :: rest -> mk (Ast.Match (expr scrutinee, arms f rest)) | 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. ;; the line that would count 1, 1, 1, 1.
(defvar tally 0) (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 ;; 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 [.] ;; 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 ;; 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)) (set .init-once.counter (+ .init-once.counter 1))
(put state :runs (+ (get state :runs) 1)) (put state :runs (+ (get state :runs) 1))
(set tally (+ tally 1)) (set tally (+ tally 1))
(set (at grid 0 0) (u8 (+ (i32 (at grid 0 0)) 1)))
(print "counter ") (print counter) (println "") (print "counter ") (print counter) (println "")
(print "zeroed ") (print zeroed) (println "") (print "zeroed ") (print zeroed) (println "")
(print "runs ") (print (get state :runs)) (println "") (print "runs ") (print (get state :runs)) (println "")
(print "tally ") (print tally) (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 "") (print "base ") (print base) (println "")
;; Long enough for a client to be served, short enough to park well inside ;; Long enough for a client to be served, short enough to park well inside
;; any watchdog — dev-macro.flan's clock, for its reason. ;; 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 (* (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. *) 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"; 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 (* 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 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 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. initialiser every *other* time would pass a single re-run.
[programs/dev-rerun.flan] prints one line per case per run, and the [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, incremented four times, [zeroed] uncomputed and incremented four times,
a computed dyn map whose contents were mutated four times, a dyn global 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. *) [defconst] that no run can have changed. *)
let rsock = tmp "rerun.sock" and rout = tmp "rerun.out" in let rsock = tmp "rerun.sock" and rout = tmp "rerun.out" in
(try Sys.remove rsock with Sys_error _ -> ()); (try Sys.remove rsock with Sys_error _ -> ());
@ -4802,6 +4803,14 @@ let () =
guard because it *is* the same declaration by the time anything guard because it *is* the same declaration by the time anything
downstream sees it. *) downstream sees it. *)
"tally 4"; "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. *) (* And a [defconst], which no run can have changed. *)
"base 40" ] "base 40" ]
in in

View File

@ -697,6 +697,26 @@ let () =
~needle:"expected a type"; ~needle:"expected a type";
parse_rejects "array with a non-constant count" "(defn f [] () (array (+ 1 1) f32))" 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"; ~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 ─────────────────────────────────────────── *) (* ── The corpus parses ─────────────────────────────────────────── *)
List.iter List.iter
@ -897,6 +917,16 @@ let () =
infers "array constructor" "(array 4 f32)" "[4 f32]"; infers "array constructor" "(array 4 f32)" "[4 f32]";
infers "array of a struct" "(array 2 i32)" "[2 i32]"; infers "array of a struct" "(array 2 i32)" "[2 i32]";
infers "array of an array" "(array 2 [3 u8])" "[2 [3 u8]]"; 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 "bytes of a string" "(bytes \"hi\")" "[u8]";
infers "len is i32" "(len (bytes \"hi\"))" "i32"; infers "len is i32" "(len (bytes \"hi\"))" "i32";
infers "slice of a slice" "(slice (bytes \"hi\") 0 1)" "[u8]"; 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 [] ())" "(defvar score i64 1) (defvar total scor) (defn f [] ())"
~needle:"Nothing named scor is declared as either — did you mean score?"; ~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 ────────────────────────────────── (* ── Computed global initialisers ──────────────────────────────────
The order they run in is the compiler's to choose, so a global written The order they run in is the compiler's to choose, so a global written
above the one it reads is fine... *) above the one it reads is fine... *)