Merge branch 'lane-array-fill' into dev-loop

# Conflicts:
#	FIX.org
This commit is contained in:
Joseph Ferano 2026-09-20 21:39:21 +07:00
commit 86091541d4
10 changed files with 676 additions and 37 deletions

46
FIX.org
View File

@ -3824,3 +3824,49 @@ test-flan.el: output reaches the daemon buffer with no REPL open, output
lands above the value at the REPL and is mirrored, the rejection summary and
its full message in the diagnostics list, both clears, and the two-section
layout (errors above, memory below, replace-whole, clear takes both).
* (array-fill [n ...] v) and (array-gen [n ...] f), 2026-09-20
DISCUSS.org asked for a value-producing array constructor: =(array n T)= is
the zeroed array and =dotimes= is Unit, so "an array of these" had no spelling
that could stand where an expression must — a defvar's initialiser being the
line the note was written about. These two are that expression, at any rank.
The dimensions sit in brackets and are the same compile-time lengths the
=[n T]= type spelling takes — an integer literal or a defconst's name, one
rule in one place (=array_len=) — with one extra condition the type spelling
does not need: a dimension has to fit an i32, because every index in the
language is an i32 and so is the loop that writes the elements.
The generator is a function value called once per element with one i32 index
per dimension, first dimension's index first, and its return type is the
element type. Row-major order is pinned as a promise, and the fill value and
the generator *value* are each evaluated once, before any loop runs — =(array-fill
[n] (next-id))= is one call and n copies of its answer.
The lowering is want-driven and reaches no backend: bind a slot, =Zero= it,
one =While= per dimension writing each element through =Set= of a =Pindex=,
answer the slot. Those are nodes both backends already had, so LLVM, x86 and
the js one all get the form with no edit. The annotation's element type is
threaded down as the want, so a fill value that disagrees with =[rows [cols
u8]]= is reported at the value in expected/found words, not as a whole-array
mismatch.
Composition is the ordinary kind: =(array-fill [2] (array-fill [3] 7))= is an
array whose fill value is an array, and it works because the inner form is
just an expression in the value slot. What does *not* exist is a nested
bracket syntax — =[2 [3]]= as a dimension list means nothing; ranks are
spelled flat, =(array-fill [2 3] 7)=.
** The inline fn, and the want it was owed
=(array-gen [3 4] (fn [i j] ...))= — the canonical form — was refused at
first: an fn takes its types from its position, this position carried no
=(Fn ...)= want, and =check_fn= answered "nothing here says what this fn's
parameters are". But the form *does* say: one i32 per dimension is the rank's
own promise. =check_array_gen= now hands an inline fn its parameter types
directly, with the annotated element type as the return want where the
annotation reaches that deep, and with the return left to the body where it
does not — so a bare =(array-gen [3] (fn [i] (* i i)))= infers =[3 i32]= the
same way a fill value infers its element. A body that disagrees with an
annotated element type is reported at the generator's answer — expected u8,
found f64, caret on the offending expression — per element, not per array.
Named defn generators check exactly as before, arity and index types in
array-gen's own words.

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

@ -3020,6 +3020,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) ->
@ -3410,10 +3412,19 @@ and block ctx ?want ?(defer_ok = false) loc body =
checkable exactly where something says what is wanted. An argument position
does, because [named_call] threads the callee's parameter type into each
argument; a bare [(let [f (fn [x] x)])] does not, and is refused saying so. *)
and check_fn ctx ~want loc (params : string list) body =
let pts, ret =
and check_fn ctx ~want ?gen loc (params : string list) body =
(* [gen] is (array-gen ...)'s way in for an inline fn: the *form* knows the
parameter types one i32 index per dimension without there being a
[(Fn ...)] want to say so, and the return is the annotated element type,
or [None] to take the body's own. Everything else threads [want]. The
caller has already checked the arity, in its own words. *)
let pts, ret0 =
match gen with
| Some (pts, r) -> pts, r
| None ->
match want with
| Some (Types.Fn (ps, r)) when List.length ps = List.length params -> ps, r
| Some (Types.Fn (ps, r)) when List.length ps = List.length params ->
ps, Some r
| Some (Types.Fn (ps, r)) ->
fail loc
"this fn has %d parameter%s and %s was wanted here"
@ -3426,22 +3437,28 @@ and check_fn ctx ~want loc (params : string list) body =
fail loc
"nothing here says what this fn's parameters are — an fn takes its \
types from the position it is written in, so it goes in an argument \
whose parameter is a (Fn [T ...] R), and a name already written as a \
defn goes anywhere"
whose parameter is a (Fn [T ...] R), and a name already written as \
a defn goes anywhere"
in
(* Its own frame and its own empty scope, with [outer] kept only so that a
reference to the enclosing function's locals is refused for the reason it
is really refused for. *)
is really refused for. When the return is being inferred the context gets
Unit provisionally a [return] inside such a body would check against
it, which is a rough edge left rough on purpose: the body of a generator
is an expression, and no machinery is built for the form nobody writes. *)
let fctx =
{ (invented_ctx ctx.env ret) with
{ (invented_ctx ctx.env (Option.value ret0 ~default:Types.Unit)) with
outer = ctx.scope; outer_what = Some "an fn"; owner = ctx.owner }
in
List.iter2
(fun n t -> ignore (bind fctx n t ~assignable:false)) params pts;
let fbody = map_lr (fun e -> check fctx e) body in
(* The same rule an ordinary defn's body follows: the last form is the
answer, and it has to be the declared return type. *)
let fbody =
answer, and it has to be the declared return type or, when nothing
declared one ([ret0] is [None]), the last form's own type *is* the
return, which is what lets a bare generator's element type be read off
its body. *)
let fbody, ret =
match List.rev fbody with
(* An fn with no body answers unit, the same as a defn whose declared
return type is () and whose body is empty. Unlike a defn it declares no
@ -3449,14 +3466,19 @@ and check_fn ctx ~want loc (params : string list) body =
the *position* names one, and a position wanting a value is the case
[Check] has to refuse. Without this the empty body would simply fall
through and the call would read a return value nothing ever wrote. *)
| [] when not (Types.equal ret Types.Unit) ->
| [] ->
(match ret0 with
| Some r when not (Types.equal r Types.Unit) ->
fail loc
"an fn with no body answers (), and this one is in a position that \
wants %s write the value it should answer"
(Types.to_string ret)
| [] -> fbody
"an fn with no body answers (), and this one is in a position \
that wants %s write the value it should answer"
(Types.to_string r)
| _ -> fbody, Types.Unit)
| last :: rest ->
List.rev (expect fctx last.Tast.loc ~want:(Some ret) last :: rest)
(match ret0 with
| Some r ->
List.rev (expect fctx last.Tast.loc ~want:(Some r) last :: rest), r
| None -> fbody, last.Tast.ty)
in
(* Named after the function it was written in and numbered within it, which
is the handler clause's rule and is stable for the same reason: a
@ -4680,6 +4702,202 @@ 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 plural n = if n = 1 then "" else "s" in
let f =
match f.Ast.e with
(* The canonical inline form, [(array-gen [3 4] (fn [i j] ...))]. On its
own [check] would refuse the fn it takes its types from its position,
and only an argument position names them but *this* position knows
them just as well: one i32 index per dimension, and the annotated
element type as the return where the annotation reaches this deep.
With no annotation the return is left for the body to say, which is
the same inference the fill value gets. Arity is checked here so the
refusal talks about dimensions and indices, not about parameters some
(Fn ...) want expected. *)
| Ast.Fn (ps, fbody) ->
let got = List.length ps in
if got <> rank then
fail f.Ast.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);
check_fn ctx ~want:None
~gen:(List.init rank (fun _ -> index_ty), array_elem_want rank want)
f.Ast.loc ps fbody
| _ -> check ctx f
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

