diff --git a/FIX.org b/FIX.org index 3d0e193..67b56d5 100644 --- a/FIX.org +++ b/FIX.org @@ -1978,3 +1978,50 @@ message at a *new* site — check_fn's empty-body refusal — and rewrote none. parse.ml edits are structural: a dropped guard in ~when~, a dropped guard in ~fn~, a new arm at the top of ~dmap~. Expect a rebase, not a conflict of intent. + +* (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/check.ml b/lib/check.ml index fe3d4f1..f9fd53f 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -2908,38 +2908,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 @@ -2947,14 +2962,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 @@ -4248,8 +4268,31 @@ and check_array_fill ctx ~want loc dims v = 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 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) -> diff --git a/test/programs/array-fill.flan b/test/programs/array-fill.flan index f42aa2b..4be1fe9 100644 --- a/test/programs/array-fill.flan +++ b/test/programs/array-fill.flan @@ -30,6 +30,16 @@ (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)] @@ -77,6 +87,23 @@ (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. diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 7a46772..f10bf10 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -392,7 +392,10 @@ let () = 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. + — 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 @@ -400,7 +403,7 @@ let () = 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" + 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" diff --git a/test/test_flan.ml b/test/test_flan.ml index a436d6e..14099ea 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2269,6 +2269,32 @@ let () = (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... *)