From aa629da2e591e86318a0c64a1fd027a29753e377 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 07:10:48 +0700 Subject: [PATCH] vec-new and map-new take a type expression where they take a type, so (vec-new [u8]) makes a Vec of byte slices --- lib/ast.ml | 8 +++++++- lib/check.ml | 31 +++++++++++++++++++++++++------ lib/load.ml | 3 ++- lib/parse.ml | 27 +++++++++++++++++++++++++++ test/programs/vec-new-type.flan | 25 +++++++++++++++++++++++++ test/test_acceptance.ml | 6 ++++++ test/test_flan.ml | 7 +++++++ 7 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 test/programs/vec-new-type.flan diff --git a/lib/ast.ml b/lib/ast.ml index c49e9e36..cf923cc0 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -97,6 +97,12 @@ 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 *) + (* (vec-new [u8]) and (map-new string [u8]) — a type written where an + argument goes. Only the type positions of those two forms read one, and + only when the form's shape says type and not value: brackets, or a + parenthesised Ptr, Option, Vec, Map, Fn or CFn. A bare name stays a + [Var], which the checker already answers as a type. *) + | TypeArg of texpr (* (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 @@ -403,7 +409,7 @@ let map_children f (e : expr) : expr = let kind = match e.e with | Int _ | Float _ | Byte _ | Str _ | Kw _ | Quote _ | Var _ | ArrayOf _ - | Break _ | Continue _ -> e.e + | TypeArg _ | Break _ | Continue _ -> e.e | Do es -> Do (List.map ex es) | Let (bs, es) -> Let (List.map bind bs, List.map ex es) | If (c, a, b) -> If (ex c, ex a, Option.map ex b) diff --git a/lib/check.ml b/lib/check.ml index 0865e717..49711623 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3547,6 +3547,11 @@ 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)) + (* Parse writes one only into a type position of vec-new or map-new, and + those read it before it could get here. *) + | Ast.TypeArg _ -> + fail loc "internal: a type argument reached the checker outside vec-new or \ + map-new — this is a compiler bug" | 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 @@ -6564,12 +6569,15 @@ and type_named ctx n = || Hashtbl.mem ctx.env.enums n || Hashtbl.mem ctx.env.aliases n -(* The element type for [vec-new]: a leading bare symbol naming a type, or the - expectation at the site. A bare symbol shadowed by a local or a global is - that binding — an allocator, in practice — and not a type. *) +(* The element type for [vec-new]: a leading bare symbol naming a type, a + leading type expression — [(vec-new [u8])], [(vec-new (Ptr Cell))], which + Parse has already read as one — or the expectation at the site. A bare + symbol shadowed by a local or a global is that binding — an allocator, in + practice — and not a type. *) and vec_new_elem ctx ~want loc args = let named = match args with + | { Ast.e = Ast.TypeArg t; _ } :: rest -> Some (resolve ctx.env t, rest) | { Ast.e = Ast.Var n; _ } :: rest when lookup ctx n = None && (not (Hashtbl.mem ctx.env.globals n)) @@ -6604,10 +6612,21 @@ and map_new_types ctx ~want loc args = && (not (Hashtbl.mem ctx.env.globals n)) && type_named ctx n in + (* A type position holds a bare name or a type expression Parse has read + as one, as [vec-new]'s does. *) + let as_type (a : Ast.expr) = + match a.Ast.e with + | Ast.TypeArg t -> Some (resolve ctx.env t) + | Ast.Var n when is_type n -> Some (resolve_name ctx.env ~seen:[] loc n) + | _ -> None + in match args with - | { Ast.e = Ast.Var k; _ } :: { Ast.e = Ast.Var v; _ } :: rest - when is_type k && is_type v -> - resolve_name ctx.env ~seen:[] loc k, resolve_name ctx.env ~seen:[] loc v, rest + | k :: v :: rest when as_type k <> None && as_type v <> None -> + Option.get (as_type k), Option.get (as_type v), rest + | { Ast.e = Ast.TypeArg _; _ } :: _ -> + fail loc + "(map-new) names a key and no value — write both, as (map-new string \ + i32), or give the binding a type" | { Ast.e = Ast.Var k; _ } :: rest when is_type k && rest = [] -> fail loc "(map-new %s) names a key and no value — write both, as (map-new %s \ diff --git a/lib/load.ml b/lib/load.ml index 96629722..4fb38a9e 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -307,6 +307,7 @@ 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) + | Ast.TypeArg t -> Ast.TypeArg (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 @@ -787,7 +788,7 @@ let rec expr_uses acc (e : Ast.expr) = | Ast.Bare kvs -> List.iter (fun (_, v) -> go v) kvs | 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 + | Ast.ArrayOf t | Ast.TypeArg 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) -> diff --git a/lib/parse.ml b/lib/parse.ml index 456c8a8b..d17399b2 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -425,6 +425,33 @@ 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)") + (* ── (vec-new [u8]) and (map-new string [u8]) ─────────────────────── + The type positions of these two take a type expression as well as a bare + name. A bare name is left for the checker, which knows whether it names a + type or an allocator; a bracket or a parenthesised type constructor can + only be a type there, so it is read as one now, with [texpr], the reader + a parameter list's types go through. *) + | Sym (("vec-new" | "builtin/vec-new" | "map-new" | "builtin/map-new") as n) -> + let slots = + if n = "vec-new" || n = "builtin/vec-new" then 1 else 2 + in + let is_type (a : Form.t) = + match a.v with + | Vec _ -> true + | List ({ v = Sym ("Ptr" | "Option" | "Vec" | "Map" | "Fn" | "CFn"); _ } + :: _ :: _) -> true + | _ -> false + in + let args = + List.mapi + (fun i a -> + if i < slots && is_type a then + { Ast.e = Ast.TypeArg (texpr a); loc = a.loc } + else expr a) + args + in + mk (Ast.Call (expr head, args)) + (* ── (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 diff --git a/test/programs/vec-new-type.flan b/test/programs/vec-new-type.flan new file mode 100644 index 00000000..5d338a70 --- /dev/null +++ b/test/programs/vec-new-type.flan @@ -0,0 +1,25 @@ +;;;; vec-new and map-new take a type expression where they take a type, so a +;;;; local can hold a Vec of slices, of arrays or of pointers with nothing +;;;; else naming the element type. The last Vec names an allocator after its +;;;; type, which is the one argument that may follow. + +(defn main [] i32 + (let [a (arena-new 4096) + words (vec-new [u8]) + pairs (vec-new [2 i32]) + ptrs (vec-new (Ptr i32)) + opts (vec-new (Option i64) a) + m (map-new string [u8]) + x (i32 7)] + (push words (bytes-view "ab")) + (push words (bytes-view "cde")) + (push pairs [3 4]) + (push ptrs (addr x)) + (push opts (Some (i64 9))) + (put m "k" (bytes-view "xyz")) + (println (length words) (length (at words 1)) + (at (at pairs 0) 1) (deref (at ptrs 0)) + (match (at opts 0) (Some v) v None -1) + (match (get m "k") (Some v) (length v) None -1)) + (free words) (free pairs) (free ptrs) (free m)) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 7a21bb0c..d1163867 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -502,6 +502,12 @@ let () = rebinds every name at once, and interleaved writes would print 1. *) outputs "loop and recur" "programs/recur.flan" "10\n2\n21\n8\n10000000\n64\n012\n0\n4\n012\n6\n17\n"; + (* A type expression where vec-new and map-new take a type: a slice, an + array, a pointer and an option, with nothing else naming the element. *) + outputs "vec-new takes a type expression" "programs/vec-new-type.flan" + "2 3 4 7 9 3\n"; + outputs ~x86:true "vec-new takes a type expression, x86" + "programs/vec-new-type.flan" "2 3 4 7 9 3\n"; (* into. The count of pulls is the assertion a unit test cannot make: one pass, one call per element per stage it reaches, and no intermediate collection anywhere. The two show lines either side of it are the same diff --git a/test/test_flan.ml b/test/test_flan.ml index 04f993e8..ff0cf47d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -6031,6 +6031,13 @@ let () = rejects_check "vec-new with no element type and nothing to take one from" ~needle:"nothing here says what (vec-new) is a Vec of" "(defn f [x $t] i32 (do x (let [v (vec-new)] (free v) 0)))"; + (* A type expression in a type position, generic or not. *) + accepts "vec-new over a slice of a type variable" + "(defn f [x [$t]] i32 (let [v (vec-new [$t])] (push v x) \ + (let [n (length v)] (free v) n)))"; + rejects_check "map-new with a key type expression and no value type" + ~needle:"(map-new) names a key and no value" + "(defn f [] i32 (let [m (map-new [u8])] (free m) 0))"; (* And a sigil on a name nothing binds is answered as the unbound variable it is, rather than as a missing element type — with the names that *are* bound, because inside a signature that introduces one the mistake is