core: staged recompile for early defns; keys/vals/empty? leave the seed (jolt-4j3)

recompile-defns! is the defn analog of recompile-macros!: pre/at-kernel
overlay defns (00-syntax's destructure and friends; the kernel tier too in
interpret mode) load as interpreted closures, the evaluator stashes their fn
source on the var (:defn-src, scoped by a flag only api/load-core-overlay!
sets), and the end-of-init pass compiles them and swaps the var root. With
that in place, keys/vals/empty? — the fns the 00-syntax expanders call at
expansion time — move to the top of 00-syntax as raw fn* defs (canonical:
keys/vals project (seq m), so sorted maps come back in comparator order and
(keys {}) is nil; empty? keeps O(1) count dispatch with seq's cell check only
for the lazy/list fallback). The sorted tier drops its now-dead :keys/:vals
ops.

Correctness fixes that surfaced once the gate was run with a REAL exit code
(the previous 'jpm test | grep' gates reported grep's exit and masked spec
failures across #48-#50):
- map conj is strict again: a non-nil/non-map arg must be a 2-element vector
  ('Vector arg to map conj must be a pair'), and merge inherits it — the
  batch-2 canonical merge had silently dropped the validation
- conj onto a lazy seq prepends (it fell into the MAP fallback); upstream
  clojure.data/diff relies on (conj seq x) via set/union over keys, so diff
  now matches Clojure exactly
- (seq {}) / (seq #{}) / empty phm are nil, not ()
- key/val are strict (a plain vector is not an entry); find mints a REAL
  entry as the first entry of a one-entry map, nil values intact
- the sci avoid-method-too-large stub passes its registry map through
  instead of returning a raw host table (strict conj rejected it; sci's
  clojure-core registry is also no longer discarded)

Test updates: lazy-infinite pins take-nth realization at 5 (was 7 — the
canonical lazy impl realizes fewer); self-host asserts the analyzer IS loaded
in interpret mode (compiled expanders, PR #50) and is NOT in the
:compile-macros? false oracle. 18 new maps-spec rows.

Gate: jpm test exit 0 (verified directly, not through a pipe), conformance
326x3, suite 4698 >= 4660.
This commit is contained in:
Yogthos 2026-06-10 16:16:23 -04:00
parent e6f562c175
commit 63eb6eca6e
11 changed files with 176 additions and 50 deletions

View file

@ -4,10 +4,45 @@
;; them is compiled — including the kernel tier, the self-hosted analyzer, and the
;; seq/coll tiers.
;;
;; CONSTRAINT: a macro here may use ONLY special forms (if/do/let*/fn*/not) and
;; core-renames SEED primitives (first/next/rest/nth/count/empty?/...). It must
;; NOT use kernel-tier fns (second/peek/subvec/...) or anything defined later —
;; those don't exist yet when this tier loads.
;; CONSTRAINT: code here may use ONLY special forms (if/do/let*/fn*/not) and
;; SEED primitives (first/next/rest/nth/count/seq/...), plus earlier defs in
;; THIS file. It must NOT use kernel-tier fns (second/peek/subvec/...) or
;; anything defined later — those don't exist yet when this tier loads. Raw
;; fn*/let* (no destructuring) and no when/cond/and/or above their defmacros.
;;
;; This tier's defns load interpreted and are recompiled by the staged pass
;; (backend/recompile-defns!) once the analyzer is alive — same lifecycle as
;; the defmacro expanders.
;; empty?/keys/vals live HERE (not 20-coll) because the expanders below call
;; them at expansion time, which first happens during the kernel-tier compile.
;; empty? keeps O(1) dispatch for counted things; only the lazy/list fallback
;; goes through seq's cell check.
(def empty?
(fn* empty? [coll]
(if (nil? coll)
true
(if (vector? coll)
(zero? (count coll))
(if (map? coll)
(zero? (count coll))
(if (set? coll)
(zero? (count coll))
(if (string? coll)
(zero? (count coll))
(nil? (seq coll)))))))))
;; Canonical: the seq of entries/elements, projected. (keys {}) is nil; sorted
;; maps iterate in comparator order ((seq sm) is the value's own :seq op).
(def keys
(fn* keys [m]
(let* [s (seq m)]
(if s (map (fn* [e] (nth e 0)) s) nil))))
(def vals
(fn* vals [m]
(let* [s (seq m)]
(if s (map (fn* [e] (nth e 1)) s) nil))))
(defmacro when [test & body]
`(if ~test (do ~@body)))

View file

@ -634,17 +634,18 @@
;; --- Phase 2 leaf batch 2 (jolt-ded): canonical Clojure ports ----------------
;; key/val/find first — merge-with and memoize below use them.
;; An entry is any 2-element vector in jolt ((seq m) yields tuples, find
;; builds a pvec — both count as entries; Clojure's stricter MapEntry-only
;; key/val has no analog here).
(defn- map-entryish? [e] (and (vector? e) (= 2 (count e))))
(defn key [e] (if (map-entryish? e) (nth e 0) (throw (ex-info "key requires a map entry" {}))))
(defn val [e] (if (map-entryish? e) (nth e 1) (throw (ex-info "val requires a map entry" {}))))
;; Strict, as in Clojure: an entry is what (seq m) yields (a host tuple), NOT
;; a plain vector — (key [1 2]) throws.
(defn key [e] (if (map-entry? e) (nth e 0) (throw (ex-info "key requires a map entry" {}))))
(defn val [e] (if (map-entry? e) (nth e 1) (throw (ex-info "val requires a map entry" {}))))
;; find was previously missing from jolt entirely. Presence (contains?), not
;; value, decides — so (find {:a nil} :a) is [:a nil]. Works on vectors by index.
;; value, decides — so (find {:a nil} :a) is [:a nil]. Works on vectors by
;; index. The result must be a REAL entry (key/val are strict), so it is
;; minted as the first entry of a one-entry map — nil values survive (the
;; map builder switches to a phm when nil is involved).
(defn find [m k]
(when (contains? m k) [k (get m k)]))
(when (contains? m k) (first {k (get m k)})))
(defn some? [x] (not (nil? x)))
(defn true? [x] (= true x))

View file

@ -142,8 +142,6 @@
:seq (fn [sm] (seq (sfield sm :entries)))
:rseq (fn [sm] (seq (vec (reverse (sfield sm :entries)))))
:first (fn [sm] (first (sfield sm :entries)))
:keys (fn [sm] (seq (mapv first (sfield sm :entries))))
:vals (fn [sm] (seq (mapv second (sfield sm :entries))))
:get sm-get
:contains (fn [sm k] (not (neg? (find-idx sm first k))))
:assoc sm-assoc-many