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/eval_runtime.janet b/src/jolt/eval_runtime.janet index 0e20298..07b6b06 100644 --- a/src/jolt/eval_runtime.janet +++ b/src/jolt/eval_runtime.janet @@ -707,6 +707,10 @@ "Double" (number? val) "String" (string? val) "java.lang.String" (string? val) + # String implements CharSequence — malli's :re validator gates on + # (instance? CharSequence x) before matching (jolt-ltwk). + "CharSequence" (string? val) + "java.lang.CharSequence" (string? val) "Boolean" (or (= true val) (= false val)) "Keyword" (keyword? val) # regex patterns (cuerdas-style (instance? Pattern x) checks) diff --git a/src/jolt/interop/host_io.janet b/src/jolt/interop/host_io.janet index 4dc689b..4186233 100644 --- a/src/jolt/interop/host_io.janet +++ b/src/jolt/interop/host_io.janet @@ -288,6 +288,12 @@ # (String. bytes) / (String. bytes charset): UTF-8 bytes to string. (each nm ["String" "java.lang.String"] (register-class-ctor! nm (fn [x &opt charset] (string x)))) + # String statics. valueOf(Object) yields "null" for nil, else toString — + # hiccup's compiler emits (String/valueOf (or arg "")) to stringify attribute + # values; core-str gives jolt's str semantics for keywords/numbers/etc. + (each nm ["String" "java.lang.String"] + (register-class-statics! nm + @{"valueOf" (fn [x &opt off cnt] (if (nil? x) "null" (core-str x)))})) # java.net.URL: enough for selmer's template cache — file: URLs only. # A protocol-less spec throws (selmer catches MalformedURLException and # prepends file:///), and getPath hands back a stat-able filesystem path. 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/host-interop-spec.janet b/test/spec/host-interop-spec.janet index 356e4df..ca7ed39 100644 --- a/test/spec/host-interop-spec.janet +++ b/test/spec/host-interop-spec.janet @@ -79,6 +79,14 @@ [".charAt" "\\b" "(.charAt \"abc\" 1)"] [".equalsIgnoreCase" "true" "(.equalsIgnoreCase \"AbC\" \"aBc\")"] ["Long/MAX_VALUE" "true" "(pos? Long/MAX_VALUE)"] + # String/valueOf static (jolt-nkx): hiccup stringifies attribute values with it. + ["String/valueOf num" "\"42\"" "(String/valueOf 42)"] + ["String/valueOf str" "\"hi\"" "(String/valueOf \"hi\")"] + ["String/valueOf kw" "\":k\"" "(String/valueOf :k)"] + ["String/valueOf nil" "\"null\"" "(String/valueOf nil)"] + # String implements CharSequence (malli's :re gates on it, jolt-ltwk). + ["instance? CharSequence" "true" "(instance? CharSequence \"aaa\")"] + ["instance? CharSequence non-str" "false" "(instance? CharSequence 42)"] ["unsupported method throws" :throws "(.frobnicate \"abc\")"]) # java.time shims (jolt-ea7): epoch-ms backed values + a DateTimeFormatter 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).