core.logic constraint layer: fixes for the CLP/unifier failures

Follow-on to the core.logic relational-engine work. These clear every crash in
core.logic's constraint-logic-programming and unifier layers (33 errors -> 0) and
most of the value mismatches; the suite goes 504 -> 523 passing assertions. All
are general gaps, not core.logic-specific.

- symbols intern their ns/name strings (JVM Symbol.intern .intern()s them): two
  separately-read `?a` symbols now share one name-string object. core.logic's
  non-unique lvars compare names by identity (via (str sym)), so without this a
  term's lvar and a constraint's lvar built from different `?a` reads never matched
  and constraints silently never fired.
- (str x) of a single arg returns its rendering directly instead of copying through
  string-append, and a symbol stringifies to its (interned) name — JVM (str x) is
  x.toString(). Needed for the identity comparison above.
- a clojure.core-qualified special form dispatches correctly: syntax-quote
  namespace-qualifies a macro like letfn to clojure.core/letfn (matching Clojure,
  where it's a macro), and the analyzer now maps that back to the special form
  instead of treating it as an invoke of a nil var. core.logic's fnc/defnc emit
  (clojure.core/letfn ...). Re-mint.
- (disj nil ...) is nil (JVM), instead of crashing in the set path — core.logic's
  constraint store does (disj (get km v) id) where the get can be nil.

corpus.edn: 4 JVM-certified rows. make test + shakesmoke green, 0 new divergences,
self-host fixpoint holds.
This commit is contained in:
Yogthos 2026-06-27 10:37:32 -04:00
parent 36105ba702
commit e6aa2aace7
6 changed files with 56 additions and 12 deletions

View file

@ -27,6 +27,15 @@
((and (flonum? v) (fl= v +inf.0)) "Infinity") ((and (flonum? v) (fl= v +inf.0)) "Infinity")
((and (flonum? v) (fl= v -inf.0)) "-Infinity") ((and (flonum? v) (fl= v -inf.0)) "-Infinity")
((and (flonum? v) (not (fl= v v))) "NaN") ((and (flonum? v) (not (fl= v v))) "NaN")
;; a symbol stringifies to its name (JVM Symbol.toString returns the interned
;; name), so (str sym) of a no-ns symbol is the SAME string object the symbol
;; holds — code that compares those by identity (core.logic's non-unique lvar
;; equality) depends on it.
((symbol-t? v)
(let ((ns (symbol-t-ns v)))
(if (or (not ns) (jolt-nil? ns))
(symbol-t-name v)
(string-append ns "/" (symbol-t-name v)))))
(else (else
(let loop ((rs str-render-registry)) (let loop ((rs str-render-registry))
(cond (cond
@ -49,10 +58,17 @@
(jolt-pr-readable v) (jolt-pr-readable v)
(jolt-str-render-one v))) (jolt-str-render-one v)))
(define (jolt-str . xs) (define (jolt-str . xs)
(let loop ((xs xs) (acc '())) (cond
(if (null? xs) ((null? xs) "")
(apply string-append (reverse acc)) ;; single arg returns its rendering directly (no string-append copy), so
(loop (cdr xs) (cons (jolt-str-one (car xs)) acc))))) ;; (str sym) hands back the symbol's own name string — JVM (str x) is
;; x.toString(), and core.logic's non-unique lvar equality compares those by
;; identity.
((null? (cdr xs)) (jolt-str-one (car xs)))
(else (let loop ((xs xs) (acc '()))
(if (null? xs)
(apply string-append (reverse acc))
(loop (cdr xs) (cons (jolt-str-one (car xs)) acc)))))))
;; jolt indices are flonums; substring etc. need exact ints. ;; jolt indices are flonums; substring etc. need exact ints.
(define (jolt->idx n) (exact (truncate n))) (define (jolt->idx n) (exact (truncate n)))

File diff suppressed because one or more lines are too long

View file

@ -171,8 +171,11 @@
;; persistent disj over sets (pset-disj already exists in collections.ss). ;; persistent disj over sets (pset-disj already exists in collections.ss).
(define (jolt-disj s . xs) (define (jolt-disj s . xs)
(meta-carry s ;; (disj nil ...) is nil on the JVM (disj is otherwise set-only).
(let loop ((s s) (xs xs)) (if (null? xs) s (loop (pset-disj s (car xs)) (cdr xs)))))) (if (jolt-nil? s)
jolt-nil
(meta-carry s
(let loop ((s s) (xs xs)) (if (null? xs) s (loop (pset-disj s (car xs)) (cdr xs)))))))
;; --- see-through accessors --------------------------------------------------- ;; --- see-through accessors ---------------------------------------------------
(define (tvec-in-bounds? t i) (and (fixnum? i) (fx>=? i 0) (fx<? i (jolt-transient-n t)))) (define (tvec-in-bounds? t i) (and (fixnum? i) (fx>=? i 0) (fx<? i (jolt-transient-n t))))

View file

@ -47,9 +47,21 @@
(define (keyword? x) (keyword-t? x)) (define (keyword? x) (keyword-t? x))
;; --- symbols: ns + name + meta; NOT interned (meta varies), = by ns/name ------ ;; --- symbols: ns + name + meta; NOT interned (meta varies), = by ns/name ------
;; The ns/name STRINGS are pooled (like JVM Symbol.intern, which .intern()s them):
;; two separately-read `?a` symbols share one name-string object, so code that
;; compares symbol names by identity (core.logic's non-unique lvar equality, via
;; (str sym)) behaves like the JVM.
(define symbol-string-pool (make-hashtable string-hash string=?))
(define (intern-symbol-string s)
(if (string? s)
(or (hashtable-ref symbol-string-pool s #f)
(begin (hashtable-set! symbol-string-pool s s) s))
s))
(define-record-type symbol-t (fields ns name meta) (nongenerative symbol-v1)) (define-record-type symbol-t (fields ns name meta) (nongenerative symbol-v1))
(define (jolt-symbol ns name) (make-symbol-t ns name jolt-nil)) (define (jolt-symbol ns name)
(define (jolt-symbol/meta ns name meta) (make-symbol-t ns name meta)) (make-symbol-t (intern-symbol-string ns) (intern-symbol-string name) jolt-nil))
(define (jolt-symbol/meta ns name meta)
(make-symbol-t (intern-symbol-string ns) (intern-symbol-string name) meta))
(define (jolt-symbol? x) (symbol-t? x)) (define (jolt-symbol? x) (symbol-t? x))
;; chars/strings: Chez natives (strings treated immutable). ;; chars/strings: Chez natives (strings treated immutable).

View file

@ -611,6 +611,15 @@
(quote-node form) (quote-node form)
(let [head (first items) (let [head (first items)
hname (when (and (form-sym? head) (nil? (form-sym-ns head))) (form-sym-name head)) hname (when (and (form-sym? head) (nil? (form-sym-ns head))) (form-sym-name head))
;; a special-form head may arrive clojure.core-qualified: syntax-quote
;; namespace-qualifies a macro like `letfn` to `clojure.core/letfn`
;; (matching Clojure, where it is a macro), so a macro-emitted
;; (clojure.core/letfn …) must still dispatch to the special form.
sf-name (or hname
(when (and (form-sym? head)
(= "clojure.core" (form-sym-ns head))
(contains? handled (form-sym-name head)))
(form-sym-name head)))
shadowed (and hname (local? env hname))] shadowed (and hname (local? env hname))]
(cond (cond
;; Canonical order (Clojure/CLJS analyze-seq): macroexpand FIRST, then ;; Canonical order (Clojure/CLJS analyze-seq): macroexpand FIRST, then
@ -619,7 +628,7 @@
;; the reference macroexpand1's isSpecial check — so a ns that redefs a ;; the reference macroexpand1's isSpecial check — so a ns that redefs a
;; macro `def`/`and`/`or` (clojure.spec.alpha) keeps the special form `def`. ;; macro `def`/`and`/`or` (clojure.spec.alpha) keeps the special form `def`.
(and (form-sym? head) (not shadowed) (and (form-sym? head) (not shadowed)
(not (contains? handled hname)) (form-macro? ctx head)) (not (contains? handled sf-name)) (form-macro? ctx head))
;; defn/defn- expand to (def name (fn …)); carry the ORIGINAL form's ;; defn/defn- expand to (def name (fn …)); carry the ORIGINAL form's
;; source offset onto the resulting def, since the macro builds a fresh ;; source offset onto the resulting def, since the macro builds a fresh
;; (def …) with no metadata. So the back end can register fn defs. ;; (def …) with no metadata. So the back end can register fn defs.
@ -639,10 +648,10 @@
;; special-form heads are NOT shadowable (unlike macros): a local named ;; special-form heads are NOT shadowable (unlike macros): a local named
;; `if` does not change the meaning of (if …) in operator position, per ;; `if` does not change the meaning of (if …) in operator position, per
;; spec §3 and the reference. No (not shadowed) guard here. ;; spec §3 and the reference. No (not shadowed) guard here.
(and hname (contains? handled hname)) (and sf-name (contains? handled sf-name))
;; stamp the form's source offset onto a top-level def so the back end ;; stamp the form's source offset onto a top-level def so the back end
;; can register it (jv$ns$name -> source) for native stack traces. ;; can register it (jv$ns$name -> source) for native stack traces.
(let [node (analyze-special ctx hname items env) (let [node (analyze-special ctx sf-name items env)
p (form-position form)] p (form-position form)]
(if (and p (= :def (:op node))) (assoc node :pos p) node)) (if (and p (= :def (:op node))) (assoc node :pos p) node))
(and hname (not shadowed) (method-head? hname)) (and hname (not shadowed) (method-head? hname))

View file

@ -3343,4 +3343,8 @@
{:suite "host interop / Collection.contains" :label "value membership over vector/list/set" :expected "[true false true]" :actual "[(.contains [1 2 3] 2) (.contains (list :a :b) :z) (.contains #{1 2} 1)]"} {:suite "host interop / Collection.contains" :label "value membership over vector/list/set" :expected "[true false true]" :actual "[(.contains [1 2 3] 2) (.contains (list :a :b) :z) (.contains #{1 2} 1)]"}
{:suite "host interop / clojure.lang.Util" :label "Util/hash is Java hashCode" :expected "120" :actual "(clojure.lang.Util/hash \"x\")"} {:suite "host interop / clojure.lang.Util" :label "Util/hash is Java hashCode" :expected "120" :actual "(clojure.lang.Util/hash \"x\")"}
{:suite "deftype / IObj metadata" :label "deftype meta/withMeta govern (meta x)" :expected "{:a 1}" :actual "(do (deftype Sm [m] clojure.lang.IObj (meta [_] m) (withMeta [_ n] (Sm. n))) (meta (Sm. {:a 1})))"} {:suite "deftype / IObj metadata" :label "deftype meta/withMeta govern (meta x)" :expected "{:a 1}" :actual "(do (deftype Sm [m] clojure.lang.IObj (meta [_] m) (withMeta [_ n] (Sm. n))) (meta (Sm. {:a 1})))"}
{:suite "collections / disj" :label "disj of nil is nil" :expected "nil" :actual "(disj nil :a)"}
{:suite "special forms / qualified" :label "clojure.core-qualified special form (from syntax-quote)" :expected "[:g 9]" :actual "(clojure.core/letfn [(g [x] [:g x])] (g 9))"}
{:suite "symbols / interning" :label "(str sym) is the symbol's interned name (identity-stable)" :expected "[true true]" :actual "(let [s (quote ?a)] [(identical? (name s) (str s)) (= (str s) \"?a\")])"}
{:suite "symbols / interning" :label "equal symbols share an interned name string" :expected "true" :actual "(let [a (quote ?foo) b (quote ?foo)] (identical? (name a) (name b)))"}
] ]