Skip to content

Commit 5837cfe

Browse files
leifericfclaude
andcommitted
Add MINO_MAP_ENTRY value type
JVM Clojure's MapEntry is vector-shaped but distinct: (key [1 2]) throws because a plain vector isn't a Map.Entry. mino conflated the two, so key.cljc and val.cljc failed on the throw assertions. Add a distinct MINO_MAP_ENTRY type carrying (k, v). seq of a map / sorted-map / record now produces MAP_ENTRY values; find / clojure.lang.MapEntry/create likewise. key / val accept only MAP_ENTRY. vector? / coll? / counted? / associative? / reversible? / sequential? return true on it. Equality with [k v] is element-wise via the cross-type sequential path; hash matches a 2-vector so it round-trips through hash maps. first / rest / nth / get / count / empty? / vector destructuring / compare / into-map / conj-map / conj-of-MAP_ENTRY all dispatch through it. New C primitive `map-entry` constructs one. aset is intentionally not implemented for MAP_ENTRY (entries are immutable in JVM Clojure too). Internal 1476 / 7089 / 0. key.cljc 8/17 -> 17/17, val.cljc 7/16 -> 16/16, plus merge / sort / sort_by / zipmap / seq fixes for the MAP_ENTRY contagion through into / conj / compare / hash. External suite 209 -> 211 OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8d4fc83 commit 5837cfe

17 files changed

Lines changed: 298 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
# Changelog
22

3+
## v0.100.32
4+
5+
### Add `MINO_MAP_ENTRY` value type
6+
7+
JVM Clojure's MapEntry is a vector-shaped seq returned by `first` /
8+
`seq` of a map; `key` and `val` accept it but throw on a plain
9+
2-vector. mino conflated map entries and 2-vectors, so
10+
`(p/thrown? (key [1 2]))` failed. Add a distinct `MINO_MAP_ENTRY`
11+
type with `(k, v)` slots, GC mark + verify, hash that matches a
12+
2-vector (so cross-type equality works in hash maps), `(type x)`
13+
returns `:map-entry`, and `vector?` / `coll?` / `counted?` /
14+
`associative?` / `reversible?` / `sequential?` return true on it.
15+
Equality with `[k v]` is element-wise via the existing cross-type
16+
sequential path. `seq` of a map / sorted-map / record now produces
17+
MAP_ENTRY values; `find`, `first`, `rest`, `nth`, `get`, `count`,
18+
`empty?`, vector destructuring, `compare`, `into`-map, `conj`-map,
19+
and `conj`-of-MAP_ENTRY all dispatch through it. `key` / `val` in
20+
`src/core.clj` accept only MAP_ENTRY and throw otherwise.
21+
`clojure.lang.MapEntry/create` now constructs a MAP_ENTRY (via the
22+
new `map-entry` C primitive). `aset` is intentionally not
23+
implemented for MAP_ENTRY since entries are immutable.
24+
25+
External `key.cljc` 8/17 -> 17/17, `val.cljc` 7/16 -> 16/16. External
26+
suite: 209 -> 211 OK.
27+
328
## v0.100.31
429

530
### `(float x)` narrows to 32-bit float precision

src/collections/clone.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ static mino_val_t *clone_val(mino_state_t *dst, const mino_val_t *v)
213213
case MINO_RECORD:
214214
case MINO_FUTURE:
215215
case MINO_HOST_ARRAY:
216+
case MINO_MAP_ENTRY:
216217
return NULL;
217218
case MINO_UUID:
218219
return mino_uuid_from_bytes(dst, v->as.uuid.bytes);

