From 20da34a432b6a954cf96c15f7163580db6a64927 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Tue, 9 Jun 2026 08:52:01 -0400 Subject: [PATCH] core: wrap macro expanders in fn (not fn*) so destructured arglists compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- jolt-core/clojure/core/30-macros.clj | 23 ++++++++++------------- src/jolt/api.janet | 17 ++++++++++------- src/jolt/backend.janet | 7 +++++-- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/jolt-core/clojure/core/30-macros.clj b/jolt-core/clojure/core/30-macros.clj index ffea3b3..1379e63 100644 --- a/jolt-core/clojure/core/30-macros.clj +++ b/jolt-core/clojure/core/30-macros.clj @@ -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] diff --git a/src/jolt/api.janet b/src/jolt/api.janet index c172a1b..fe10f27 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -14,18 +14,21 @@ (import ./stdlib_embed :as stdlib-embed) (import ./host_iface :as host) -# A defmacro expander compiles to a native fn (built as (fn* args body...) and run +# A defmacro expander compiles to a native fn (built as (fn args body...) and run # through the self-hosted pipeline) so macro expansion is COMPILED code, zero runtime # cost — instead of an interpreted closure, mirroring Clojure (macros are ordinary -# compiled fns). Returns nil when the analyzer isn't built yet (the early macros, -# expanded WHILE the analyzer is being bootstrapped) or the body isn't compilable; in -# that case defmacro keeps an interpreted closure, and backend/recompile-macros! -# replaces it with a compiled expander once the analyzer comes alive (staged -# bootstrap — the interpreter is a build-time crutch, gone by steady state). +# compiled fns). Wrapped in the `fn` MACRO (not the `fn*` primitive) so a destructured +# macro arglist — `[a & [b]]`, `[& {:keys [x]}]`, nested — desugars before lowering; +# raw fn* punts on a destructuring rest param. Returns nil when the analyzer isn't +# built yet (the early macros, expanded WHILE the analyzer is being bootstrapped) or +# the body isn't compilable; in that case defmacro keeps an interpreted closure, and +# backend/recompile-macros! replaces it with a compiled expander once the analyzer +# comes alive (staged bootstrap — the interpreter is a build-time crutch, gone by +# steady state). (set macro-compile-hook (fn [ctx args-form body] (backend/try-compile-fn ctx - (array/concat @[{:jolt/type :symbol :ns nil :name "fn*"} args-form] body)))) + (array/concat @[{:jolt/type :symbol :ns nil :name "fn"} args-form] body)))) (defn normalize-pvecs "Deep-convert any sequential (pvec/tuple/array) to a Janet tuple. Test helper diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index 357e444..9352517 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -373,7 +373,10 @@ (def r (protect (eval (compiled 1) (comp/ctx-janet-env ctx)))) (when (r 0) (r 1))))) -(def- fn*-sym {:jolt/type :symbol :ns nil :name "fn*"}) +# Wrap expanders in the `fn` MACRO, not the `fn*` primitive: `fn` desugars a +# destructured macro arglist (`[a & [b]]`, `[& {:keys [x]}]`) before lowering, +# whereas raw fn* punts on a destructuring rest param. +(def- fn-sym {:jolt/type :symbol :ns nil :name "fn"}) (defn recompile-macros! "Staged-bootstrap second pass: once the self-hosted analyzer is alive, replace @@ -398,7 +401,7 @@ (not (v :macro-uses-env))) (def [args-form body] (v :macro-src)) (def compiled - (try-compile-fn ctx (array/concat @[fn*-sym args-form] body))) + (try-compile-fn ctx (array/concat @[fn-sym args-form] body))) (when compiled (bind-root v compiled) (put v :macro-compiled true)