@ -183,6 +183,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
@ -192,12 +201,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) ->
@ -302,6 +306,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) ->
@ -767,6 +777,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

@ -444,6 +444,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,113 @@
;;;; (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))
;; An aggregate element: the store each loop pass writes is a struct copy.
(defstruct Cell [row i32 col i32])
;; Counts its own calls, for the evaluated-once line below.
(defvar calls i32)
(defn bump [] i32
(set calls (+ calls 1))
7)
(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
;; The generator written in place — the canonical inline form. The brackets
;; are the only thing that says what [i] and [j] are: one i32 index per
;; dimension, and the element type is read off the body.
(let [q (array-gen [2 3] (fn [i j] (+ (* i 10) j)))]
(print (at q 0 0)) (print " ") (print (at q 1 2)) (println "")) ; 0 12
;; A struct-valued fill. The element is an aggregate, so what the loop
;; writes per element is a struct copy, on every backend.
(let [cs (array-fill [2 2] (Cell 3 4))]
(print (.row (at cs 0 0))) (print " ")
(print (.col (at cs 1 1))) (println "")) ; 3 4
;; Evaluated once: the fill *value* is bound before any loop runs, so a
;; call in that position is one call, however many elements get its answer.
(let [f (array-fill [4] (bump))]
(print (at f 3)) (print " ") (print calls) (println "")) ; 7 1
;; 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,32 @@ 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. "3 4" is the
aggregate element the per-element store is a struct copy and
"7 1" is evaluated-once: the fill value's call ran one time for four
elements.
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\
0 12\n3 4\n7 1\nempty 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

@ -4940,10 +4940,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 _ -> ());
@ -5000,6 +5001,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

@ -719,6 +719,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
@ -919,6 +939,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]";
@ -2569,6 +2599,114 @@ let () =
rejects_check "but a mistyped machine type still is"
"(defn g [x f65] f64 x)" ~needle:"unknown type f65 — did you mean f64?";
(* ── (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)";
(* The inline form, the design's canonical one. An fn normally takes its
types from a (Fn ...) want, and this position has none the *form*
supplies them instead: one i32 index per dimension, and the annotated
element type as the return where there is one. With no annotation the
element type is the body's, the same inference the fill value gets. *)
infers "array-gen takes an inline fn, rank 1"
"(array-gen [5] (fn [i] (* i i)))" "[5 i32]";
infers "array-gen takes an inline fn, rank 2"
"(array-gen [2 3] (fn [i j] (+ (* i 100) j)))" "[2 [3 i32]]";
infers "an inline generator's element type is read off its body"
"(array-gen [3] (fn [i] (i64 i)))" "[3 i64]";
accepts "an annotated defvar takes an inline generator"
"(defvar grid [2 [3 u8]] (array-gen [2 3] (fn [i j] (u8 (+ i j)))))\n\
(defn f [] i32 (i32 (at grid 1 2)))";
(* The annotated element type is the want the body is checked against, so a
disagreement is reported at the generator's answer, in the ordinary
expected/found words not as a whole-array mismatch a line up. *)
rejects_check "an inline generator's body has to answer the element type"
"(defvar grid [2 [3 u8]] (array-gen [2 3] (fn [i j] 1.5)))\n\
(defn f [] i32 0)"
~needle:"expected u8, found f64";
rejects_check "an inline generator takes one argument per dimension too"
"(defn f [] i32 (let [a (array-gen [2] (fn [i j] i))] 0))"
~needle:"this array-gen has 1 dimension, so its generator is called with \
1 index and this one takes 2 arguments";
(* ── 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... *)