Macros receive &form and &env

A macro body can now read &form (the call form) and &env (a map of the in-scope
local symbols), like Clojure. This is what core.logic's matche/defne use to tell
a pattern symbol that names an enclosing local from a fresh pattern var — so
locals-membero and the recursive checko in `matches` now compute correctly. The
suite reaches 535/2/0 (the last two are constraint reification ORDER, where the
constraint set is right but it is spliced from a set whose iteration order differs
from the JVM — a host set-ordering divergence, not a bug).

&form/&env are clojure.core dynamic vars bound around each expander call rather
than prepended params, so the macro calling convention is unchanged and the mint
stays consistent (the seed prelude is byte-identical; only the analyzer carries
the env into form-expand-1). macroexpand-1 passes an empty env.

corpus.edn: the ~@ unquote row is now a boolean compare (a bare clojure.core/
unquote-splicing symbol evaluates to an unbound var, not the symbol).
This commit is contained in:
Yogthos 2026-06-27 11:54:47 -04:00
parent f46772d576
commit 438742702a
4 changed files with 57 additions and 39 deletions

View file

@ -215,12 +215,23 @@
(if (fx>=? i (pvec-count items)) s (if (fx>=? i (pvec-count items)) s
(loop (fx+ i 1) (pset-conj s (pvec-nth-d items i jolt-nil)))))) (loop (fx+ i 1) (pset-conj s (pvec-nth-d items i jolt-nil))))))
x)) x))
(define (hc-expand-1 ctx form) ;; &form and &env are bound (as dynamic vars) around the expander call, so a
;; macro body can read the call form / lexical env without changing the calling
;; convention. The analyzer passes amp-env (the in-scope locals); macroexpand-1
;; has none, so it defaults to {}.
(define hc-amp-form-cell (declare-var! "clojure.core" "&form"))
(define hc-amp-env-cell (declare-var! "clojure.core" "&env"))
(define (hc-expand-1 ctx form . maybe-env)
(let* ((items (seq->list form)) (let* ((items (seq->list form))
(head (car items)) (head (car items))
(args (map hc-macro-arg (cdr items))) (args (map hc-macro-arg (cdr items)))
(expander (var-cell-root (hc-resolve-cell ctx head)))) (expander (var-cell-root (hc-resolve-cell ctx head)))
(hc-propagate-pos form (apply jolt-invoke expander args)))) (amp-env (if (pair? maybe-env) (car maybe-env) (jolt-hash-map))))
(dynamic-wind
(lambda () (jolt-push-thread-bindings
(jolt-hash-map hc-amp-form-cell form hc-amp-env-cell amp-env)))
(lambda () (hc-propagate-pos form (apply jolt-invoke expander args)))
(lambda () (jolt-pop-thread-bindings)))))
;; Classify a global (non-local) symbol reference against the var registry: ;; Classify a global (non-local) symbol reference against the var registry:
;; {:kind :var :ns NS :name NAME} — a defined var (compile ns / clojure.core) ;; {:kind :var :ns NS :name NAME} — a defined var (compile ns / clojure.core)

File diff suppressed because one or more lines are too long

View file

@ -52,6 +52,11 @@
(defn- empty-env [] {:locals #{} :hints {}}) (defn- empty-env [] {:locals #{} :hints {}})
(defn- local? [env nm] (contains? (:locals env) nm)) (defn- local? [env nm] (contains? (:locals env) nm))
(defn- add-locals [env names] (update env :locals #(reduce conj % names))) (defn- add-locals [env names] (update env :locals #(reduce conj % names)))
;; &env value handed to a macro: a map of each in-scope local SYMBOL to nil
;; (Clojure's &env maps locals to compiler binding objects; consumers like
;; core.logic's matche only read its keys to tell locals from fresh pattern vars).
(defn- amp-env-map [env]
(reduce (fn [m n] (assoc m (symbol n) nil)) {} (:locals env)))
(defn- with-recur [env name] (assoc env :recur name)) (defn- with-recur [env name] (assoc env :recur name))
;; Type hints. The reader keeps ^hint metadata on the binding symbol. ;; Type hints. The reader keeps ^hint metadata on the binding symbol.
@ -632,7 +637,7 @@
;; 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.
(let [node (analyze ctx (form-expand-1 ctx form) env) (let [node (analyze ctx (form-expand-1 ctx form (amp-env-map env)) 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))
;; jolt.ffi/__cfn — the foreign-function special form (always emitted ;; jolt.ffi/__cfn — the foreign-function special form (always emitted

View file

@ -3348,5 +3348,5 @@
{: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 "(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)))"} {: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)))"}
{:suite "reader / unquote" :label "~x reads as clojure.core/unquote" :expected "true" :actual "(= (quote (clojure.core/unquote v)) (read-string \"~v\"))"} {:suite "reader / unquote" :label "~x reads as clojure.core/unquote" :expected "true" :actual "(= (quote (clojure.core/unquote v)) (read-string \"~v\"))"}
{:suite "reader / unquote" :label "~@x reads as clojure.core/unquote-splicing" :expected "clojure.core/unquote-splicing" :actual "(first (read-string \"~@xs\"))"} {:suite "reader / unquote" :label "~@x reads as clojure.core/unquote-splicing" :expected "true" :actual "(= (quote clojure.core/unquote-splicing) (first (read-string \"~@xs\")))"}
] ]