From c22b23fb51e6f143019d3b4be3c03a48431d4e5f Mon Sep 17 00:00:00 2001 From: Yogthos Date: Mon, 15 Jun 2026 15:15:32 -0400 Subject: [PATCH] Reader: #() params survive syntax-quote (auto-gensym names) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #(...) named its synthesized params with bare gensyms, so a #() written inside a syntax-quote had its params qualified to the current ns by sq-symbol — and a qualified symbol isn't a valid fn param. hiccup's compiler emits `(let [sb# ..] (iterate! #(.append sb# %) ..)), which broke with "Unable to resolve symbol: ns/_NNNN". Name the params with a trailing # (auto-gensym suffix, like Clojure's p1__N#) so syntax-quote maps them consistently and leaves them unqualified. Harmless outside a backtick (just a regular symbol name). --- docs/spec/02-reader.md | 5 +++++ src/jolt/reader.janet | 9 +++++++-- test/spec/macros-spec.janet | 9 ++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/spec/02-reader.md b/docs/spec/02-reader.md index 0cc7598..6d7fb70 100644 --- a/docs/spec/02-reader.md +++ b/docs/spec/02-reader.md @@ -125,6 +125,11 @@ checks → UNVERIFIED (rows to add). nested form including vector, map and set literals — `#(assoc {} :k %)`, `#(hash-set % %2)` and `#(get {:t %} :t)` all see their `%`s. (A reader that scanned only call forms would miscompile `#(identity {:text %})` as a 0-arg fn.) +- The synthesized parameters are auto-gensyms (their names carry the `#` suffix, + like Clojure's `p1__N#`), so an `#()` written inside a syntax-quote survives: + the params are mapped consistently and left unqualified rather than being + qualified to the current namespace (a qualified symbol is not a valid + parameter). E.g. `` `(map #(inc %) xs) `` expands correctly inside a macro. - `#()` literals MUST NOT nest. ```clojure diff --git a/src/jolt/reader.janet b/src/jolt/reader.janet index 46883be..3b61731 100644 --- a/src/jolt/reader.janet +++ b/src/jolt/reader.janet @@ -448,10 +448,15 @@ nil)) (scan-pct form) # One canonical gensym per slot 1..max-n (placeholders for unused), plus rest. + # Param names carry a trailing `#` (auto-gensym suffix, matching Clojure's + # `p1__N#`) so a #() written inside a syntax-quote survives: sq-symbol treats + # `#`-suffixed names as auto-gensyms (mapped consistently, left unqualified) + # rather than qualifying them to the current ns — a qualified symbol is not a + # valid fn param. Harmless outside a backtick (just a regular symbol name). (def slot-syms @{}) (var i 1) - (while (<= i max-n) (put slot-syms i (sym (string (gensym)))) (++ i)) - (def rest-sym (if has-rest (sym (string (gensym))) nil)) + (while (<= i max-n) (put slot-syms i (sym (string (gensym) "#"))) (++ i)) + (def rest-sym (if has-rest (sym (string (gensym) "#")) nil)) # Pass 2: replace each %-symbol with its slot's gensym. (defn- replace-pct [f] (cond diff --git a/test/spec/macros-spec.janet b/test/spec/macros-spec.janet index eb35387..5dd1fbd 100644 --- a/test/spec/macros-spec.janet +++ b/test/spec/macros-spec.janet @@ -24,7 +24,14 @@ ["gensym unique" "false" "(= (gensym) (gensym))"] ["gensym# in template" "true" - "(do (defmacro m [] `(let [x# 1] x#)) (= 1 (m)))"]) + "(do (defmacro m [] `(let [x# 1] x#)) (= 1 (m)))"] + # An #() lambda written inside a syntax-quote: its generated params must not be + # qualified to the ns (a qualified symbol is not a valid param). hiccup's + # compiler emits `(let [sb# ..] (run! #(.append sb# %) ..)) (jolt-nkx). + ["#() inside syntax-quote" "[2 4 6]" + "(do (defmacro m [] `(mapv #(* % 2) [1 2 3])) (m))"] + ["#() + auto-gensym share in template" "\"ab\"" + "(do (defmacro m [] `(let [sb# (StringBuilder.)] (mapv #(.append sb# %) [\"a\" \"b\"]) (.toString sb#))) (m))"]) # Core macros ported from Janet to the Clojure overlay (jolt-1j0 phase 3, # jolt-core/clojure/core/30-macros.clj).