Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## 0.6.35 (2026-07-18)

- Fix nested macro expansion: a macro expanding to a fully qualified call of
another macro namespace's macro was emitted as a runtime call, like squint
- Share the macro scan and macro lookup with squint. Namespaces flagged
`{:squint/compile-time true}` load only their compile-time part into the
macro environment, like squint

- Fix `deftype` implementing cljs.core protocols such as `Inst`, `IIterable`
and `IAtom`: their marker properties were Closure-renamed in the precompiled
core and missing from the emitter's core protocol set. The externs list and
Expand Down
7 changes: 7 additions & 0 deletions bb/integration_tests.clj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
(is (str/includes? out "22"))
(is (str/includes? out "1"))))

(deftest nested-macro-test
(t/testing "a macro expansion calling another macro ns fully qualified expands, not a runtime call"
(let [out (:out (sh {:err :inherit
:dir "test-resources/test_project"}
"npx cherry run nested_macro_test.cljs"))]
(is (str/includes? out "25")))))

(deftest equality-test
(let [out (:out (sh {:err :inherit
:dir "test-resources/test_project"}
Expand Down
2 changes: 1 addition & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
:deps {org.babashka/cli {:mvn/version "0.11.72"}
org.babashka/sci {:mvn/version "0.15.56"}
io.github.squint-cljs/squint
{:git/sha "79c06128cbe749477e49983fb47752b664e000d3"}
{:git/sha "a50e1d200ec9bdc352e644d74a4a2ea0e1bca925"}
#_{:local/root "/Users/borkdude/dev/squint"}}
:aliases
{:cljs {:extra-paths ["test"]
Expand Down
32 changes: 1 addition & 31 deletions src/cherry/compiler.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -241,37 +241,7 @@
macro (when (and (symbol? head)
(not (:squint.compiler/skip-macro mexpr)))
(or (built-in-macros (strip-core-symbol head))
(let [ns (namespace head)
nm (name head)
ns-state @(:ns-state env)
current-ns (:current ns-state)
nms (symbol nm)
current-ns-state (get ns-state current-ns)]
(if ns
(let [nss (symbol ns)]
(or
;; used by cherry embed:
(some-> env :macros (get nss) (get nms))
(let [macro-alias-ns (get-in current-ns-state [:macro-aliases nss])
resolved-ns (or macro-alias-ns
(get-in current-ns-state [:aliases nss] nss))
macro-ns (cc/resolve-macro-ns resolved-ns (:target env))]
(or (get-in ns-state [:macros macro-ns nms])
;; Built-in fallback. Only consult if the user actually
;; required this ns. Check after we know there's a candidate.
(when-let [m (get-in built-in-macro-nss [macro-ns nms])]
(when (or (contains? (:aliases current-ns-state) nss)
(contains? (:macro-aliases current-ns-state) nss)
(contains? (:aliases current-ns-state)
(symbol (cc/alias-munge (str nss)))))
m))))))
(let [refers (:refers current-ns-state)]
(when-let [macro-ns (get refers nms)]
(let [resolved (cc/resolve-macro-ns macro-ns (:target env))]
(or (some-> env :macros (get (symbol macro-ns)) (get nms))
(get-in ns-state [:macros resolved nms])
(get-in ns-state [:macros macro-ns nms])
(get-in built-in-macro-nss [resolved nms])))))))))]
(cc/lookup-macro head env built-in-macro-nss)))]
(if macro
(let [;; fix for calling macro with more than 20 args
#?@(:cljs [macro (or (.-afn ^js macro) macro)])
Expand Down
123 changes: 13 additions & 110 deletions src/cherry/compiler/node.cljs
Original file line number Diff line number Diff line change
@@ -1,133 +1,36 @@
(ns cherry.compiler.node
(:require
["crypto" :as crypto]
["fs" :as fs]
["path" :as path]
[cherry.compiler :as compiler]
[clojure.string :as str]
[edamame.core :as e]
[shadow.esm :as esm]
[squint.compiler-common :as cc]
[squint.internal.node.macro-scan :as ms]
[squint.internal.node.utils :as utils]))

(def sci (atom nil))

(def config-file "cherry.edn")

;; Tracks macro source files so we only re-eval (`:reload`) a macro namespace
;; when its file actually changed. Without this, the persistent SCI instance
;; keeps the first-loaded macro defs forever in watch mode (squint#819);
;; reloading on every compile would re-eval untouched macro nses instead of a
;; microsecond stat. path -> {:mtime _ :sha _}.
(def macro-state (atom {}))

(defn- sha256 [s]
(-> (crypto/createHash "sha256") (.update s) (.digest "hex")))

(defn- macro-file [macro-ns]
(utils/resolve-file macro-ns (:paths (utils/get-cfg config-file) ["." "src"])))

(defn- macro-file-changed?
"True when macro-ns's source file changed since last seen. Cheap mtime gate
first; only on mtime change do we read + sha256 to confirm real content
change (suppresses spurious reloads on touch-without-edit). Updates cache."
[macro-ns]
(when-let [path (macro-file macro-ns)]
(let [{:keys [mtime sha]} (get @macro-state path)
cur-mtime (.-mtimeMs (fs/statSync path))]
(when (not= cur-mtime mtime)
(let [cur-sha (sha256 (fs/readFileSync path "utf-8"))]
(swap! macro-state assoc path {:mtime cur-mtime :sha cur-sha})
(not= cur-sha sha))))))

(defn slurp [f]
(fs/readFileSync f "utf-8"))

(defn spit [f s]
(fs/writeFileSync f s "utf-8"))

(defn- cljc-with-macros?
"Check if a require clause refers to a .cljc file that contains defmacro."
[libspec]
(let [libname (if (symbol? libspec) libspec (first libspec))]
(when (symbol? libname)
(when-let [path (macro-file libname)]
(and (str/ends-with? path ".cljc")
(str/includes? (fs/readFileSync path "utf-8") "defmacro"))))))

(defn- as-alias? [libspec]
(and (sequential? libspec)
(some #{:as-alias} libspec)))

(defn- self-ref? [the-ns-name libspec]
(and (sequential? libspec) (= the-ns-name (first libspec))))

(defn scan-macros [s {:keys [ns-state]}]
(let [maybe-ns (e/parse-next (e/reader s) compiler/cherry-parse-opts)]
(when (and (seq? maybe-ns)
(= 'ns (first maybe-ns)))
(let [[_ns the-ns-name & clauses] maybe-ns
[require-macros reload] (some (fn [[clause reload]]
(when (and (seq? clause)
(= :require-macros (first clause)))
[(rest clause) reload]))
(partition-all 2 1 clauses))
;; a regular :require of a .cljc that contains defmacro loads its
;; macros too, like squint
require-libs (some->> clauses
(some (fn [clause]
(when (and (seq? clause)
(= :require (first clause)))
(rest clause)))))
require-cljc (->> require-libs
(remove #(self-ref? the-ns-name %))
(remove as-alias?)
(filter cljc-with-macros?))
require-macros (concat require-macros require-cljc)]
(when (seq require-macros)
(.then (esm/dynamic-import "./compiler.sci.js")
(fn [_]
(let [eval-form (:eval-form @sci)]
(reduce
(fn [prev require-macros]
(.then prev
(fn [_]
(let [;; a libspec may be a bare symbol, normalize to vector
require-macros (if (symbol? require-macros)
[require-macros]
require-macros)
[macro-ns & {:keys [refer as]}] require-macros
actual-ns (cc/resolve-macro-ns macro-ns :cherry)
file (macro-file actual-ns)
built-in (get compiler/built-in-macro-nss actual-ns)
reload? (or reload (macro-file-changed? actual-ns))
macros (cond
file
(js/Promise.resolve
(do (eval-form (cond-> (list 'require (list 'quote actual-ns))
reload? (concat [:reload])))
(let [publics (eval-form
`(ns-publics '~actual-ns))
macros (keep (fn [[k v]]
(when (:macro (meta v))
[k (deref v)])) publics)
macros (into {} macros)]
macros)))
built-in
(js/Promise.resolve built-in)
:else
(throw (js/Error. (str "Could not locate macro namespace " actual-ns))))]
(.then macros
(fn [macros]
(swap! ns-state (fn [ns-state]
(cond-> (assoc-in ns-state [:macros actual-ns] macros)
as (-> (assoc-in [the-ns-name :aliases as] macro-ns)
(assoc-in [the-ns-name :macro-aliases as] macro-ns))
refer (update-in [the-ns-name :refers]
merge
(zipmap refer (repeat macro-ns))))))))))))
(js/Promise.resolve nil)
require-macros)))))))))
(def dialect
{:parse-opts compiler/cherry-parse-opts
:features #{:cherry :cljs :default}
:config-file config-file
:target :cherry
:import-sci (fn [] (esm/dynamic-import "./compiler.sci.js"))
:sci sci})

(def compile-time-source (partial ms/compile-time-source dialect))

(defn scan-macros [s opts]
(ms/scan-macros dialect s opts))

(defn default-ns-state []
(atom {:current 'user}))
Expand Down
3 changes: 2 additions & 1 deletion src/cherry/compiler/sci.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
{})
(when-let [f (resolve-macro-file namespace)]
(let [fstr (slurp f)]
;; flagged ns -> its compile-time part only.
;; :file lets SCI bind *file* so an error
;; while loading this ns names its source.
{:file f
:source fstr}))))
:source (or (cn/compile-time-source fstr) fstr)}))))
:namespaces {'cljs.test test-ns
'clojure.test test-ns
'cherry.test test-ns}
Expand Down
4 changes: 4 additions & 0 deletions test-resources/test_project/nested_macro_test.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(ns nested-macro-test
(:require-macros [macros-outer :refer [outer]]))

(prn (outer 5))
4 changes: 4 additions & 0 deletions test-resources/test_project/src/macros_inner.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(ns macros-inner)

(defmacro inner [x]
(list '* x x))
5 changes: 5 additions & 0 deletions test-resources/test_project/src/macros_outer.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(ns macros-outer
(:require [macros-inner :as inner]))

(defmacro outer [x]
`(inner/inner ~x))
Loading