vec-new and map-new take a type expression where they take a type, so (vec-new [u8]) makes a Vec of byte slices

This commit is contained in:
Joseph Ferano 2026-09-25 07:10:48 +07:00
parent b4e19bebf8
commit aa629da2e5
7 changed files with 99 additions and 8 deletions

View File

@ -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)

View File

@ -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 \

View File

@ -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) ->

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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