src/collections/map.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,15 @@ uint32_t hash_val(const mino_val_t *v)
187187
}
188188
return h;
189189
}
190+
case MINO_MAP_ENTRY: {
191+
/* Hashes as a 2-vector of (k, v) so cross-type equality with
192+
* MINO_VECTOR also gives identical hashes (so a hash-map keyed
193+
* by entries vs. by 2-vectors still compares equal). */
194+
h = fnv_mix(h, 0x09);
195+
h = hash_uint32_bytes(h, hash_val(v->as.map_entry.k));
196+
h = hash_uint32_bytes(h, hash_val(v->as.map_entry.v));
197+
return h;
198+
}
190199
case MINO_MAP: {
191200
/* XOR-fold of per-entry hashes for order independence. Each entry's
192201
* hash mixes key and value hashes with a prime to avoid (k ^ v)

src/collections/val.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ mino_val_t *mino_cons(mino_state_t *S, mino_val_t *car, mino_val_t *cdr)
228228
return v;
229229
}
230230

231+
mino_val_t *mino_map_entry(mino_state_t *S, mino_val_t *k, mino_val_t *v)
232+
{
233+
mino_val_t *e = alloc_val(S, MINO_MAP_ENTRY);
234+
e->as.map_entry.k = k;
235+
e->as.map_entry.v = v;
236+
return e;
237+
}
238+
231239
/* ------------------------------------------------------------------------- */
232240
/* Host arrays */
233241
/* ------------------------------------------------------------------------- */
@@ -626,7 +634,8 @@ static int eq_seq_like_force(mino_state_t *S, const mino_val_t *a,
626634
static int is_sequential(mino_type_t t)
627635
{
628636
return (t == MINO_CONS || t == MINO_VECTOR || t == MINO_EMPTY_LIST
629-
|| t == MINO_LAZY || t == MINO_CHUNKED_CONS);
637+
|| t == MINO_LAZY || t == MINO_CHUNKED_CONS
638+
|| t == MINO_MAP_ENTRY);
630639
}
631640

632641
/*
@@ -666,7 +675,7 @@ static void eq_seq_step(const mino_val_t **cur, size_t *idx)
666675
}
667676
return;
668677
}
669-
/* MINO_VECTOR */
678+
/* MINO_VECTOR or MINO_MAP_ENTRY (treated as 2-element vector). */
670679
(*idx)++;
671680
}
672681

@@ -691,10 +700,12 @@ static int eq_seq_like(const mino_val_t *a, const mino_val_t *b)
691700
a_end = (ca == NULL || ca->type == MINO_NIL
692701
|| ca->type == MINO_EMPTY_LIST
693702
|| (ca->type == MINO_VECTOR && ia >= ca->as.vec.len)
703+
|| (ca->type == MINO_MAP_ENTRY && ia >= 2)
694704
|| ca->type == MINO_LAZY /* unrealized */);
695705
b_end = (cb == NULL || cb->type == MINO_NIL
696706
|| cb->type == MINO_EMPTY_LIST
697707
|| (cb->type == MINO_VECTOR && ib >= cb->as.vec.len)
708+
|| (cb->type == MINO_MAP_ENTRY && ib >= 2)
698709
|| cb->type == MINO_LAZY /* unrealized */);
699710

700711
if (a_end && b_end) return 1;
@@ -703,11 +714,15 @@ static int eq_seq_like(const mino_val_t *a, const mino_val_t *b)
703714
if (ca->type == MINO_CONS) ea = ca->as.cons.car;
704715
else if (ca->type == MINO_CHUNKED_CONS)
705716
ea = ca->as.chunked_cons.chunk->as.chunk.vals[ia];
717+
else if (ca->type == MINO_MAP_ENTRY)
718+
ea = ia == 0 ? ca->as.map_entry.k : ca->as.map_entry.v;
706719
else ea = vec_nth(ca, ia);
707720

708721
if (cb->type == MINO_CONS) eb = cb->as.cons.car;
709722
else if (cb->type == MINO_CHUNKED_CONS)
710723
eb = cb->as.chunked_cons.chunk->as.chunk.vals[ib];
724+
else if (cb->type == MINO_MAP_ENTRY)
725+
eb = ib == 0 ? cb->as.map_entry.k : cb->as.map_entry.v;
711726
else eb = vec_nth(cb, ib);
712727

