A fixed array of strings or of structs is a map key, hashed and compared element by element

This commit is contained in:
Joseph Ferano 2026-09-25 15:21:30 +07:00
parent a41e43cbba
commit 580d377847
4 changed files with 161 additions and 13 deletions

View File

@ -911,11 +911,6 @@ there is a =Vec= to write real arena programs with, so whether the escapes that
actually occur are lexical can now be answered. The next thing to look at, not the actually occur are lexical can now be answered. The next thing to look at, not the
next thing to build. next thing to build.
** TODO A fixed array of structs or of strings is not a map key
Refused by name, narrower than the spec's key set; a struct holding the array
works. It needs the per-element walk a struct key gets, driven by a loop rather
than a field list.
** DONE (vec-new [u8]) is refused ** DONE (vec-new [u8]) is refused
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
The type positions of =vec-new= and =map-new= take a type expression: brackets, or The type positions of =vec-new= and =map-new= take a type expression: brackets, or

View File

@ -3662,14 +3662,9 @@ let rec key_pair env loc (k : Types.t) : Tast.fnref * Tast.fnref =
fail loc fail loc
"%s is a union, and a union is not a map key — key on the member you \ "%s is a union, and a union is not a map key — key on the member you \
meant" n meant" n
| Types.Array (_, e) -> (* A bytewise array took the arm above; this is one whose elements need
(* A fixed array of a struct or of strings would need the same per-element their own pair, a string's or a struct's. *)
walk a struct key gets, driven by a loop rather than by a field list. | Types.Array (n, e) -> array_key_pair env loc n e
Nothing has wanted one, so it is refused by name rather than written
untested — and refused with the shape that does work named beside it. *)
fail loc
"a fixed array of %s is not a map key — a struct holding the array is"
(Types.to_string e)
| Types.Float _ -> | Types.Float _ ->
(* Not a milestone question, which is why it is said separately: NaN is not (* Not a milestone question, which is why it is said separately: NaN is not
equal to itself, and 0.0 and -0.0 are equal while differing bytewise. A equal to itself, and 0.0 and -0.0 are equal while differing bytewise. A
@ -3801,6 +3796,124 @@ and struct_key_pair env loc n =
Tast.Flanfn hname, Tast.Flanfn ename Tast.Flanfn hname, Tast.Flanfn ename
end end
(* The pair for a fixed array whose elements are not bytewise: the struct
pair's shape, with the field list replaced by a loop over the elements, so
[[64 string]] is one call site in a loop and not sixty-four. Each element is
hashed and compared by its own pair, so an array of structs holding strings
is served by the same recursion. *)
and array_key_pair env loc n e =
if Int64.compare n 0L <= 0 then
fail loc
"%s has no elements, so it is not a map key — every value of it would be \
the same key" (Types.to_string (Types.Array (n, e)));
let aty = Types.Array (n, e) in
(* The type's printed form, with what a symbol cannot hold replaced. *)
let tag =
String.map
(fun c -> match c with
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '-' -> c
| _ -> '_')
(Types.to_string aty)
in
let hname = "map/hash/array/" ^ tag and ename = "map/eq/array/" ^ tag in
let known name =
List.exists (fun (f : Tast.fn) -> f.Tast.name = name) env.lifted
in
if known hname then Tast.Flanfn hname, Tast.Flanfn ename
else begin
let pty = Types.Ptr (Types.Mut, aty) in
let hparams = [ pty; hash_ty; Types.Int Types.I64 ] in
let eparams = [ pty; pty; Types.Int Types.I64 ] in
let placeholder name ret params =
{ Tast.name; params; slots = Array.of_list params;
snames = Array.make (List.length params) None;
ret; body = []; fdefers = []; fenv = None; fparent = None; floc = loc }
in
env.lifted <-
placeholder hname hash_ty hparams
:: placeholder ename (Types.Int Types.I8) eparams
:: env.lifted;
let h, eq = key_pair env loc e in
let call ret f args =
match f with
| Tast.Rtfn s -> rt loc ret (direct s) args
| Tast.Flanfn s | Tast.Fnval s -> mk loc ret (Tast.Call (s, args))
in
let elem_addr p i =
let target = mk loc aty (Tast.Deref (mk loc pty (Tast.Local p))) in
mk loc (Types.Ptr (Types.Mut, e))
(Tast.Addr (Tast.Pindex (target, [ mk loc index_ty (Tast.Local i) ])))
in
(* The counter and its loop, which carries no break and no continue — the
condition tast.ml puts on a [While] the checker invents. *)
let loop ctx body =
let i = fresh_slot ~name:"i" ctx index_ty in
let iv = mk loc index_ty (Tast.Local i) in
let limit = mk loc index_ty (Tast.Int (n, Types.I32)) in
let one = mk loc index_ty (Tast.Int (1L, Types.I32)) in
let cond = mk loc Types.Bool (Tast.Prim (Tast.Lt, [ iv; limit ])) in
let step =
mk loc Types.Unit
(Tast.Set (Tast.Plocal i,
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; one ]))))
in
mk loc Types.Unit
(Tast.Let ([ (i, mk loc index_ty (Tast.Int (0L, Types.I32))) ],
[ mk loc Types.Unit (Tast.While (cond, [ body i ], [ step ])) ]))
in
let hctx = invented_ctx env hash_ty in
let kp = fresh_slot ~name:"key" hctx pty in
let seed = fresh_slot ~name:"seed" hctx hash_ty in
ignore (fresh_slot ~name:"size" hctx (Types.Int Types.I64));
let acc = fresh_slot ~name:"h" hctx hash_ty in
let hbody =
[ mk loc Types.Unit
(Tast.Set (Tast.Plocal acc, mk loc hash_ty (Tast.Local seed)));
loop hctx (fun i ->
let one =
call hash_ty h
[ elem_addr kp i; mk loc hash_ty (Tast.Local seed);
size_of loc e ]
in
mk loc Types.Unit
(Tast.Set (Tast.Plocal acc,
rt loc hash_ty "flan_hash_combine"
[ mk loc hash_ty (Tast.Local acc); one ])));
mk loc hash_ty (Tast.Local acc) ]
in
let ectx = invented_ctx env (Types.Int Types.I8) in
let ap = fresh_slot ~name:"a" ectx pty in
let bp = fresh_slot ~name:"b" ectx pty in
ignore (fresh_slot ~name:"size" ectx (Types.Int Types.I64));
let i8 v = mk loc (Types.Int Types.I8) (Tast.Int (v, Types.I8)) in
let ebody =
[ loop ectx (fun i ->
let same =
call (Types.Int Types.I8) eq
[ elem_addr ap i; elem_addr bp i; size_of loc e ]
in
mk loc Types.Unit
(Tast.If (mk loc Types.Bool (Tast.Prim (Tast.Eq, [ same; i8 0L ])),
mk loc Types.Never (Tast.Return (Some (i8 0L))),
unit_at loc)));
i8 1L ]
in
let finish name ret params ctx body =
{ Tast.name; params;
slots = Array.of_list (List.rev ctx.slot_tys);
snames = Array.of_list (List.rev ctx.slot_names);
ret; body; fdefers = []; fenv = None; fparent = None; floc = loc }
in
env.lifted <-
finish hname hash_ty hparams hctx hbody
:: finish ename (Types.Int Types.I8) eparams ectx ebody
:: List.filter
(fun (f : Tast.fn) ->
f.Tast.name <> hname && f.Tast.name <> ename)
env.lifted;
Tast.Flanfn hname, Tast.Flanfn ename
end
(* The pair as two expressions, ready to be passed. Their Flan type is (* The pair as two expressions, ready to be passed. Their Flan type is
[(Ptr ())]: one opaque word, which is all the backend needs. *) [(Ptr ())]: one opaque word, which is all the backend needs. *)
let key_fns env loc k = let key_fns env loc k =

