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
|
||||
[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
|
||||
[m]
|
||||
|
|
|
|||
|
|
@ -1004,12 +1004,15 @@
|
|||
# collection, in which case a phm so the key compares by value. Embedded as a
|
||||
# function constant in emitted code (functions marshal by reference).
|
||||
(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)
|
||||
(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))
|
||||
(if coll-key
|
||||
(if need-phm
|
||||
(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)
|
||||
|
|
|
|||
|
|
@ -370,11 +370,14 @@
|
|||
(if (= idx (length result)) (array/push result v) (put result idx v)))
|
||||
(+= i 2))
|
||||
(if (tuple? m) (tuple/slice (tuple ;result)) result))
|
||||
# map (struct/table). If any key is a collection, a Janet struct/table keys
|
||||
# it by identity — promote to a phm so such keys compare by value.
|
||||
# map (struct/table). Promote to a phm when any new key is a collection (a
|
||||
# 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)
|
||||
(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)]
|
||||
(if coll-key
|
||||
(do (var result (make-phm))
|
||||
|
|
|
|||
|
|
@ -1432,6 +1432,24 @@
|
|||
args (map |(eval-form ctx bindings $) (tuple/slice form 1))]
|
||||
(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]
|
||||
(cond
|
||||
(nil? form) nil
|
||||
|
|
@ -1467,19 +1485,15 @@
|
|||
(each k (keys form)
|
||||
(array/push kvs (eval-form ctx bindings k))
|
||||
(array/push kvs (eval-form ctx bindings (get form k))))
|
||||
# If any key is a collection (a Janet table/array — phm/pvec/plist/
|
||||
# record/list), a Janet struct would key it by identity; use a phm so
|
||||
# such keys compare by value.
|
||||
(var coll-key false)
|
||||
(var ki 0)
|
||||
(while (< ki (length kvs))
|
||||
(let [kk (in kvs ki)] (when (or (table? kk) (array? kk)) (set coll-key true)))
|
||||
(+= ki 2))
|
||||
(if coll-key
|
||||
(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))))))))
|
||||
(build-eval-map kvs)))))))
|
||||
# A phm map-literal FORM (reader emits one for {:a nil} etc., which a struct
|
||||
# would have dropped): evaluate its key/value forms and rebuild, preserving nil.
|
||||
(phm? form)
|
||||
(let [kvs @[]]
|
||||
(each e (phm-entries form)
|
||||
(array/push kvs (eval-form ctx bindings (in e 0)))
|
||||
(array/push kvs (eval-form ctx bindings (in e 1))))
|
||||
(build-eval-map kvs))
|
||||
(array? form)
|
||||
(if (= 0 (length form))
|
||||
@[]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
(use ./types)
|
||||
(use ./evaluator)
|
||||
(use ./core)
|
||||
(import ./phm :as phm)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Form introspection
|
||||
|
|
@ -36,7 +37,12 @@
|
|||
|
||||
(defn h-list? [form] (array? form)) # a call / list (reader: array)
|
||||
(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-char? [form] (and (struct? form) (= :jolt/char (form :jolt/type))))
|
||||
|
||||
|
|
@ -48,7 +54,9 @@
|
|||
(defn h-elements [form] (make-vec form))
|
||||
(defn h-vector-items [form] (make-vec 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)))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
# Sets #{1 2} → tagged struct {:jolt/type :jolt/set :value [1 2]}
|
||||
|
||||
(use ./types)
|
||||
(import ./phm :as phm)
|
||||
|
||||
# Forward declaration for mutual recursion
|
||||
(var read-form nil)
|
||||
|
|
@ -266,6 +267,16 @@
|
|||
(read-vec-items s new-pos (array/push items form))))))))
|
||||
(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]
|
||||
# pos is at opening brace
|
||||
(defn read-kvs [s pos kvs]
|
||||
|
|
@ -273,7 +284,7 @@
|
|||
(if (>= pos (length s))
|
||||
(error "Unterminated map"))
|
||||
(if (= (s pos) 125) # }
|
||||
[(struct ;kvs) (+ pos 1)]
|
||||
[(reader-map kvs) (+ pos 1)]
|
||||
(let [[key new-pos] (read-form s pos)]
|
||||
(if (and (struct? key) (= :jolt/skip (key :jolt/type)))
|
||||
(read-kvs s new-pos kvs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue