Chez Phase 2 (inc K): namespace value model (jolt-yxqm)

Reimplement the ctx-coupled seed ns natives over the rt.ss var-table, since
Chez has no ctx. host/chez/ns.ss adds a jns namespace value + a registry and
binds find-ns/the-ns/create-ns/in-ns/all-ns/ns-publics/ns-map/ns-interns/
ns-aliases/resolve/find-var/ns-unmap/*ns* into clojure.core.

The resolve friction: native-ops (+, map, …) are inlined at emit so they have
no var-cell, and (resolve '+) was nil — diverging from Clojure where it's a
var. Added a defined? flag to the var-cell record (set by def-var!/declare-var!,
left false on a lazily-materialised forward ref) and def-var!'d every native-op
name to its value-position proc, so resolve returns the cell iff genuinely
defined. ns-unmap clears the flag. resolve never interns an empty cell
(var-cell-lookup is non-creating).

ns-name is overridden natively in post-prelude (the overlay reads
(get ns :name), nil on a jns record); the printers render a namespace as its
name. *ns* binds to the user ns; in-ns re-binds it. use/require cross-ns
switching stays deferred to Phase 3 (the analyzer bakes a def's target ns at
compile time).

Prelude parity 1951 -> 1969, 0 new divergences; four now-passing allowlist
entries dropped (ns *ns* cases + str-of-a-var). New focused gate test/chez/
_ns.janet (19 cases, expectations from the JVM-canonical build/jolt).
This commit is contained in:
Yogthos 2026-06-18 16:49:35 -04:00
parent eb26ad0401
commit 32e2d8bd58
6 changed files with 247 additions and 10 deletions

View file

@ -19,7 +19,8 @@
(array/push parts (slurp (string "jolt-core/clojure/core/" tf ".clj")))) (array/push parts (slurp (string "jolt-core/clojure/core/" tf ".clj"))))
(each f ["host/chez/emit.janet" "host/chez/driver.janet" "host/chez/rt.ss" (each f ["host/chez/emit.janet" "host/chez/driver.janet" "host/chez/rt.ss"
"host/chez/values.ss" "host/chez/collections.ss" "host/chez/seq.ss" "host/chez/values.ss" "host/chez/collections.ss" "host/chez/seq.ss"
"host/chez/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss"] "host/chez/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss"
"host/chez/ns.ss" "host/chez/post-prelude.ss"]
(array/push parts (slurp f))) (array/push parts (slurp f)))
(string/slice (string (hash (string/join parts))) 0)) (string/slice (string (hash (string/join parts))) 0))

164
host/chez/ns.ss Normal file
View file

@ -0,0 +1,164 @@
;; namespaces (jolt-yxqm, Phase 2) — the namespace value model.
;;
;; Chez has no ctx, so the ctx-coupled seed natives (find-ns/resolve/in-ns/…) are
;; reimplemented over the rt.ss var-table (cells carry ns + name + defined?) and
;; the multimethods.ss chez-current-ns box. A namespace VALUE is a `jns` record
;; carrying its name string — distinct from a map/record so (map? ns) is #f, but
;; the overlay's `ns-name` reads (get ns :name); that's overridden natively in
;; post-prelude.ss (loads after the overlay clobbers it).
;;
;; Loaded LAST from rt.ss. SCOPE (jolt-yxqm): the read/resolve/in-ns/*ns* ops.
;; use/require cross-ns SWITCHING is deferred (Phase 3) — the analyzer bakes a
;; def's target ns at compile time, so a runtime in-ns can't redirect later defs.
(define-record-type jns (fields name) (nongenerative chez-jns-v1))
;; registry: name-string -> jns. Seeded with the two always-present namespaces;
;; grown by in-ns / create-ns. find-ns ALSO derives existence from the var-table
;; (any cell with that ns), so a namespace that only ever had vars def'd into it
;; is still found.
(define ns-registry (make-hashtable string-hash string=?))
(define (intern-ns! name)
(or (hashtable-ref ns-registry name #f)
(let ((n (make-jns name))) (hashtable-set! ns-registry name n) n)))
(intern-ns! "user")
(intern-ns! "clojure.core")
;; a namespace designator -> its name string (a jns or a symbol; the corpus never
;; passes a bare string).
(define (ns-desig->name d)
(if (jns? d) (jns-name d) (symbol-t-name d)))
(define (ns-has-vars? nm)
(let ((found #f))
(vector-for-each
(lambda (c) (when (and (not found) (string=? (var-cell-ns c) nm)) (set! found #t)))
(hashtable-values var-table))
found))
(define (jolt-find-ns desig)
(let ((nm (ns-desig->name desig)))
(or (hashtable-ref ns-registry nm #f)
(and (ns-has-vars? nm) (intern-ns! nm))
jolt-nil)))
(define (jolt-the-ns desig)
(if (jns? desig) desig
(let ((n (jolt-find-ns desig)))
(if (jns? n) n (error #f "No namespace" desig)))))
(define (jolt-create-ns desig) (intern-ns! (ns-desig->name desig)))
;; in-ns: register + switch the current ns + re-bind *ns* + return the jns. NOTE
;; (Phase-3 deferral): this updates only the RUNTIME current ns — subsequent defs
;; in the same program were already ns-baked by the analyzer, so it does not
;; redirect them. It is enough for *ns* / str-of-ns to track the switch.
(define (jolt-in-ns desig)
(let* ((nm (ns-desig->name desig)) (n (intern-ns! nm)))
(set-chez-ns! nm)
(def-var! "clojure.core" "*ns*" n)
n))
;; ns-name: a namespace's name as a (no-ns) symbol. Overrides the overlay (which
;; reads (get ns :name) = nil on a jns record) — wired in via post-prelude.ss.
(define (jolt-ns-name desig)
(jolt-symbol #f (jns-name (jolt-the-ns desig))))
(define (jolt-all-ns)
(let ((seen (make-hashtable string-hash string=?)))
(vector-for-each (lambda (k) (hashtable-set! seen k #t)) (hashtable-keys ns-registry))
(vector-for-each (lambda (c) (hashtable-set! seen (var-cell-ns c) #t)) (hashtable-values var-table))
(list->cseq (map intern-ns! (vector->list (hashtable-keys seen))))))
;; ns-publics / ns-map / ns-interns: a {sym -> var-cell} jolt map built by scanning
;; the var-table for defined cells in the namespace. (Private vars are not tracked
;; yet, so ns-publics == ns-interns.) ns-aliases is an empty map (map? is true).
(define (ns-vars-pmap nm)
(let ((m (jolt-hash-map)))
(vector-for-each
(lambda (c)
(when (and (string=? (var-cell-ns c) nm) (var-cell-defined? c))
(set! m (jolt-assoc m (jolt-symbol #f (var-cell-name c)) c))))
(hashtable-values var-table))
m))
(define (jolt-ns-publics desig) (ns-vars-pmap (ns-desig->name desig)))
(define (jolt-ns-aliases desig) (jolt-hash-map))
;; resolve: an unqualified symbol resolves in the current ns then clojure.core; a
;; qualified one in its own ns. Returns the var iff genuinely defined, else nil —
;; never interns an empty cell (var-cell-lookup is non-creating).
(define (jolt-resolve sym)
(let* ((sns (symbol-t-ns sym)) (nm (symbol-t-name sym))
(c (if (string? sns)
(var-cell-lookup sns nm)
(or (var-cell-lookup (chez-current-ns) nm)
(var-cell-lookup "clojure.core" nm)))))
(if (and c (var-cell-defined? c)) c jolt-nil)))
(define (jolt-find-var sym)
(let ((sns (symbol-t-ns sym)) (nm (symbol-t-name sym)))
(if (string? sns)
(let ((c (var-cell-lookup sns nm))) (if (and c (var-cell-defined? c)) c jolt-nil))
(error #f "find-var requires a fully-qualified symbol" sym))))
;; ns-unmap: clear the mapping — drop defined? and reset the root to unbound, so a
;; later resolve returns nil.
(define (jolt-ns-unmap ns-desig sym)
(let ((c (var-cell-lookup (ns-desig->name ns-desig) (symbol-t-name sym))))
(when c (var-cell-defined?-set! c #f) (var-cell-root-set! c jolt-unbound)))
jolt-nil)
;; --- RESOLVE FRICTION: native-op cells -------------------------------------
;; Native-op primitives (+ map reduce …) are INLINED at emit, so they have no
;; var-cell and (resolve '+) would be nil — diverging from Clojure where it is a
;; var. def-var! each to its value-position procedure so it has a real, defined
;; cell (calls still inline, so no perf hit; #'+ deref and ((resolve '+) 1 2) also
;; work now). The clojure.core prelude, loaded AFTER rt.ss, overwrites the cells
;; for names it also defines in the overlay (map/filter/…); the purely-inlined
;; scalars (+/-/</inc/…) keep these.
(for-each
(lambda (p) (def-var! "clojure.core" (car p) (cdr p)))
(list
(cons "+" jolt-add) (cons "-" jolt-sub) (cons "*" jolt-mul) (cons "/" jolt-div)
(cons "<" <) (cons ">" >) (cons "<=" <=) (cons ">=" >=)
(cons "=" jolt=) (cons "inc" jolt-inc) (cons "dec" jolt-dec) (cons "not" jolt-not)
(cons "min" min) (cons "max" max)
(cons "mod" modulo) (cons "rem" remainder) (cons "quot" quotient)
(cons "vector" jolt-vector) (cons "hash-map" jolt-hash-map) (cons "hash-set" jolt-hash-set)
(cons "conj" jolt-conj) (cons "get" jolt-get) (cons "nth" jolt-nth) (cons "count" jolt-count)
(cons "assoc" jolt-assoc) (cons "dissoc" jolt-dissoc) (cons "contains?" jolt-contains?)
(cons "empty?" jolt-empty?) (cons "peek" jolt-peek) (cons "pop" jolt-pop)
(cons "first" jolt-first) (cons "rest" jolt-rest) (cons "next" jolt-next) (cons "seq" jolt-seq)
(cons "cons" jolt-cons) (cons "list" jolt-list) (cons "reverse" jolt-reverse) (cons "last" jolt-last)
(cons "map" jolt-map) (cons "filter" jolt-filter) (cons "remove" jolt-remove)
(cons "reduce" jolt-reduce) (cons "into" jolt-into) (cons "concat" jolt-concat) (cons "apply" jolt-apply)
(cons "range" jolt-range) (cons "take" jolt-take) (cons "drop" jolt-drop)
(cons "keys" jolt-keys) (cons "vals" jolt-vals)
(cons "even?" jolt-even?) (cons "odd?" jolt-odd?) (cons "pos?" jolt-pos?) (cons "neg?" jolt-neg?)
(cons "zero?" jolt-zero?) (cons "identity" jolt-identity)
(cons "ex-info" jolt-ex-info)))
;; --- bindings + *ns* --------------------------------------------------------
(def-var! "clojure.core" "find-ns" jolt-find-ns)
(def-var! "clojure.core" "the-ns" jolt-the-ns)
(def-var! "clojure.core" "create-ns" jolt-create-ns)
(def-var! "clojure.core" "in-ns" jolt-in-ns)
(def-var! "clojure.core" "all-ns" jolt-all-ns)
(def-var! "clojure.core" "ns-publics" jolt-ns-publics)
(def-var! "clojure.core" "ns-map" jolt-ns-publics)
(def-var! "clojure.core" "ns-interns" jolt-ns-publics)
(def-var! "clojure.core" "ns-aliases" jolt-ns-aliases)
(def-var! "clojure.core" "resolve" jolt-resolve)
(def-var! "clojure.core" "find-var" jolt-find-var)
(def-var! "clojure.core" "ns-unmap" jolt-ns-unmap)
;; *ns* starts at the user namespace (the current ns for -e user code). in-ns
;; re-binds it. (ns-name is overridden natively in post-prelude.ss.)
(def-var! "clojure.core" "*ns*" (intern-ns! "user"))
;; --- printer patches: a namespace renders as its name (str / pr-str / -e) ----
(define %ns-pr-str jolt-pr-str)
(set! jolt-pr-str (lambda (x) (if (jns? x) (jns-name x) (%ns-pr-str x))))
(define %ns-pr-readable jolt-pr-readable)
(set! jolt-pr-readable (lambda (x) (if (jns? x) (jns-name x) (%ns-pr-readable x))))
(define %ns-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (jns? x) (jns-name x) (%ns-str-render-one x))))

View file

@ -28,3 +28,6 @@
(def-var! "clojure.core" "random-uuid" jolt-random-uuid) (def-var! "clojure.core" "random-uuid" jolt-random-uuid)
(def-var! "clojure.core" "parse-uuid" jolt-parse-uuid) (def-var! "clojure.core" "parse-uuid" jolt-parse-uuid)
(def-var! "clojure.core" "tagged-literal?" jolt-tagged-literal-pred?) (def-var! "clojure.core" "tagged-literal?" jolt-tagged-literal-pred?)
;; ns-name: the overlay reads (get ns :name) — nil on a jns namespace record.
;; Native version (defined in ns.ss) returns the namespace's name symbol.
(def-var! "clojure.core" "ns-name" jolt-ns-name)

View file

@ -61,27 +61,34 @@
;; until the real def overwrites it (a forward reference resolves to the cell, and ;; until the real def overwrites it (a forward reference resolves to the cell, and
;; correct code never reads it before the binding def runs). ;; correct code never reads it before the binding def runs).
(define jolt-unbound (string->symbol "#<jolt-unbound>")) (define jolt-unbound (string->symbol "#<jolt-unbound>"))
(define-record-type var-cell (fields ns name (mutable root)) (nongenerative var-cell-v1)) ;; `defined?` distinguishes a genuinely interned var (def / declare / a native-op
;; cell) from a cell lazily materialised by a forward `var-deref` / `(var x)` on a
;; not-yet-defined name — `resolve` returns the cell iff defined? (jolt-yxqm).
;; ns-unmap clears it. Avoids the (def x nil) edge of probing the root.
(define-record-type var-cell (fields ns name (mutable root) (mutable defined?)) (nongenerative var-cell-v2))
(define var-table (make-hashtable string-hash string=?)) (define var-table (make-hashtable string-hash string=?))
(define (jolt-var ns name) (define (jolt-var ns name)
(let ((k (string-append ns "/" name))) (let ((k (string-append ns "/" name)))
(or (hashtable-ref var-table k #f) (or (hashtable-ref var-table k #f)
(let ((c (make-var-cell ns name jolt-nil))) (let ((c (make-var-cell ns name jolt-nil #f)))
(hashtable-set! var-table k c) (hashtable-set! var-table k c)
c)))) c))))
;; non-creating lookup (resolve / find-var / ns-unmap): #f when absent, so a
;; probe never interns an empty cell.
(define (var-cell-lookup ns name) (hashtable-ref var-table (string-append ns "/" name) #f))
(define (var-deref ns name) (var-cell-root (jolt-var ns name))) (define (var-deref ns name) (var-cell-root (jolt-var ns name)))
;; def-var! / declare-var! return the VAR CELL, not the value — Clojure's `def` ;; def-var! / declare-var! return the VAR CELL, not the value — Clojure's `def`
;; evaluates to #'ns/name (a first-class var), so (var? (def x 1)) is true and ;; evaluates to #'ns/name (a first-class var), so (var? (def x 1)) is true and
;; (pr-str (def x 1)) is "#'ns/x". The prelude's def-var! forms discard the ;; (pr-str (def x 1)) is "#'ns/x". The prelude's def-var! forms discard the
;; return, so this is transparent there. ;; return, so this is transparent there.
(define (def-var! ns name v) (let ((c (jolt-var ns name))) (var-cell-root-set! c v) c)) (define (def-var! ns name v) (let ((c (jolt-var ns name))) (var-cell-root-set! c v) (var-cell-defined?-set! c #t) c))
;; declare / (def name) with no init: reserve the cell ONLY if absent. An ;; declare / (def name) with no init: reserve the cell ONLY if absent. An
;; existing root is left intact — Clojure's (def x) with no init does not clobber ;; existing root is left intact — Clojure's (def x) with no init does not clobber
;; a prior binding (do (def x 7) (def x) x) => 7. Returns the cell either way. ;; a prior binding (do (def x 7) (def x) x) => 7. Returns the cell either way.
(define (declare-var! ns name) (define (declare-var! ns name)
(let ((k (string-append ns "/" name))) (let ((k (string-append ns "/" name)))
(or (hashtable-ref var-table k #f) (or (hashtable-ref var-table k #f)
(let ((c (make-var-cell ns name jolt-unbound))) (let ((c (make-var-cell ns name jolt-unbound #t))) ; declared => interned/resolvable
(hashtable-set! var-table k c) (hashtable-set! var-table k c)
c)))) c))))
@ -234,3 +241,10 @@
;; a uuid). Overlay names (uuid?/random-uuid/parse-uuid/tagged-literal?) re-asserted ;; a uuid). Overlay names (uuid?/random-uuid/parse-uuid/tagged-literal?) re-asserted
;; in post-prelude.ss. ;; in post-prelude.ss.
(load "host/chez/natives-misc.ss") (load "host/chez/natives-misc.ss")
;; namespaces (jolt-yxqm, Phase 2): the namespace value model — find-ns/ns-name/
;; all-ns/the-ns/create-ns/in-ns/ns-publics/ns-map/ns-interns/ns-aliases/resolve/
;; find-var/ns-unmap/*ns*, over the var-table + chez-current-ns. Loaded LAST: needs
;; var-cell + var-cell-defined?, jolt-symbol/jolt-hash-map/jolt-assoc, chez-current-ns
;; (multimethods.ss), list->cseq (seq.ss), and the fully-patched printers (vars.ss).
(load "host/chez/ns.ss")

54
test/chez/_ns.janet Normal file
View file

@ -0,0 +1,54 @@
# jolt-yxqm — namespace value model (find-ns/ns-name/all-ns/resolve/ns-publics/
# in-ns/*ns* …). TDD harness: drives bin/jolt-chez -e per case (fresh subprocess
# = per-case isolation), checks the LAST printed line == expected. Expected
# values are the JVM-canonical reference (build/jolt), captured up front.
#
# janet test/chez/_ns.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/jolt-chez"))
# [label expr expected-last-line]
(def cases
[["find-ns existing" "(some? (find-ns 'clojure.core))" "true"]
["find-ns missing" "(nil? (find-ns 'does.not.exist))" "true"]
["resolve native-op +" "(var? (resolve '+))" "true"]
["resolve undefined" "(nil? (resolve 'totally-undefined-xyz))" "true"]
["ns-publics has def" "(do (def npv 1) (some? (get (ns-publics 'user) 'npv)))" "true"]
["ns-map has def" "(do (def nmv 1) (some? (get (ns-map 'user) 'nmv)))" "true"]
["ns-aliases is a map" "(map? (ns-aliases 'clojure.core))" "true"]
["ns-interns is a map" "(map? (ns-interns 'user))" "true"]
["ns-interns count pos" "(do (def q 1) (pos? (count (ns-interns 'user))))" "true"]
["all-ns count pos" "(pos? (count (all-ns)))" "true"]
["ns-name *ns* = user" "(= (ns-name *ns*) (ns-name (find-ns 'user)))" "true"]
["in-ns returns ns str" "(str (in-ns 'jolt.test-ns-b))" "jolt.test-ns-b"]
["in-ns updates *ns*" "(do (in-ns 'jolt.test-ns-a) (str *ns*))" "jolt.test-ns-a"]
["ns-unmap clears var" "(do (def nuv 1) (ns-unmap 'user 'nuv) (nil? (resolve 'nuv)))" "true"]
["in-ns no error" "(do (in-ns 'my.ns) (symbol? 'x))" "true"]
["str ns-name *ns*" "(str (ns-name *ns*))" "user"]
["find-var qualified" "(var? (find-var 'clojure.core/map))" "true"]
["the-ns ns-name" "(= 'user (ns-name (the-ns 'user)))" "true"]
["create-ns ns-name" "(= 'foo.bar (ns-name (create-ns 'foo.bar)))" "true"]])
(defn run-capture [expr]
(def proc (os/spawn [jolt-bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (if err (string err) "")])
(var pass 0)
(def fails @[])
(each [label expr expected] cases
(def [code got err] (run-capture expr))
(cond
(not= code 0) (array/push fails [label (string "exit " code "; err: " (string/trim err))])
(= got expected) (++ pass)
(array/push fails [label (string "want `" expected "`, got `" got "`")])))
(printf "\n_ns parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [l m] fails (printf " FAIL [%s] %s" l m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -49,10 +49,8 @@
"keys evaluate before their values, pairwise" true "keys evaluate before their values, pairwise" true
"source order with a nil value (phm form)" true "source order with a nil value (phm form)" true
"close on throw" true "close on throw" true
"ns-name of *ns*" true # *ns* now a namespace value (jolt-yxqm): str/ns-name of *ns* + the var str
"str of *ns*" true # case ("ns-name of *ns*" / "str of *ns*" / "*ns* user" / "str of a var") pass.
"*ns* user" true
"str of a var" true
# *clojure-version* / *unchecked-math* now bound by dynamic-vars.ss (inc 3r) # *clojure-version* / *unchecked-math* now bound by dynamic-vars.ss (inc 3r)
# (str [##Inf]) wants "[Infinity]" but the Chez -e printer (jolt-pr-str, which # (str [##Inf]) wants "[Infinity]" but the Chez -e printer (jolt-pr-str, which
# str falls back to for collections) renders inf as "inf" — str needs a # str falls back to for collections) renders inf as "inf" — str needs a
@ -189,8 +187,11 @@
# matching Clojure, which also un-allowlists pr-str-of-var/defn) and inc J # matching Clojure, which also un-allowlists pr-str-of-var/defn) and inc J
# (jolt-snry: scalar natives — UUID random-uuid/parse-uuid/uuid?, format/printf, # (jolt-snry: scalar natives — UUID random-uuid/parse-uuid/uuid?, format/printf,
# tagged-literal, bigint) 1951. # tagged-literal, bigint) 1951.
# jolt-yxqm (namespace value model — find-ns/ns-name/all-ns/resolve/ns-publics/
# in-ns/*ns* over the var-table + a jns ns value; native-op var cells so
# (resolve '+) is a var; *ns* bound to the user ns) 1969.
# Strided runs scale down. # Strided runs scale down.
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "1951"))) (def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "1969")))
(def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor)) (def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor))
(when (or (> (length diverged) 0) (< pass floor)) (when (or (> (length diverged) 0) (< pass floor))
(printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged))) (printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged)))