713728
if (!mino_eq(ea, eb)) return 0;
@@ -1020,6 +1035,9 @@ int mino_eq(const mino_val_t *a, const mino_val_t *b)
10201035
* (object-array 3) calls are NOT `=` even if their elements
10211036
* match. */
10221037
return a == b;
1038+
case MINO_MAP_ENTRY:
1039+
return mino_eq(a->as.map_entry.k, b->as.map_entry.k)
1040+
&& mino_eq(a->as.map_entry.v, b->as.map_entry.v);
10231041
}
10241042
return 0;
10251043
}
@@ -1053,22 +1071,28 @@ static int eq_seq_like_force(mino_state_t *S, const mino_val_t *a,
10531071

10541072
a_end = (ca == NULL || ca->type == MINO_NIL
10551073
|| ca->type == MINO_EMPTY_LIST
1056-
|| (ca->type == MINO_VECTOR && ia >= ca->as.vec.len));
1074+
|| (ca->type == MINO_VECTOR && ia >= ca->as.vec.len)
1075+
|| (ca->type == MINO_MAP_ENTRY && ia >= 2));
10571076
b_end = (cb == NULL || cb->type == MINO_NIL
10581077
|| cb->type == MINO_EMPTY_LIST
1059-
|| (cb->type == MINO_VECTOR && ib >= cb->as.vec.len));
1078+
|| (cb->type == MINO_VECTOR && ib >= cb->as.vec.len)
1079+
|| (cb->type == MINO_MAP_ENTRY && ib >= 2));
10601080

10611081
if (a_end && b_end) return 1;
10621082
if (a_end || b_end) return 0;
10631083

10641084
if (ca->type == MINO_CONS) ea = ca->as.cons.car;
10651085
else if (ca->type == MINO_CHUNKED_CONS)
10661086
ea = ca->as.chunked_cons.chunk->as.chunk.vals[ia];
1087+
else if (ca->type == MINO_MAP_ENTRY)
1088+
ea = ia == 0 ? ca->as.map_entry.k : ca->as.map_entry.v;
10671089
else ea = vec_nth(ca, ia);
10681090

10691091
if (cb->type == MINO_CONS) eb = cb->as.cons.car;
10701092
else if (cb->type == MINO_CHUNKED_CONS)
10711093
eb = cb->as.chunked_cons.chunk->as.chunk.vals[ib];
1094+
else if (cb->type == MINO_MAP_ENTRY)
1095+
eb = ib == 0 ? cb->as.map_entry.k : cb->as.map_entry.v;
10721096
else eb = vec_nth(cb, ib);
10731097

10741098
if (!mino_eq_force(S, ea, eb)) return 0;

src/core.clj

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -836,11 +836,12 @@
836836
"Returns true if x supports assoc (maps and vectors)."
837837
[x]
838838
(let [t (type x)]
839-
(or (= t :map) (= t :vector) (= t :sorted-map))))
839+
(or (= t :map) (= t :vector) (= t :sorted-map) (= t :map-entry))))
840840
(defn reversible?
841841
"Returns true if x supports rseq (vectors and sorted collections)."
842842
[x] (let [t (type x)]
843-
(or (= t :vector) (= t :sorted-map) (= t :sorted-set))))
843+
(or (= t :vector) (= t :sorted-map) (= t :sorted-set)
844+
(= t :map-entry))))
844845
(defn any? "Returns true for any argument." [x] true)
845846
(defn seqable?
846847
"Returns true if (seq x) is supported."
@@ -912,8 +913,20 @@
912913

913914
;; --- Map entry accessors ---
914915

915-
(defn key "Returns the key of a map entry." [entry] (first entry))
916-
(defn val "Returns the value of a map entry." [entry] (second entry))
916+
(defn key
917+
"Returns the key of a map entry. Throws on values that are not
918+
map entries (a literal 2-vector, for instance)."
919+
[entry]
920+
(if (= :map-entry (type entry))
921+
(first entry)
922+
(throw (str "key: expected a map entry, got " (type entry)))))
923+
(defn val
924+
"Returns the value of a map entry. Throws on values that are not
925+
map entries (a literal 2-vector, for instance)."
926+
[entry]
927+
(if (= :map-entry (type entry))
928+
(second entry)
929+
(throw (str "val: expected a map entry, got " (type entry)))))
917930

918931
(defn counted?
919932
"Returns true if (count x) is a constant-time operation. Per
@@ -923,7 +936,7 @@
923936
[x]
924937
(let [t (type x)]
925938
(or (= t :vector) (= t :map) (= t :set)
926-
(= t :sorted-map) (= t :sorted-set))))
939+
(= t :sorted-map) (= t :sorted-set) (= t :map-entry))))
927940

