maps: preserve nil keys/values (jolt-c7h)
Janet structs drop nil, so {:b nil}, (assoc {} :b nil) and friends silently
lost the entry — diverging from Clojure, where a nil value is still a present
key. Map construction now uses a phm (which preserves nil) whenever a key or
value is nil; the nil-free case stays a fast struct.
Touched the whole construction path: the reader (a nil-bearing map literal form
is a phm, not a struct), eval-form (evaluates phm map-literal forms, rebuilds
preserving nil), build-map-literal (compiled path), core-assoc (promotes on a
nil key/value), and the host map-form predicates h-map?/h-map-pairs (recognize
a phm form) so the self-hosted analyzer handles {:b nil} literals.
Safe now that the back end densifies phm IR nodes and the IR is nil-field-clean
(prior commits): const-nil nodes go phm but read back correctly, and the common
nodes stay structs, so fib stays compiled (0.08s, no interpret fallback).
Also fixes clojure.set/rename-keys, which deleted the old key via (assoc m old
nil) — a trick that only worked while nil was dropped; now uses dissoc.
conformance 218/218 x3, suite green, clojure-test-suite 3919 -> 3926.
This commit is contained in:
parent
b2ff65972b
commit
c280b10b29
6 changed files with 67 additions and 23 deletions
|
|
@ -24,7 +24,12 @@
|
||||||
|
|
||||||
(defn rename-keys
|
(defn rename-keys
|
||||||
[map kmap]
|
[map kmap]
|
||||||
(reduce (fn [m [old new]] (if (contains? m old) (assoc m new (get m old) old nil) m)) map kmap))
|
(reduce (fn [m [old new]]
|
||||||
|
(if (contains? map old)
|
||||||
|
(assoc m new (get map old))
|
||||||
|
m))
|
||||||
|
(apply dissoc map (keys kmap))
|
||||||
|
kmap))
|
||||||
|
|
||||||
(defn map-invert
|
(defn map-invert
|
||||||
[m]
|
[m]
|
||||||
|
|
|
||||||
|
|
@ -1004,12 +1004,15 @@
|
||||||
# collection, in which case a phm so the key compares by value. Embedded as a
|
# collection, in which case a phm so the key compares by value. Embedded as a
|
||||||
# function constant in emitted code (functions marshal by reference).
|
# function constant in emitted code (functions marshal by reference).
|
||||||
(defn build-map-literal [& kvs]
|
(defn build-map-literal [& kvs]
|
||||||
(var coll-key false)
|
# phm (not a Janet struct) when a key is a collection (value-based hashing) or a
|
||||||
|
# key/value is nil (structs drop nil; phm preserves it, matching Clojure).
|
||||||
|
(var need-phm false)
|
||||||
(var ki 0)
|
(var ki 0)
|
||||||
(while (< ki (length kvs))
|
(while (< ki (length kvs))
|
||||||
(let [kk (in kvs ki)] (when (or (table? kk) (array? kk)) (set coll-key true)))
|
(let [kk (in kvs ki) vv (in kvs (+ ki 1))]
|
||||||
|
(when (or (table? kk) (array? kk) (nil? kk) (nil? vv)) (set need-phm true)))
|
||||||
(+= ki 2))
|
(+= ki 2))
|
||||||
(if coll-key
|
(if need-phm
|
||||||
(do (var m (make-phm)) (var j 0)
|
(do (var m (make-phm)) (var j 0)
|
||||||
(while (< j (length kvs)) (set m (phm-assoc m (in kvs j) (in kvs (+ j 1)))) (+= j 2))
|
(while (< j (length kvs)) (set m (phm-assoc m (in kvs j) (in kvs (+ j 1)))) (+= j 2))
|
||||||
m)
|
m)
|
||||||
|
|
|
||||||
|
|
@ -370,11 +370,14 @@
|
||||||
(if (= idx (length result)) (array/push result v) (put result idx v)))
|
(if (= idx (length result)) (array/push result v) (put result idx v)))
|
||||||
(+= i 2))
|
(+= i 2))
|
||||||
(if (tuple? m) (tuple/slice (tuple ;result)) result))
|
(if (tuple? m) (tuple/slice (tuple ;result)) result))
|
||||||
# map (struct/table). If any key is a collection, a Janet struct/table keys
|
# map (struct/table). Promote to a phm when any new key is a collection (a
|
||||||
# it by identity — promote to a phm so such keys compare by value.
|
# Janet struct/table would key it by identity) or any new key/value is nil (a
|
||||||
|
# struct drops nil; phm preserves it, matching Clojure). m itself is a struct
|
||||||
|
# here (phm handled above), so only the new kvs can introduce these.
|
||||||
(let [coll-key (do (var c false) (var i 0)
|
(let [coll-key (do (var c false) (var i 0)
|
||||||
(while (< i (length kvs))
|
(while (< i (length kvs))
|
||||||
(when (let [k (in kvs i)] (or (table? k) (array? k))) (set c true))
|
(let [k (in kvs i) v (in kvs (+ i 1))]
|
||||||
|
(when (or (table? k) (array? k) (nil? k) (nil? v)) (set c true)))
|
||||||
(+= i 2)) c)]
|
(+= i 2)) c)]
|
||||||
(if coll-key
|
(if coll-key
|
||||||
(do (var result (make-phm))
|
(do (var result (make-phm))
|
||||||
|
|
|
||||||
|
|
@ -1432,6 +1432,24 @@
|
||||||
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
|
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
|
||||||
(jolt-invoke ctx f args)))))
|
(jolt-invoke ctx f args)))))
|
||||||
|
|
||||||
|
# Build a map value from an array of evaluated [k v k v ...]. A phm (not a Janet
|
||||||
|
# struct) is used when a key is a collection (value-based hashing) OR a key/value
|
||||||
|
# is nil (Janet structs drop nil; phm preserves it, matching Clojure). The common
|
||||||
|
# scalar/nil-free case stays a struct.
|
||||||
|
(defn- map-needs-phm? [kvs]
|
||||||
|
(var need false) (var i 0)
|
||||||
|
(while (< i (length kvs))
|
||||||
|
(let [k (in kvs i) v (in kvs (+ i 1))]
|
||||||
|
(when (or (table? k) (array? k) (nil? k) (nil? v)) (set need true) (break)))
|
||||||
|
(+= i 2))
|
||||||
|
need)
|
||||||
|
|
||||||
|
(defn- build-eval-map [kvs]
|
||||||
|
(if (map-needs-phm? kvs)
|
||||||
|
(do (var m (make-phm)) (var j 0)
|
||||||
|
(while (< j (length kvs)) (set m (phm-assoc m (in kvs j) (in kvs (+ j 1)))) (+= j 2)) m)
|
||||||
|
(struct ;kvs)))
|
||||||
|
|
||||||
(set eval-form (fn [ctx bindings form]
|
(set eval-form (fn [ctx bindings form]
|
||||||
(cond
|
(cond
|
||||||
(nil? form) nil
|
(nil? form) nil
|
||||||
|
|
@ -1467,19 +1485,15 @@
|
||||||
(each k (keys form)
|
(each k (keys form)
|
||||||
(array/push kvs (eval-form ctx bindings k))
|
(array/push kvs (eval-form ctx bindings k))
|
||||||
(array/push kvs (eval-form ctx bindings (get form k))))
|
(array/push kvs (eval-form ctx bindings (get form k))))
|
||||||
# If any key is a collection (a Janet table/array — phm/pvec/plist/
|
(build-eval-map kvs)))))))
|
||||||
# record/list), a Janet struct would key it by identity; use a phm so
|
# A phm map-literal FORM (reader emits one for {:a nil} etc., which a struct
|
||||||
# such keys compare by value.
|
# would have dropped): evaluate its key/value forms and rebuild, preserving nil.
|
||||||
(var coll-key false)
|
(phm? form)
|
||||||
(var ki 0)
|
(let [kvs @[]]
|
||||||
(while (< ki (length kvs))
|
(each e (phm-entries form)
|
||||||
(let [kk (in kvs ki)] (when (or (table? kk) (array? kk)) (set coll-key true)))
|
(array/push kvs (eval-form ctx bindings (in e 0)))
|
||||||
(+= ki 2))
|
(array/push kvs (eval-form ctx bindings (in e 1))))
|
||||||
(if coll-key
|
(build-eval-map kvs))
|
||||||
(do (var m (make-phm)) (var j 0)
|
|
||||||
(while (< j (length kvs)) (set m (phm-assoc m (in kvs j) (in kvs (+ j 1)))) (+= j 2))
|
|
||||||
m)
|
|
||||||
(struct ;kvs))))))))
|
|
||||||
(array? form)
|
(array? form)
|
||||||
(if (= 0 (length form))
|
(if (= 0 (length form))
|
||||||
@[]
|
@[]
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
(use ./types)
|
(use ./types)
|
||||||
(use ./evaluator)
|
(use ./evaluator)
|
||||||
(use ./core)
|
(use ./core)
|
||||||
|
(import ./phm :as phm)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Form introspection
|
# Form introspection
|
||||||
|
|
@ -36,7 +37,12 @@
|
||||||
|
|
||||||
(defn h-list? [form] (array? form)) # a call / list (reader: array)
|
(defn h-list? [form] (array? form)) # a call / list (reader: array)
|
||||||
(defn h-vector? [form] (tuple? form)) # a vector literal (reader: tuple)
|
(defn h-vector? [form] (tuple? form)) # a vector literal (reader: tuple)
|
||||||
(defn h-map? [form] (and (struct? form) (nil? (form :jolt/type))))
|
# A map-literal form is a plain struct, or a phm when the reader preserved a nil
|
||||||
|
# key/value (Janet structs drop nil). Sets/chars/symbols are tagged structs (have
|
||||||
|
# :jolt/type); phm carries :jolt/deftype, distinct from those.
|
||||||
|
(defn h-map? [form]
|
||||||
|
(or (and (struct? form) (nil? (form :jolt/type)))
|
||||||
|
(phm/phm? form)))
|
||||||
(defn h-set? [form] (and (struct? form) (= :jolt/set (form :jolt/type))))
|
(defn h-set? [form] (and (struct? form) (= :jolt/set (form :jolt/type))))
|
||||||
(defn h-char? [form] (and (struct? form) (= :jolt/char (form :jolt/type))))
|
(defn h-char? [form] (and (struct? form) (= :jolt/char (form :jolt/type))))
|
||||||
|
|
||||||
|
|
@ -48,7 +54,9 @@
|
||||||
(defn h-elements [form] (make-vec form))
|
(defn h-elements [form] (make-vec form))
|
||||||
(defn h-vector-items [form] (make-vec form))
|
(defn h-vector-items [form] (make-vec form))
|
||||||
(defn h-map-pairs [form]
|
(defn h-map-pairs [form]
|
||||||
(make-vec (map (fn [k] (make-vec [k (get form k)])) (keys form))))
|
(if (phm/phm? form)
|
||||||
|
(make-vec (map (fn [e] (make-vec [(in e 0) (in e 1)])) (phm/phm-entries form)))
|
||||||
|
(make-vec (map (fn [k] (make-vec [k (get form k)])) (keys form)))))
|
||||||
(defn h-set-items [form] (make-vec (form :value)))
|
(defn h-set-items [form] (make-vec (form :value)))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
# Sets #{1 2} → tagged struct {:jolt/type :jolt/set :value [1 2]}
|
# Sets #{1 2} → tagged struct {:jolt/type :jolt/set :value [1 2]}
|
||||||
|
|
||||||
(use ./types)
|
(use ./types)
|
||||||
|
(import ./phm :as phm)
|
||||||
|
|
||||||
# Forward declaration for mutual recursion
|
# Forward declaration for mutual recursion
|
||||||
(var read-form nil)
|
(var read-form nil)
|
||||||
|
|
@ -266,6 +267,16 @@
|
||||||
(read-vec-items s new-pos (array/push items form))))))))
|
(read-vec-items s new-pos (array/push items form))))))))
|
||||||
(read-vec-items s (+ pos 1) @[]))
|
(read-vec-items s (+ pos 1) @[]))
|
||||||
|
|
||||||
|
# A map-literal form. Janet structs drop nil keys/values, so when a key or value
|
||||||
|
# is nil (e.g. {:a nil}) build a phm — it preserves nil, matching Clojure. The
|
||||||
|
# common nil-free case stays a struct: fast, and what the downstream map-form
|
||||||
|
# handling (evaluator/analyzer) already expects. Collection keys are left to
|
||||||
|
# eval-time construction (build-map-literal/eval-form), which phm-ifies them.
|
||||||
|
(defn- reader-map [kvs]
|
||||||
|
(var has-nil false) (var i 0)
|
||||||
|
(while (< i (length kvs)) (when (nil? (in kvs i)) (set has-nil true) (break)) (++ i))
|
||||||
|
(if has-nil (phm/make-phm kvs) (struct ;kvs)))
|
||||||
|
|
||||||
(defn read-map [s pos]
|
(defn read-map [s pos]
|
||||||
# pos is at opening brace
|
# pos is at opening brace
|
||||||
(defn read-kvs [s pos kvs]
|
(defn read-kvs [s pos kvs]
|
||||||
|
|
@ -273,7 +284,7 @@
|
||||||
(if (>= pos (length s))
|
(if (>= pos (length s))
|
||||||
(error "Unterminated map"))
|
(error "Unterminated map"))
|
||||||
(if (= (s pos) 125) # }
|
(if (= (s pos) 125) # }
|
||||||
[(struct ;kvs) (+ pos 1)]
|
[(reader-map kvs) (+ pos 1)]
|
||||||
(let [[key new-pos] (read-form s pos)]
|
(let [[key new-pos] (read-form s pos)]
|
||||||
(if (and (struct? key) (= :jolt/skip (key :jolt/type)))
|
(if (and (struct? key) (= :jolt/skip (key :jolt/type)))
|
||||||
(read-kvs s new-pos kvs)
|
(read-kvs s new-pos kvs)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue