diff --git a/BUILT.md b/BUILT.md index b29fc07..9b6d984 100644 --- a/BUILT.md +++ b/BUILT.md @@ -2435,6 +2435,30 @@ reason `-linkall` is not optional. Say plainly what that coverage is not: nothin before this landed, so `macro-unless.flan` is a test written after the feature. The corpus written before it is `sand.flan` and `web/examples/control.flan`, and both compile unchanged. +## `(array 4 rl/Vector2)`, and the one position with no type slot + +`[4 T]` is the ordinary type spelling and is unchanged. It already works everywhere a type is expected — `(defvar +points [4 rl/Vector2])`, `(defn draw [pts [4 rl/Vector2]] ...)`, a `defstruct` field, a return. A **`let` binding is +the single position with no type slot**, and there the brackets are read as what they are in expression position: an +array *literal* of two elements, whose second element is a type name nothing declares. So `(let [pts [4 rl/Vector2]] +...)` failed with *unknown name rl/Vector2*, which describes the symptom and not the mistake. It cost 32 hand-written +`Vector2`s in one raylib example. + +`(array COUNT TYPE)` is the answer: a zeroed fixed array, told its count and its element type as plain arguments. +`(array 4 rl/Vector2)` and the type `[4 rl/Vector2]` denote the same type, so the constructor is assignable to a +declaration written the other way and either spelling can be the parameter — `array-ctor.flan` asserts exactly that. + +It is a parser form and not a builtin call, because the second argument is a *type* and there are no types in the +parser's callers. `Parse` assembles the whole `Tarray (len COUNT, TYPE)` itself, which is why the count takes a +constant's name for free — `len` is the same function `[n T]` goes through — and why a non-type second argument is +refused by the type reader's own message rather than as an unknown name. The checker resolves it and hands back +`Tast.Zero`, the same node a declaration with no initialiser gets. There is no new backend node and no new type. + +**`(zeroed)` was the first proposal and was rejected on how it reads.** `(zeroed [4 rl/Vector2])` is unambiguous to the +*parser* — a bracket in argument position could be a type there — but to a person it still looks like a two-element +vector, which is the exact confusion being fixed. `zeroed` keeps its existing job: the empty value of whatever type the +destination wants, inferred and never written. `array` is the one that is told. + ## `defer` may be written in a `let` The whole of this project's resource-cleanup answer, and NEXT.md records `drop` and a `with-cleanup` form as both diff --git a/NEXT.md b/NEXT.md index ca0bea9..9439b8c 100644 --- a/NEXT.md +++ b/NEXT.md @@ -685,7 +685,7 @@ debug tracking allocator, which is the leak safety net and a good candidate when ## Decided in discussion — the array constructor and the module system -**`(array 4 rl/Vector2)` makes a fixed array; `[4 T]` stays the type syntax.** The problem this solves: a `let` +**`(array 4 rl/Vector2)` makes a fixed array; `[4 T]` stays the type syntax. Built** — see BUILT.md, "`(array 4 rl/Vector2)`, and the one position with no type slot". The problem it solved: a `let` binding takes no type, so `(let [pts [4 rl/Vector2]] ...)` reads `[4 rl/Vector2]` as a two-element array *literal* and fails with *unknown name rl/Vector2*. It cost 32 hand-written `Vector2`s in one raylib example. diff --git a/lib/ast.ml b/lib/ast.ml index 9485081..e147eef 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -48,6 +48,13 @@ and expr_kind = | Match of expr * arm list | Struct of string * (string * expr) list (* (Cursor {.src s}) *) | Arr of expr list (* [0xE6B800FF ...] — a fixed array value *) + (* (array 4 rl/Vector2) — a zeroed fixed array, given its count and its + element type. [n T] is the ordinary *type* syntax and already works + everywhere a type is expected; a [let] binding is the one position with no + type slot, so there [4 rl/Vector2] reads as a two-element [Arr] literal and + 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 *) (* 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 * expr * expr list (* (dotimes [i n] ...) *) diff --git a/lib/check.ml b/lib/check.ml index a593571..8835f99 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -920,6 +920,12 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr = expect loc ~want (mk loc fty (Tast.Field (target, i)))) | Ast.Struct (name, kvs) -> check_struct ctx ~want loc name kvs | Ast.Arr items -> check_arr ctx ~want loc items + (* (array 4 rl/Vector2). Parse already assembled the whole array type, so + there is nothing to infer: resolve it and hand back its all-bytes-zero + value, which is what a declared array with no initialiser gets. *) + | Ast.ArrayOf t -> + let ty = resolve ctx.env t in + expect loc ~want (mk loc ty (Tast.Zero ty)) | Ast.Match (scrutinee, arms) -> check_match ctx ?want loc scrutinee arms | Ast.Call (head, args) -> check_call ctx ~want loc head args | Ast.Unwrap (Ast.Usome, v) -> diff --git a/lib/load.ml b/lib/load.ml index f02584d..574ad77 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -201,6 +201,7 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr = | Ast.Struct (n, kvs) -> Ast.Struct (name n, List.map (fun (k, v) -> (k, go v)) kvs) | Ast.Arr items -> Ast.Arr (gos items) + | Ast.ArrayOf t -> Ast.ArrayOf (rename_texpr owned alias t) | Ast.Fn (ps, body) -> Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body) | Ast.Dotimes (i, n, body) -> @@ -382,6 +383,7 @@ let rec expr_uses acc (e : Ast.expr) = acc := (n, e.Ast.loc) :: !acc; List.iter (fun (_, v) -> go v) kvs | Ast.Arr items -> gos items + | Ast.ArrayOf t -> texpr_uses acc t | 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 33b737b..864957c 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -177,6 +177,24 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = | [ target; value ] -> mk (Ast.Set (place target, expr value)) | _ -> fail f "set is (set place value)") + (* ── (array 4 rl/Vector2) ─────────────────────────────────────────── + A zeroed fixed array, told its count and its element type. The type + spelling [4 rl/Vector2] is unchanged and still works everywhere a type is + expected; what it cannot do is appear in a [let] binding, which has no + type slot, because there the brackets are an array *literal* of two + elements and the second of them is a name nothing declares. So the count + and the type arrive as plain arguments and Parse assembles the type + itself. [(zeroed)] keeps its own job — the empty value of whatever the + destination wants — and this is the one that is told. *) + | Sym "array" -> + (match args with + | [ n; t ] -> + mk (Ast.ArrayOf { Ast.t = Ast.Tarray (len n, texpr t); tloc = f.loc }) + | _ -> + fail f + "array is (array COUNT TYPE), as in (array 4 rl/Vector2) — a zeroed \ + fixed array of COUNT of them") + | Sym "match" -> (match args with | scrutinee :: rest -> mk (Ast.Match (expr scrutinee, arms f rest)) diff --git a/test/programs/array-ctor.flan b/test/programs/array-ctor.flan new file mode 100644 index 0000000..9010b3c --- /dev/null +++ b/test/programs/array-ctor.flan @@ -0,0 +1,43 @@ +;;;; (array COUNT TYPE) — the zeroed fixed array a [let] binding could not ask +;;;; for. A let has no type slot, so [4 V2] there is an array *literal* of two +;;;; elements and the second of them is a type name, which is an unknown name +;;;; and not a helpful error. The type spelling is untouched: `points` below is +;;;; still declared [3 V2], and the two are the same type, which is the point — +;;;; the constructor is assignable to the declaration. +(defstruct V2 [x f32 y f32]) + +(defconst n 3) +(defvar points [3 V2]) + +(defn sumx [ps [3 V2]] i32 + (let [t (f32 0.0)] + (dotimes [i 3] + (set t (+ t (.x (at ps i))))) + (i32 t))) + +(defn main [] i32 + ;; The case that cost 32 hand-written Vector2s: a local array of structs. + (let [pts (array 3 V2)] + (set (.x (at pts 0)) 1.5) + (set (.x (at pts 2)) 2.5) + (print (sumx pts)) (println "")) ; 4 + + ;; Zeroed, not uninitialised: every element reads as the all-zero value. + (let [z (array 4 i32)] + (print (at z 3)) (println "")) ; 0 + + ;; The count takes a constant's name, exactly as [n V2] does. + (let [c (array n V2)] + (set (.y (at c 1)) 7.0) + (print (i32 (.y (at c 1)))) (println "")) ; 7 + + ;; Element types nest, and the result is assignable to a declaration written + ;; the other way round — same type, two spellings. + (let [g (array 2 [2 i32])] + (set (at (at g 1) 1) 9) + (print (at (at g 1) 1)) (println "")) ; 9 + + (set points (array 3 V2)) + (set (.x (at points 0)) 4.0) + (print (sumx points)) (println "") ; 4 + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 37db25f..bb08748 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -117,6 +117,9 @@ let () = outputs "value semantics" "programs/values.flan" values_out; outputs "machine surface" "programs/machine.flan" machine_out; outputs "unit main exits 0" "programs/unit-main.flan" "ok\n"; + (* (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"; (* The prelude's slice algorithms. Every assertion here is over an input a wrong implementation fails: unsorted with duplicates, negatives and an odd length; a reverse-sorted slice; and a sort of a subslice whose diff --git a/test/test_flan.ml b/test/test_flan.ml index 3afb524..3362325 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -462,6 +462,12 @@ let () = parse_rejects "odd field pairs" "(defstruct S [a])"; parse_rejects "cond without body" "(cond a)"; parse_rejects "unknown top form" "(nope x)"; + parse_rejects "array with no type" "(defn f [] (array 4))" + ~needle:"array is (array COUNT TYPE)"; + parse_rejects "array given a value, not a type" "(defn f [] (array 4 5))" + ~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 corpus parses ─────────────────────────────────────────── *) List.iter @@ -612,6 +618,14 @@ let () = infers "cast" "(f64 3)" "f64"; infers "array literal" "[1 2 3]" "[3 i32]"; infers "nested array" "[[1 2] [3 4]]" "[2 [2 i32]]"; + (* (array COUNT TYPE): the constructor a [let] binding needs, because a let + has no type slot and [4 P] there is a two-element literal whose second + element is a name nothing declares. The type spelling is unchanged — the + two [infers] above still hold — and this is the position that had no way + to say it. *) + 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]]"; 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]";