928941
(defn bounded-count
929942
"Returns the count of coll, but stops counting at n."
@@ -3193,12 +3206,12 @@
31933206
(def clojure.lang.IPending :future)
31943207
(def clojure.lang.BigInt :bigint)
31953208

3196-
;; Bridge `clojure.lang.MapEntry/create` to a 2-vector constructor.
3209+
;; Bridge `clojure.lang.MapEntry/create` to mino's MAP_ENTRY type.
31973210
;; Cross-dialect tests use it under `:default` to build a map entry
3198-
;; literal; mino represents map entries as 2-vectors and `key`/`val`
3199-
;; already accept that shape, so the only missing piece is the ctor.
3211+
;; literal; the C primitive `map-entry` returns a MINO_MAP_ENTRY,
3212+
;; which `key`/`val` accept.
32003213
(ns clojure.lang.MapEntry)
3201-
(defn create [k v] [k v])
3214+
(defn create [k v] (clojure.core/map-entry k v))
32023215
(in-ns 'clojure.core)
32033216

32043217
;; In Clojure JVM the primed arithmetic forms (`+'`, `-'`, `*'`,

src/eval/bindings.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ static int bind_vec_destructure(mino_state_t *S, mino_env_t *env,
5050
lst = mino_cons(S, vec_nth(val, j), lst);
5151
}
5252
args = lst;
53+
} else if (val != NULL && val->type == MINO_MAP_ENTRY) {
54+
/* Map entry behaves like a 2-vector for destructuring. */
55+
args = mino_cons(S, val->as.map_entry.k,
56+
mino_cons(S, val->as.map_entry.v, mino_nil(S)));
5357
}
5458
for (i = 0; i < plen; i++) {
5559
mino_val_t *p = vec_nth(pattern, i);

src/eval/print.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,16 @@ void mino_print_to(mino_state_t *S, FILE *out, const mino_val_t *v)
450450
fputc('"', out);
451451
return;
452452
}
453+
case MINO_MAP_ENTRY: {
454+
/* Print as `[k v]` so map-entry round-trips through equality
455+
* with a 2-vector and matches Clojure's pr/print form. */
456+
fputc('[', out);
457+
mino_print_to(S, out, v->as.map_entry.k);
458+
fputc(' ', out);
459+
mino_print_to(S, out, v->as.map_entry.v);
460+
fputc(']', out);
461+
return;
462+
}
453463
case MINO_HOST_ARRAY: {
454464
/* Mirror Clojure JVM's #object[...] form for arrays since
455465
* arrays don't round-trip through the reader. */

src/eval/special.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ mino_val_t *eval_impl(mino_state_t *S, mino_val_t *form, mino_env_t *env, int ta
716716
case MINO_UUID:
717717
case MINO_REGEX:
718718
case MINO_HOST_ARRAY:
719+
case MINO_MAP_ENTRY:
719720
return form;
720721
case MINO_SYMBOL:
721722
return eval_symbol(S, form, env);

src/gc/driver.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,10 @@ void gc_trace_children(mino_state_t *S, gc_hdr_t *h)
454454
}
455455
break;
456456
}
457+
case MINO_MAP_ENTRY:
458+
gc_mark_child_push(S, v->as.map_entry.k);
459+
gc_mark_child_push(S, v->as.map_entry.v);
460+
break;
457461
case MINO_CHUNKED_CONS:
458462
gc_mark_child_push(S, v->as.chunked_cons.chunk);
459463
gc_mark_child_push(S, v->as.chunked_cons.more);

src/gc/minor.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ static void gc_verify_remset_complete(mino_state_t *S)
276276
}
277277
break;
278278
}
279+
case MINO_MAP_ENTRY:
280+
gc_verify_check(S, h, v->as.map_entry.k);
281+
gc_verify_check(S, h, v->as.map_entry.v);
282+
break;
279283
case MINO_CHUNKED_CONS:
280284
gc_verify_check(S, h, v->as.chunked_cons.chunk);
281285
gc_verify_check(S, h, v->as.chunked_cons.more); break;

0 commit comments

Comments
 (0)