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

@ -75,7 +75,9 @@
["LAZY take-while" "6" "(do (def c (atom 0)) (dorun (take-while (fn [x] (swap! c inc) (< x 5)) (range))) @c)"]
["LAZY drop-while" "6" "(do (def c (atom 0)) (dorun (take 3 (drop-while (fn [x] (swap! c inc) (< x 5)) (range)))) @c)"]
["LAZY distinct" "4" "(do (def c (atom 0)) (dorun (take 3 (distinct (map (fn [x] (swap! c inc) x) (cycle [1 2 1 3 1]))))) @c)"]
["LAZY take-nth" "7" "(do (def c (atom 0)) (dorun (take 3 (take-nth 2 (map (fn [x] (swap! c inc) x) (range))))) @c)"]
# 5, was 7: the canonical lazy take-nth (40-lazy, jolt-ded batch 3) realizes
# only the elements the taken outputs and their drops touch.
["LAZY take-nth" "5" "(do (def c (atom 0)) (dorun (take 3 (take-nth 2 (map (fn [x] (swap! c inc) x) (range))))) @c)"]
["LAZY map-indexed" "3" "(do (def c (atom 0)) (dorun (take 3 (map-indexed (fn [i x] (swap! c inc) [i x]) (range)))) @c)"]
["LAZY keep" "6" "(do (def c (atom 0)) (dorun (take 3 (keep (fn [x] (swap! c inc) (if (odd? x) x nil)) (range)))) @c)"]
["LAZY keep-indexed" "6" "(do (def c (atom 0)) (dorun (take 3 (keep-indexed (fn [i x] (swap! c inc) (if (odd? i) x)) (range)))) @c)"]

View file

@ -75,3 +75,17 @@
(os/setenv "JOLT_INTERPRET_MACROS" nil)
(print "compiled macro expansion passed!")
# 6. Early overlay DEFNS get the same staged-recompile treatment (jolt-4j3):
# 00-syntax fns (destructure, and the expander-called keys/vals/empty?) load
# interpreted pre-kernel, then compile in the same end-of-init pass.
(each nm ["destructure" "empty?" "keys" "vals"]
(def v (macro-var ictx nm))
(assert v (string nm " var exists"))
(assert (get v :defn-compiled)
(string nm " early defn is compiled (interpret mode)")))
(def cctx2 (init {:compile? true}))
(assert (get (macro-var cctx2 "destructure") :defn-compiled)
"early defn compiled in compile mode too")
(assert (not (get (macro-var octx "destructure") :defn-compiled))
"oracle mode keeps early defns interpreted")

View file

@ -70,9 +70,17 @@
# Proof the self-hosted pipeline actually ran: only backend/ensure-analyzer
# populates jolt.analyzer. An interpret-only ctx never loads it.
(assert (pos? (length ((ctx-find-ns ctx "jolt.analyzer") :mappings))) "analyzer loaded under :compile?"))
# Interpret mode now loads the analyzer too — for compiled macro expansion
# (ensure-macros-compiled!, every mode). The fully-interpreted oracle is the
# :compile-macros? false ctx, which must never touch the analyzer.
(let [ctx (init-cached {})]
(eval-one ctx (parse-string "(+ 1 2)"))
(assert (zero? (length ((ctx-find-ns ctx "jolt.analyzer") :mappings))) "analyzer NOT loaded when interpreting"))
(assert (pos? (length ((ctx-find-ns ctx "jolt.analyzer") :mappings)))
"analyzer loaded when interpreting (compiled expanders)"))
(let [ctx (init-cached {:compile-macros? false})]
(eval-one ctx (parse-string "(+ 1 2)"))
(assert (zero? (length ((ctx-find-ns ctx "jolt.analyzer") :mappings)))
"analyzer NOT loaded in the interpreted-macro oracle"))
# clojure.core overlay: fns moved from core.janet to jolt-core/clojure/core.clj
# load into clojure.core at init and work the same compiled or interpreted.

View file

@ -155,3 +155,25 @@
["update-vals empty" "{}" "(update-vals {} inc)"]
["update-vals nil" "{}" "(update-vals nil inc)"]
["update-vals keeps keys" "[:a :b]" "(sort (keys (update-vals {:a 1 :b 2} inc)))"])
# keys/vals/empty? are 00-syntax overlay fns now (jolt-4j3) — expander-called,
# so they live in the first tier and get the staged defn recompile.
(defspec "maps / keys-vals-empty? as overlay fns"
["keys" "(quote (:a))" "(keys {:a 1})"]
["keys empty map" "nil" "(keys {})"]
["keys nil" "nil" "(keys nil)"]
["vals" "(quote (1))" "(vals {:a 1})"]
["vals empty" "nil" "(vals {})"]
["keys sorted order" "[1 2 3]" "(vec (keys (sorted-map 2 :b 1 :a 3 :c)))"]
["vals sorted order" "[:a :b :c]" "(vec (vals (sorted-map 2 :b 1 :a 3 :c)))"]
["keys/vals zip" "{:a 1 :b 2}" "(zipmap (keys {:a 1 :b 2}) (vals {:a 1 :b 2}))"]
["empty? map" "true" "(empty? {})"]
["empty? vec" "[true false]" "[(empty? []) (empty? [1])]"]
["empty? list" "[true false]" "[(empty? ()) (empty? (list 1))]"]
["empty? string" "[true false]" "[(empty? \"\") (empty? \"a\")]"]
["empty? nil" "true" "(empty? nil)"]
["empty? set" "[true false]" "[(empty? #{}) (empty? #{1})]"]
["empty? lazy" "[true false]" "[(empty? (filter pos? [-1])) (empty? (map inc [1]))]"]
["empty? lazy nil elem" "false" "(empty? (cons nil nil))"]
["empty? sorted" "[true false]" "[(empty? (sorted-map)) (empty? (sorted-set 1))]"]
["empty? number throws" :throws "(empty? 5)"])