core: wrap macro expanders in fn (not fn*) so destructured arglists compile

Follow-up to the staged macro-compile change. The macro-recompile pass
wrapped each expander in the raw fn* primitive, which punts on a
destructuring rest param — so if-not/if-let/if-some/assert (using the
canonical '& [else]') couldn't compile and had been rewritten to plain
rest params as a workaround.

Root cause: fn* is the primitive; the fn MACRO is what desugars
destructuring (rest, map, nested) into the body before lowering. Wrapping
expanders in fn instead of fn* compiles any destructured macro arglist
uniformly, so the workaround is unnecessary — reverted those 4 macros to
the canonical '& [else]' forms.

Net: rest-destructuring is fully compiled for all normal code (fn/defn/
let/macro params). Only the hand-written raw fn* primitive still punts
(jolt-f79, downgraded to P4 — falls back to interpreter, still correct).

47/47 overlay macros compiled. Gate green: conformance 267x3,
fallback-zero 31/5, bootstrap-fixpoint stage1==2==3, self-host,
staged-bootstrap, lazy-infinite 44/44, clojure-test-suite >=4034/67,
sci-bootstrap, features, all unit+spec.
This commit is contained in:
Yogthos 2026-06-09 08:52:01 -04:00
parent ad5216a178
commit 20da34a432
3 changed files with 25 additions and 22 deletions

View file

@ -12,27 +12,25 @@
(defmacro comment [& body] nil)
;; Single arglist (Jolt defmacro is single-arity); the optional else is the head of
;; a plain rest param — (first else) is the else form or nil. (A `& [else]`-style
;; rest-destructure reads the same but the analyzer can't yet compile destructuring
;; in a rest param, which would force this expander to stay interpreted — jolt-f79.)
(defmacro if-not [test then & else]
`(if (not ~test) ~then ~(first else)))
;; Single arglist (Jolt defmacro is single-arity); the optional else defaults nil
;; via rest-destructuring.
(defmacro if-not [test then & [else]]
`(if (not ~test) ~then ~else))
;; Conditional binding macros: the name is bound ONLY in the taken branch (the
;; auto-gensym temp# tests the value; the else/empty branch sees the surrounding
;; scope). temp# is a single template-local gensym — referenced twice, same symbol.
(defmacro if-let [bindings then & else]
(defmacro if-let [bindings then & [else]]
(let [form (bindings 0) tst (bindings 1)]
`(let [temp# ~tst]
(if temp# (let [~form temp#] ~then) ~(first else)))))
(if temp# (let [~form temp#] ~then) ~else))))
;; when-let lives in 00-syntax (not here): 20-coll uses it, which loads before this tier.
(defmacro if-some [bindings then & else]
(defmacro if-some [bindings then & [else]]
(let [form (bindings 0) tst (bindings 1)]
`(let [temp# ~tst]
(if (some? temp#) (let [~form temp#] ~then) ~(first else)))))
(if (some? temp#) (let [~form temp#] ~then) ~else))))
(defmacro when-some [bindings & body]
(let [form (bindings 0) tst (bindings 1)]
@ -93,9 +91,8 @@
(partition 2 clauses))]
`(let [~g ~expr ~@(thread-binds g steps)] ~(if (empty? steps) g (last steps)))))
(defmacro assert [x & message]
(let [m (first message)
msg (if m m (str "Assert failed: " (pr-str x)))]
(defmacro assert [x & [message]]
(let [msg (if message message (str "Assert failed: " (pr-str x)))]
`(when-not ~x (throw (ex-info ~msg {})))))
(defmacro delay [& body]