diff --git a/host/chez/driver.janet b/host/chez/driver.janet index 9db14cd..0404da0 100644 --- a/host/chez/driver.janet +++ b/host/chez/driver.janet @@ -29,8 +29,13 @@ (and (r 0) (zero? (r 1)))) (defn make-ctx [] - "A compile-mode jolt ctx (the analyzer pipeline is only built under :compile?)." - (api/init {:compile? true})) + "A compile-mode jolt ctx (the analyzer pipeline is only built under :compile?). + Late-bind unresolved symbols: the Chez back end has no interpreter to punt to, + so a forward reference to a runtime-interned var (defmulti/defmethod's setup + call) lowers to a var-deref instead of failing to compile (jolt-9ls5)." + (def ctx (api/init {:compile? true})) + (put (get ctx :env) :late-bind-unresolved? true) + ctx) (defn- parse-all [src] (def out @[]) @@ -138,10 +143,15 @@ (string "(import (chezscheme))\n" "(load \"host/chez/rt.ss\")\n" + # the prelude's defmultis (print-method/print-dup) must land in clojure.core, + # not the default user ns (jolt-9ls5); set the multimethod current-ns around + # the prelude load, then restore it to user for the program form. + "(set-chez-ns! \"clojure.core\")\n" "(load " (string/format "%j" prelude-path) ")\n" # native-wins overrides for overlay predicates that read :jolt/type (char?, # atom?) — must load AFTER the prelude's own def-var! to take effect. "(load \"host/chez/post-prelude.ss\")\n" + "(set-chez-ns! \"user\")\n" "(printf \"~a\\n\" (jolt-final-str " final-scm "))\n")) (defn eval-e-with-prelude diff --git a/host/chez/emit.janet b/host/chez/emit.janet index d737a9f..266dfa0 100644 --- a/host/chez/emit.janet +++ b/host/chez/emit.janet @@ -331,6 +331,13 @@ # keyword/coll/fn) -> dynamic IFn dispatch. Excludes the named-fn self-call. (and (= :local (get fnode :op)) (not (get known-procs (munge (get fnode :name))))) (string "(jolt-invoke " (emit fnode) " " (string/join args " ") ")") + # a late-bound :var call head can hold a plain procedure OR a non-applicable + # value the RT dispatches (a multimethod record, a keyword/coll IFn) — route it + # through jolt-invoke so all of those work. Transparent for a procedure + # (jolt-invoke just applies it); the hot self-recursive call is a :local + # known-proc above, so it stays a direct call. + (= :var (get fnode :op)) + (string "(jolt-invoke " (emit fnode) " " (string/join args " ") ")") (string "(" (emit fnode) " " (string/join args " ") ")"))) (set emit (fn emit [node] diff --git a/host/chez/jolt-chez.janet b/host/chez/jolt-chez.janet index 8d706eb..b4b453f 100644 --- a/host/chez/jolt-chez.janet +++ b/host/chez/jolt-chez.janet @@ -39,6 +39,9 @@ (os/exit 2)) (def src (in args 1)) (def ctx (api/init-cached {:compile? true})) + # late-bind unresolved symbols (no interpreter to punt to) so defmulti/defmethod + # forward references lower to a var-deref (jolt-9ls5), matching d/make-ctx. + (put (get ctx :env) :late-bind-unresolved? true) (def prelude-path (ensure-prelude ctx)) (def [code out err] (d/eval-e-with-prelude ctx src prelude-path)) (when (= code :emit-err) diff --git a/host/chez/multimethods.ss b/host/chez/multimethods.ss new file mode 100644 index 0000000..be04c1a --- /dev/null +++ b/host/chez/multimethods.ss @@ -0,0 +1,183 @@ +;; multimethods (jolt-9ls5) — the multimethod dispatch runtime on the Chez host. +;; +;; defmulti/defmethod are macros that expand to ctx-capturing setup CALLS +;; (defmulti-setup / defmethod-setup, + the table ops get-method/methods/ +;; remove-method/prefer-method/prefers). The Janet seed implements these against +;; the evaluator's ns/var machinery (eval_runtime.janet); this is the Chez port. +;; +;; A multimethod VALUE is a jolt-multifn record carrying its dispatch fn and a +;; mutable method table (dispatch-val -> method fn, keyed with jolt= so keyword/ +;; vector/number dispatch values match by value). jolt-invoke dispatches it: +;; an exact method, else an isa?/hierarchy match (resolved through prefer-method +;; and the overlay's isa?/derive/hierarchy), else the :default method. +;; +;; NS resolution: defmulti expands to (defmulti-setup (quote name) ...) with a +;; BARE symbol — the Chez RT has no compile-time current ns at the call site, so a +;; runtime `chez-current-ns` box names where to def-var! the multifn. It defaults +;; to "user" (matching the analyzer's ns for -e user code); the assembled prelude +;; sets it to "clojure.core" around its own load (program-with-prelude), so the +;; print-method/print-dup defmultis land in clojure.core. defmethod-setup and the +;; symbol-taking table ops resolve the multifn via (var-deref (chez-current-ns) …), +;; so they agree with defmulti. Loaded from rt.ss after seq.ss (jolt-invoke), +;; collections.ss (jolt=/key-hash/jolt-hash-map) and the var-cell machinery. + +(define chez-current-ns-box (vector "user")) +(define (chez-current-ns) (vector-ref chez-current-ns-box 0)) +(define (set-chez-ns! ns) (vector-set! chez-current-ns-box 0 ns)) + +(define-record-type jolt-multifn + (fields name dispatch-fn methods default hierarchy prefers) + (nongenerative jolt-multifn-v1)) + +(define kw-default (keyword #f "default")) +(define (new-mm-table) (make-hashtable key-hash jolt=)) + +;; (defmulti-setup 'name dispatch & opts) — opts is a flat :default/:hierarchy plist. +(define (parse-mm-opts opts) + (let loop ((o opts) (dk kw-default) (h #f)) + (if (or (null? o) (null? (cdr o))) + (values dk h) + (let ((k (car o)) (v (cadr o))) + (cond + ((and (keyword? k) (not (keyword-t-ns k)) (string=? (keyword-t-name k) "default")) + (loop (cddr o) v h)) + ((and (keyword? k) (not (keyword-t-ns k)) (string=? (keyword-t-name k) "hierarchy")) + (loop (cddr o) dk v)) + (else (loop (cddr o) dk h))))))) + +(define (jolt-defmulti-setup name-sym dispatch . opts) + (let-values (((dk h) (parse-mm-opts opts))) + (let ((mf (make-jolt-multifn (symbol-t-name name-sym) dispatch + (new-mm-table) dk h (new-mm-table)))) + (def-var! (chez-current-ns) (symbol-t-name name-sym) mf) + mf))) + +;; (defmethod-setup 'mm dispatch-val impl) — add a method. Auto-creates the multifn +;; if absent (defmethod before defmulti — rare; identity dispatch as a fallback). +(define (jolt-defmethod-setup mm-sym dval impl) + (let* ((nm (symbol-t-name mm-sym)) + (cur (var-deref (chez-current-ns) nm)) + (mf (if (jolt-multifn? cur) cur + (let ((m (make-jolt-multifn nm (var-deref "clojure.core" "identity") + (new-mm-table) kw-default #f (new-mm-table)))) + (def-var! (chez-current-ns) nm m) m)))) + (hashtable-set! (jolt-multifn-methods mf) dval impl) + mf)) + +;; --- dispatch ---------------------------------------------------------------- +(define (mm-isa? mf) + ;; the overlay's isa? (the hierarchy system is pure Clojure); a per-mm :hierarchy + ;; is an atom (deref each dispatch, like a Clojure var) or a plain map. + (let* ((isa (var-deref "clojure.core" "isa?")) + (h (jolt-multifn-hierarchy mf)) + (hval (and h (if (jolt-atom? h) (jolt-atom-val h) h)))) + (lambda (x y) (jolt-truthy? (if hval (jolt-invoke isa hval x y) (jolt-invoke isa x y)))))) + +(define (mm-find-isa mf dv) + (let* ((methods (jolt-multifn-methods mf)) + (isa? (mm-isa? mf)) + (default (jolt-multifn-default mf)) + (keys (filter (lambda (k) (not (jolt= k default))) + (vector->list (hashtable-keys methods)))) + (matches (filter (lambda (k) (isa? dv k)) keys))) + (cond + ((null? matches) #f) + ((null? (cdr matches)) (hashtable-ref methods (car matches) #f)) + (else + ;; >1 isa-match: pick the dominant key (x dominates y when x is + ;; prefer-method'd over y, or (isa? x y)); ambiguity with no dominant is an + ;; error, as in Clojure. + (let* ((prefers (jolt-multifn-prefers mf)) + (pref? (lambda (x y) + (let ((px (hashtable-ref prefers x #f))) + (and px (hashtable-ref px y #f) #t)))) + (dom? (lambda (x y) (or (pref? x y) (isa? x y)))) + (best (fold-left (lambda (b k) (if (dom? k b) k b)) (car matches) (cdr matches)))) + (for-each + (lambda (k) + (when (and (not (jolt= k best)) (not (dom? best k))) + (error #f (string-append "Multiple methods in multimethod '" (jolt-multifn-name mf) + "' match dispatch value - and neither is preferred")))) + matches) + (hashtable-ref methods best #f)))))) + +(define (multifn-dispatch mf . args) + (let* ((dv (apply jolt-invoke (jolt-multifn-dispatch-fn mf) args)) + (methods (jolt-multifn-methods mf)) + (direct (hashtable-ref methods dv #f))) + (cond + (direct (apply jolt-invoke direct args)) + ((mm-find-isa mf dv) => (lambda (m) (apply jolt-invoke m args))) + ((hashtable-ref methods (jolt-multifn-default mf) #f) + => (lambda (m) (apply jolt-invoke m args))) + (else (error #f (string-append "No method in multimethod '" (jolt-multifn-name mf) + "' for dispatch value: " (jolt-pr-str dv))))))) + +;; jolt-invoke dispatches a multifn (otherwise falls through to the prior logic). +(define %prev-jolt-invoke jolt-invoke) +(set! jolt-invoke + (lambda (f . args) + (if (jolt-multifn? f) + (apply multifn-dispatch f args) + (apply %prev-jolt-invoke f args)))) + +;; --- table ops --------------------------------------------------------------- +;; prefer-method/remove-method/remove-all-methods/prefers take the name QUOTED; +;; get-method/methods take the multifn VALUE (Clojure semantics). +(define (mm-of-sym sym) (let ((v (var-deref (chez-current-ns) (symbol-t-name sym)))) + (and (jolt-multifn? v) v))) + +(define (jolt-prefer-method-setup mm-sym dval-a dval-b) + (let ((mf (mm-of-sym mm-sym))) + (when mf + (let ((sub (or (hashtable-ref (jolt-multifn-prefers mf) dval-a #f) + (let ((h (new-mm-table))) + (hashtable-set! (jolt-multifn-prefers mf) dval-a h) h)))) + (hashtable-set! sub dval-b #t))) + mf)) + +(define (jolt-remove-method-setup mm-sym dval) + (let ((mf (mm-of-sym mm-sym))) + (when mf (hashtable-delete! (jolt-multifn-methods mf) dval)) + mf)) + +(define (jolt-remove-all-methods-setup mm-sym) + (let ((mf (mm-of-sym mm-sym))) + (when mf (hashtable-clear! (jolt-multifn-methods mf))) + mf)) + +(define (jolt-get-method-setup mf dval) + (if (jolt-multifn? mf) + (or (hashtable-ref (jolt-multifn-methods mf) dval #f) + (hashtable-ref (jolt-multifn-methods mf) (jolt-multifn-default mf) #f) + jolt-nil) + jolt-nil)) + +(define (jolt-methods-setup mf) + (if (jolt-multifn? mf) + (let-values (((ks vs) (hashtable-entries (jolt-multifn-methods mf)))) + (let loop ((i 0) (m (jolt-hash-map))) + (if (fx>=? i (vector-length ks)) m + (loop (fx+ i 1) (jolt-assoc m (vector-ref ks i) (vector-ref vs i)))))) + jolt-nil)) + +(define (jolt-prefers-setup mm-sym) + (let ((mf (mm-of-sym mm-sym))) + (if (not mf) (jolt-hash-map) + (let-values (((ks vs) (hashtable-entries (jolt-multifn-prefers mf)))) + (let loop ((i 0) (m (jolt-hash-map))) + (if (fx>=? i (vector-length ks)) m + ;; each value is an inner set of preferred-over keys -> a jolt set + (loop (fx+ i 1) + (jolt-assoc m (vector-ref ks i) + (apply jolt-hash-set + (vector->list (hashtable-keys (vector-ref vs i)))))))))))) + +(def-var! "clojure.core" "defmulti-setup" jolt-defmulti-setup) +(def-var! "clojure.core" "defmethod-setup" jolt-defmethod-setup) +(def-var! "clojure.core" "prefer-method-setup" jolt-prefer-method-setup) +(def-var! "clojure.core" "remove-method-setup" jolt-remove-method-setup) +(def-var! "clojure.core" "remove-all-methods-setup" jolt-remove-all-methods-setup) +(def-var! "clojure.core" "get-method-setup" jolt-get-method-setup) +(def-var! "clojure.core" "methods-setup" jolt-methods-setup) +(def-var! "clojure.core" "prefers-setup" jolt-prefers-setup) diff --git a/host/chez/rt.ss b/host/chez/rt.ss index 277f59b..2ac7202 100644 --- a/host/chez/rt.ss +++ b/host/chez/rt.ss @@ -165,3 +165,8 @@ ;; reduced/reduced?/identical? — seed-native fns the overlay assumes are core ;; natives. Over the seq layer + jolt-compare, so loaded after converters.ss. (load "host/chez/natives-seq.ss") + +;; multimethods (jolt-9ls5): defmulti/defmethod dispatch runtime. Needs jolt-invoke +;; (seq.ss), jolt=/key-hash/jolt-hash-map (collections.ss), jolt-atom? (atoms.ss), +;; jolt-pr-str (above), and the var-cell machinery — so loaded last. +(load "host/chez/multimethods.ss") diff --git a/jolt-core/jolt/analyzer.clj b/jolt-core/jolt/analyzer.clj index 6efd853..1e85130 100644 --- a/jolt-core/jolt/analyzer.clj +++ b/jolt-core/jolt/analyzer.clj @@ -24,7 +24,7 @@ form-regex? form-regex-source form-macro? form-expand-1 resolve-global form-sym-meta host-intern! form-syntax-quote-lower - record-type? record-ctor-key form-position]])) + record-type? record-ctor-key form-position late-bind?]])) (declare analyze) @@ -317,7 +317,13 @@ ;; (defmulti's setup call) legitimately reference the var they ;; are about to create when nested in a non-top-level do. Real ;; forward references want (declare ...), as in Clojure. - (uncompilable (str "Unable to resolve symbol: " nm " in this context"))))))) + ;; Under late-bind? (the Chez back end, which has no interpreter + ;; to punt to) an unresolved symbol instead lowers to a var-ref + ;; against the compile ns — resolved at runtime, the open-world + ;; semantics of -e — so defmulti/defmethod forward references work. + (if (late-bind? ctx) + (var-ref (compile-ns ctx) nm) + (uncompilable (str "Unable to resolve symbol: " nm " in this context")))))))) (defn- analyze-list [ctx form env] (let [items (vec (form-elements form))] diff --git a/src/jolt/host_iface.janet b/src/jolt/host_iface.janet index 8edd912..f0cc374 100644 --- a/src/jolt/host_iface.janet +++ b/src/jolt/host_iface.janet @@ -171,6 +171,14 @@ (ns-intern (ctx-find-ns ctx ns-name) nm) nil) +# Open-world (late-binding) analysis: when set on the ctx env, an unresolved +# symbol emits a var-ref against the compile ns (resolved at runtime) instead of +# punting to the interpreter. The Chez back end (host/chez/driver) sets it: it has +# no interpreter to punt to, and a forward reference to a runtime-interned var +# (e.g. defmulti's setup call) must lower to a var-deref. Off everywhere else, so +# the normal compiler keeps its strict 'Unable to resolve symbol' behavior. +(defn h-late-bind? [ctx] (truthy? (get (ctx :env) :late-bind-unresolved?))) + # --------------------------------------------------------------------------- # Installation: bind these fns as vars in the `jolt.host` namespace so jolt-core # can call them. Idempotent per context. @@ -270,7 +278,8 @@ "host-intern!" h-intern! "inline-enabled?" h-inline-enabled? "inline-ir" h-inline-ir "record-type?" h-record-type? "form-position" h-form-position - "record-ctor-key" h-record-ctor-key "record-shapes" h-record-shapes}) + "record-ctor-key" h-record-ctor-key "record-shapes" h-record-shapes + "late-bind?" h-late-bind?}) (defn install! [ctx] (def ns (ctx-find-ns ctx "jolt.host")) diff --git a/test/chez/README.md b/test/chez/README.md index 53129c3..cdda186 100644 --- a/test/chez/README.md +++ b/test/chez/README.md @@ -163,6 +163,28 @@ class names, eval-order, with-open — all deferred Phase-2 / dynamic-var gaps). The Chez host throws (Clojure-correct), so the 4 `odd arg …` corpus rows — which encode the seed's lenient behavior — sit in the crash bucket until the seed is fixed and those spec rows are updated to `:throws`. +- inc 3q multimethod dispatch + late-bind (jolt-9ls5, Phase 2): `host/chez/ + multimethods.ss` implements the multimethod runtime — `defmulti`/`defmethod` + expand to `defmulti-setup`/`defmethod-setup` calls (+ `get-method`/`methods`/ + `remove-method`/`prefer-method`/`prefers`). A `jolt-multifn` record carries its + dispatch fn and a `jolt=`-keyed method table; `jolt-invoke` dispatches it (exact + match, then isa?/hierarchy with `prefer-method`, then `:default`), reusing the + overlay's `isa?`/`derive`/`make-hierarchy`. The multifn's ns is set via a runtime + `chez-current-ns` (default "user"; the prelude load sets "clojure.core"). + + Two emit-side changes made this work: + - **late-bind** (`:late-bind-unresolved?` ctx flag, default OFF): `defmulti` + expands to a bare-symbol setup *call*, so the analyzer doesn't intern the name + and a forward reference (`(area …)` after `(defmulti area …)` in one form) was + "Unable to resolve symbol". The strict compiler punts these to the interpreter; + the Chez back end has none, so the flag makes an unresolved symbol lower to a + `var-ref` against the compile ns — the open-world semantics of `-e`. Set only by + the Chez `make-ctx`/`jolt-chez`; the main compiler keeps its strict behavior. + - a `:var` call head now routes through `jolt-invoke` (not a direct application), + since a late-bound var can hold a multifn (or a keyword/coll IFn), not just a + procedure. Transparent for procedures; the hot self-recursive call is a `:local` + known-proc, so it stays direct. (Class-based dispatch — `(class x)`/`String` — is + deferred; it needs the deftype/class subsystem.) The remaining buckets are the punch-list the next increments chase: ~361 emit-fail (genuine host interop — qualified Java/Janet refs, runtime `defmacro`/`eval`, out of diff --git a/test/chez/emit-test.janet b/test/chez/emit-test.janet index e86464a..655e331 100644 --- a/test/chez/emit-test.janet +++ b/test/chez/emit-test.janet @@ -583,6 +583,32 @@ (ok (string "y1zq -e: " src) (and (= code 0) (= out want)) (string "chez=" out " janet=" want " | " err))))) +# 3v) multimethod dispatch (jolt-9ls5): defmulti/defmethod expand to +# defmulti-setup/defmethod-setup (+ get-method/methods/remove-method/ +# prefer-method/prefers); host/chez/multimethods.ss provides the runtime. A +# jolt-multifn record carries its dispatch fn + method table; jolt-invoke +# dispatches it (direct match, then isa?/hierarchy + prefers, then :default). +# Dispatch uses the overlay isa?/derive/make-hierarchy, so these need the full +# prelude -> the -e binary. (Class-based dispatch — (class x)/String — is +# deferred; it needs the deftype/class subsystem.) +(when (os/stat "bin/jolt-chez") + (each src ["(= \"two\" (do (defmulti f identity) (defmethod f 1 [_] \"one\") (defmethod f 2 [_] \"two\") (f 2)))" + "(= \"circle\" (do (defmulti area :shape) (defmethod area :circle [_] \"circle\") (area {:shape :circle})))" + "(= \"other\" (do (defmulti f identity) (defmethod f 1 [_] \"one\") (defmethod f :default [_] \"other\") (f 99)))" + "(= 5 (do (defmulti g (fn [a b] a)) (defmethod g :add [_ b] b) (g :add 5)))" + "(= :is-shape (do (derive :hsq :hshape) (defmulti hmm identity) (defmethod hmm :hshape [_] :is-shape) (hmm :hsq)))" + "(= :parent (do (def hh (atom (derive (make-hierarchy) :c :p))) (defmulti cmm identity :hierarchy hh) (defmethod cmm :p [_] :parent) (cmm :c)))" + "(= :exact (do (derive :de1 :de2) (defmulti emm identity) (defmethod emm :de2 [_] :parent) (defmethod emm :de1 [_] :exact) (emm :de1)))" + "(= \"one\" (do (defmulti f identity) (defmethod f 1 [_] \"one\") ((get-method f 1) 1)))" + "(= \"one\" (do (defmulti f identity) (defmethod f 1 [_] \"one\") ((get (methods f) 1) 1)))" + "(= 2 (do (defmulti f identity) (defmethod f 1 [_] \"one\") (defmethod f 2 [_] \"two\") (count (methods f))))"] + (let [[code out err] (run-jolt-chez src) want (cli-oracle src)] + (ok (string "multimethod: " src) (and (= code 0) (= out want)) + (string "chez=" out " janet=" want " | " err)))) + # no-match throws (exits non-zero), like the corpus :throws row. + (let [[code out err] (run-jolt-chez "(do (defmulti f identity) (defmethod f 1 [_] \"one\") (f 99))")] + (ok "multimethod: no match throws" (not= code 0) (string "code=" code)))) + # 4) perf signal: emitted fib(30) in-Scheme timing (excludes Chez startup), to # track against the spike ceiling (hand-Scheme fib ~5ms). Informational — the # jolt-truthy? wrapper (~3x) and flonum modeling are known Phase-4 levers. diff --git a/test/chez/run-corpus-prelude.janet b/test/chez/run-corpus-prelude.janet index be8aa1c..489c41b 100644 --- a/test/chez/run-corpus-prelude.janet +++ b/test/chez/run-corpus-prelude.janet @@ -136,9 +136,9 @@ # on any NEW (un-allowlisted) divergence — a real Chez correctness regression. # Full-corpus baseline: inc 3j 1220/2497; 3k (converters) 1326; 3l (transients) # 1382; 3m (numeric-edge emit + variadic assoc!) 1407; 3n (seq-native shims + -# reduced) 1467; 3o (transducer arities) 1493; 3p (misc seq/regex gaps) 1506. -# Strided runs scale down. -(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "1506"))) +# reduced) 1467; 3o (transducer arities) 1493; 3p (misc seq/regex gaps) 1506; +# 3q (multimethod dispatch + late-bind) 1530. Strided runs scale down. +(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "1530"))) (def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor)) (when (or (> (length diverged) 0) (< pass floor)) (printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged)))