diff --git a/FIX.org b/FIX.org index f3a8435..9e21d4f 100644 --- a/FIX.org +++ b/FIX.org @@ -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. diff --git a/lib/ast.ml b/lib/ast.ml index 44430a5..3105f57 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -92,6 +92,28 @@ and expr_kind = fails on an unknown name. This is that position's answer, and it says what it does rather than looking like a vector of two things. *) | ArrayOf of texpr (* the whole array type, built by Parse *) + (* (array-fill [r c] v) and (array-gen [r c] f) — a fixed array of any rank + as an *expression*, which is what [ArrayOf] and [dotimes] between them + could not be: [ArrayOf] produces the zeroed value only, and [dotimes] is + Unit and can only mutate a place that already exists. These produce the + whole value, so they compose where a bracket literal does. + + The dimensions are in brackets and are [len]s, not expressions, for the + reason the brackets are read at all: in expression position [[rows cols]] + is an array *literal* of two names, and where those names are defconsts + it would quietly type-check as one. So the form is recognised in [Parse] + and the brackets are read with the same [len] the [n T] type spelling + uses — an integer or a compile-time constant's name, and nothing else. + + Two forms rather than one with a dispatch on the third element's type: an + array *of function values* is a thing one may want, and a single form + would have to decide whether [(array-fill [4] f)] meant four copies of + [f] or four calls of it. Spelled apart, neither reading is ever in doubt. + + [ArrayGen]'s expression is a function value taking one index per + dimension; [ArrayFill]'s is the element value itself, evaluated once. *) + | ArrayFill of len list * expr + | ArrayGen of len list * expr (* These bind names or alter control flow, so none of them can be a call. *) | Fn of string list * expr list (* (fn [x y] ...) — non-escaping *) | Dotimes of string option * string * expr * expr list (* (dotimes :o [i n] ...) *) @@ -368,6 +390,10 @@ let map_children f (e : expr) : expr = | Bare fs -> Bare (List.map (fun (n, v) -> (n, ex v)) fs) | MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs) | Arr es -> Arr (List.map ex es) + (* Not leaves: the fill value and the generator are ordinary + subexpressions. The dimensions are [len]s and hold none. *) + | ArrayFill (ds, v) -> ArrayFill (ds, ex v) + | ArrayGen (ds, f) -> ArrayGen (ds, ex f) | Fn (ps, es) -> Fn (ps, List.map ex es) | Dotimes (l, n, c, es) -> Dotimes (l, n, ex c, List.map ex es) | Defer es -> Defer (List.map ex es) diff --git a/lib/check.ml b/lib/check.ml index bebe66f..86bfeb3 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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,38 +3412,53 @@ 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 = - match want with - | Some (Types.Fn (ps, r)) when List.length ps = List.length params -> ps, r - | Some (Types.Fn (ps, r)) -> - fail loc - "this fn has %d parameter%s and %s was wanted here" - (List.length params) - (if List.length params = 1 then "" else "s") - (Types.to_string (Types.Fn (ps, r))) - | Some other when other <> Types.Never -> - fail loc "expected %s, found an fn" (Types.to_string other) - | _ -> - 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" +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, Some r + | Some (Types.Fn (ps, r)) -> + fail loc + "this fn has %d parameter%s and %s was wanted here" + (List.length params) + (if List.length params = 1 then "" else "s") + (Types.to_string (Types.Fn (ps, r))) + | Some other when other <> Types.Never -> + fail loc "expected %s, found an fn" (Types.to_string other) + | _ -> + 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" 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) -> - 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 + | [] -> + (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 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 diff --git a/lib/load.ml b/lib/load.ml index 28c0b9a..9c81481 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -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 diff --git a/lib/parse.ml b/lib/parse.ml index 1f22cff..1c74b23 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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)) diff --git a/test/programs/array-fill.flan b/test/programs/array-fill.flan new file mode 100644 index 0000000..4be1fe9 --- /dev/null +++ b/test/programs/array-fill.flan @@ -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) diff --git a/test/programs/dev-rerun.flan b/test/programs/dev-rerun.flan index f2445b5..f1ff710 100644 --- a/test/programs/dev-rerun.flan +++ b/test/programs/dev-rerun.flan @@ -46,6 +46,14 @@ ;; the line that would count 1, 1, 1, 1. (defvar tally 0) +;; A typed array with a computed initialiser: (array-fill ...) is an +;; expression, so it is lifted into the startup function and guarded there +;; exactly as [counter]'s call is. If it were not — if a fill re-ran on every +;; entry into main — this would count 251, 251, 251, 251 instead of climbing, +;; which is [counter]'s own failure in the one shape that only an array can +;; have. +(defvar grid [2 [3 u8]] (array-fill [2 3] 250)) + ;; The guard flags the fix adds are the compiler's own globals, and they used ;; to be spelled [.init-once.] — a name a program can write, since [.] ;; is an ordinary symbol constituent. This one is exactly the old spelling of @@ -62,10 +70,15 @@ (set .init-once.counter (+ .init-once.counter 1)) (put state :runs (+ (get state :runs) 1)) (set tally (+ tally 1)) + (set (at grid 0 0) (u8 (+ (i32 (at grid 0 0)) 1))) (print "counter ") (print counter) (println "") (print "zeroed ") (print zeroed) (println "") (print "runs ") (print (get state :runs)) (println "") (print "tally ") (print tally) (println "") + (print "grid ") (print (i32 (at grid 0 0))) (println "") + ;; The element the run never writes, which says the fill ran at all: 250 + ;; on every run, and 0 if the initialiser had been skipped outright. + (print "grid-far ") (print (i32 (at grid 1 2))) (println "") (print "base ") (print base) (println "") ;; Long enough for a client to be served, short enough to park well inside ;; any watchdog — dev-macro.flan's clock, for its reason. diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index bea8d35..1675f15 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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 diff --git a/test/test_dev.ml b/test/test_dev.ml index f9fb4c1..60f92c9 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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 diff --git a/test/test_flan.ml b/test/test_flan.ml index 7b21077..2e5f69d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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... *)