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 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. *) it does rather than looking like a vector of two things. *)
| ArrayOf of texpr (* the whole array type, built by Parse *) | 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 (* (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 as an *expression*, which is what [ArrayOf] and [dotimes] between them
could not be: [ArrayOf] produces the zeroed value only, and [dotimes] is 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 = let kind =
match e.e with match e.e with
| Int _ | Float _ | Byte _ | Str _ | Kw _ | Quote _ | Var _ | ArrayOf _ | Int _ | Float _ | Byte _ | Str _ | Kw _ | Quote _ | Var _ | ArrayOf _
| Break _ | Continue _ -> e.e | TypeArg _ | Break _ | Continue _ -> e.e
| Do es -> Do (List.map ex es) | Do es -> Do (List.map ex es)
| Let (bs, es) -> Let (List.map bind bs, 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) | 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 -> | Ast.ArrayOf t ->
let ty = resolve ctx.env t in let ty = resolve ctx.env t in
expect ctx loc ~want (mk loc ty (Tast.Zero ty)) 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.ArrayFill (dims, v) -> check_array_fill ctx ~want loc dims v
| Ast.ArrayGen (dims, f) -> check_array_gen ctx ~want loc dims f | 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.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.enums n
|| Hashtbl.mem ctx.env.aliases n || Hashtbl.mem ctx.env.aliases n
(* The element type for [vec-new]: a leading bare symbol naming a type, or the (* The element type for [vec-new]: a leading bare symbol naming a type, a
expectation at the site. A bare symbol shadowed by a local or a global is leading type expression — [(vec-new [u8])], [(vec-new (Ptr Cell))], which
that binding — an allocator, in practice — and not a type. *) 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 = and vec_new_elem ctx ~want loc args =
let named = let named =
match args with match args with
| { Ast.e = Ast.TypeArg t; _ } :: rest -> Some (resolve ctx.env t, rest)
| { Ast.e = Ast.Var n; _ } :: rest | { Ast.e = Ast.Var n; _ } :: rest
when lookup ctx n = None when lookup ctx n = None
&& (not (Hashtbl.mem ctx.env.globals n)) && (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)) && (not (Hashtbl.mem ctx.env.globals n))
&& type_named ctx n && type_named ctx n
in 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 match args with
| { Ast.e = Ast.Var k; _ } :: { Ast.e = Ast.Var v; _ } :: rest | k :: v :: rest when as_type k <> None && as_type v <> None ->
when is_type k && is_type v -> Option.get (as_type k), Option.get (as_type v), rest
resolve_name ctx.env ~seen:[] loc k, resolve_name ctx.env ~seen:[] loc 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 = [] -> | { Ast.e = Ast.Var k; _ } :: rest when is_type k && rest = [] ->
fail loc fail loc
"(map-new %s) names a key and no value — write both, as (map-new %s \ "(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.MapLit (tag, List.map (fun (k, v) -> (go k, go v)) kvs)
| Ast.Arr items -> Ast.Arr (gos items) | Ast.Arr items -> Ast.Arr (gos items)
| Ast.ArrayOf t -> Ast.ArrayOf (rename_texpr owned alias t) | 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 (* The dimensions too, for the reason [rename_texpr] gives about the one
inside [Tarray]: a dimension written as a name is an ordinary inside [Tarray]: a dimension written as a name is an ordinary
compile-time constant of the package and has to be qualified like any 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.Bare kvs -> List.iter (fun (_, v) -> go v) kvs
| Ast.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs | Ast.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs
| Ast.Arr items -> gos items | 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 (* A dimension written as a name is a use of that constant, exactly as it is
inside [Tarray]. *) inside [Tarray]. *)
| Ast.ArrayFill (ds, v) | Ast.ArrayGen (ds, v) -> | 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)) | [ target; value ] -> mk (Ast.Set (place target, expr value))
| _ -> fail f "set is (set place 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) ─────────────────────────────────────────── (* ── (array 4 rl/Vector2) ───────────────────────────────────────────
A zeroed fixed array, told its count and its element type. The type 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 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. *) rebinds every name at once, and interleaved writes would print 1. *)
outputs "loop and recur" "programs/recur.flan" outputs "loop and recur" "programs/recur.flan"
"10\n2\n21\n8\n10000000\n64\n012\n0\n4\n012\n6\n17\n"; "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 (* 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 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 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" 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" ~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)))"; "(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 (* 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* 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 bound, because inside a signature that introduces one the mistake is