View File

@ -0,0 +1,30 @@
;; A fixed array of strings, and of structs holding a string, as a map key.
;; Each element is hashed and compared by its own pair, so two keys built from
;; different storage with the same bytes are the same key.
(defstruct Tag [name string n i32])
(defn main [] i32
(let [m (map-new [2 string] i32)
a (the [2 string] ["ab" "cd"])
b (the [2 string] [(slice "xab" 1) (slice "cdx" 0 2)])
c (the [2 string] ["ab" "ce"])]
(put m a 1)
(put m c 3)
(println (or-else (get m b) -1))
(println (or-else (get m c) -1))
(println (length m))
(put m b 2)
(println (length m))
(println (or-else (get m a) -1))
(free m))
(let [t (map-new [3 Tag] i32)
x (the [3 Tag] [(Tag "a" 1) (Tag "b" 2) (Tag "c" 3)])
y (the [3 Tag] [(Tag "a" 1) (Tag "b" 2) (Tag "c" 4)])]
(put t x 10)
(put t y 20)
(println (or-else (get t (the [3 Tag] [(Tag "a" 1) (Tag "b" 2) (Tag "c" 3)])) -1))
(println (or-else (get t y) -1))
(println (length t))
(free t))
0)

View File

@ -4424,6 +4424,16 @@ level "1"
outputs ~opt:"-O0" "map iteration, -O0" "programs/map-iter.flan" map_iter_out; outputs ~opt:"-O0" "map iteration, -O0" "programs/map-iter.flan" map_iter_out;
outputs "map-keys and map-values" "programs/map-keys.flan" outputs "map-keys and map-values" "programs/map-keys.flan"
"10 20 30 \n100 200 300 \n3\n0\n"; "10 20 30 \n100 200 300 \n3\n0\n";
(* A fixed array of strings, and of structs, as a key: an emitted pair
with a loop in it, the one hashing and equality function the checker
builds around a [While]. *)
let map_array_key_out = "1\n3\n2\n2\n2\n10\n20\n2\n" in
outputs "an array of strings or structs as a map key"
"programs/map-array-key.flan" map_array_key_out;
outputs ~opt:"-O0" "an array of strings or structs as a map key, -O0"
"programs/map-array-key.flan" map_array_key_out;
outputs ~x86:true "an array of strings or structs as a map key, --x86"
"programs/map-array-key.flan" map_array_key_out;
(* Removal, which is the operation that can break the others. A probe (* Removal, which is the operation that can break the others. A probe
stops at the first group holding an empty slot, so a slot emptied in stops at the first group holding an empty slot, so a slot emptied in