diff --git a/jolt-core/clojure/core/00-syntax.clj b/jolt-core/clojure/core/00-syntax.clj index a6c34c4..447f572 100644 --- a/jolt-core/clojure/core/00-syntax.clj +++ b/jolt-core/clojure/core/00-syntax.clj @@ -60,14 +60,99 @@ ;; pending cells (matching the prior Janet macro). (defmacro declare [& syms] `(do)) +;; destructure — Clojure's binding-vector expander, ported from the Janet seed +;; (was core-destructure). Turns a binding vector that may contain destructuring +;; patterns into a plain binding vector (alternating symbol / init-form) built from +;; nth/nthnext/get, so the COMPILER only ever sees plain symbols (analyze-bindings +;; rejects patterns). `let` consumes it directly; `loop`/`fn` reuse it transitively +;; through `let`. Written with let*/fn* and seed primitives only — it never uses +;; let/loop/fn, so expanding its own body can't recurse back into destructure. +;; Note map? is true for symbol structs too, so the symbol? clause must come first. +;; def+fn* (not defn) because the defn macro is not defined until later in the tier. +(def destructure + (fn* destructure [bindings] + (let* [find-or + (fn* [or-map nm] + (reduce (fn* [acc k] + (if (and (symbol? k) (= nm (name k))) + [true (get or-map k)] + acc)) + [false nil] + (if or-map (keys or-map) []))) + amp? (fn* [x] (and (symbol? x) (= "&" (name x)))) + proc + (fn* proc [pat init acc] + (cond + (symbol? pat) (conj (conj acc pat) init) + (vector? pat) + (let* [g (symbol (str (gensym))) + n (count pat) + vloop + (fn* vloop [i idx a] + (if (< i n) + (let* [elem (nth pat i)] + (cond + (amp? elem) + (vloop (+ i 2) idx (proc (nth pat (inc i)) `(nthnext ~g ~idx) a)) + (= elem :as) + (vloop (+ i 2) idx (proc (nth pat (inc i)) g a)) + :else + (vloop (inc i) (inc idx) (proc elem `(nth ~g ~idx nil) a)))) + a))] + (vloop 0 0 (conj (conj acc g) init))) + (map? pat) + (let* [g (symbol (str (gensym))) + or-map (get pat :or) + as-sym (get pat :as) + base (if as-sym + (conj (conj (conj (conj acc g) init) as-sym) g) + (conj (conj acc g) init)) + group + (fn* [a kw kind] + (let* [names (get pat kw)] + (if names + (reduce + ;; s is a symbol (a b) or a keyword (:a :b); name/ + ;; namespace handle both, so :keys [:major] binds + ;; `major` looking up :major (str would keep the colon). + (fn* [aa s] + (let* [local (name s) + nsp (namespace s) + keyform (cond + (= kind :kw) (keyword (if nsp (str nsp "/" local) local)) + (= kind :str) local + :else `(quote ~(symbol nsp local))) + fo (find-or or-map local)] + (conj (conj aa (symbol local)) + (if (nth fo 0) + `(get ~g ~keyform ~(nth fo 1)) + `(get ~g ~keyform))))) + a names) + a))) + g1 (group base :keys :kw) + g2 (group g1 :strs :str) + g3 (group g2 :syms :sym)] + (reduce (fn* [a k] + (if (keyword? k) + a + (proc k `(get ~g ~(get pat k)) a))) + g3 (keys pat))) + :else (throw (str "unsupported destructuring pattern")))) + ploop + (fn* ploop [i acc] + (if (< i (count bindings)) + (ploop (+ i 2) (proc (nth bindings i) (nth bindings (inc i)) acc)) + acc))] + (ploop 0 [])))) + ;; let desugars destructuring patterns to plain bindings (via destructure) so the ;; COMPILER sees only plain symbols — analyze-bindings rejects patterns as ;; uncompilable, relying on this macro to have expanded them. (The interpreter ;; could destructure let* directly, but the compiler can't.) let* is sequential, so -;; a later init can reference an earlier destructured name. destructure is a -;; clojure.core fn; calling it at expansion time is fine — it's interned at init. +;; a later init can reference an earlier destructured name. Splice via [~@..] so the +;; binding vector is a tuple form (destructure returns a pvec), not a pvec literal. (defmacro let [bindings & body] - `(let* ~(destructure bindings) ~@body)) + `(let* [~@(destructure bindings)] ~@body)) ;; loop binds destructuring forms like let, but recur must target the loop* vars, ;; whose count can't change. So (matching Clojure): gensym one loop var per binding, diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 40a9190..8bf5f57 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -2235,85 +2235,6 @@ # declare macro — accepts symbols, does nothing (forward declaration) -# --- Destructuring expansion (Clojure's `destructure`) ----------------------- -# Expands a binding vector containing destructuring patterns into a plain binding -# vector (alternating plain-symbol / init-form), using nth/nthnext/get. Shared by -# let/loop/fn so BOTH the interpreter and the compiler see only plain bindings — -# the compiler can then compile destructuring (it never sees a pattern), and the -# kernel needn't destructure at runtime. Mirrors evaluator/destructure-bind. - -(defn- d-sym [name] {:jolt/type :symbol :ns nil :name name}) -(defn- d-amp? [x] (and (struct? x) (= :symbol (x :jolt/type)) (= "&" (x :name)))) -(defn- d-plain-sym? [x] (and (struct? x) (= :symbol (x :jolt/type)))) - -(defn- d-find-or [or-map nm] - # [has-default default-form] for binding name nm in an :or map - (var found false) (var dv nil) - (when or-map - (each k (keys or-map) - (when (and (d-plain-sym? k) (= nm (k :name))) - (set found true) (set dv (get or-map k))))) - [found dv]) - -(var d-process nil) - -(defn- d-vec [pat g out] - (var i 0) (var idx 0) (def n (length pat)) - (while (< i n) - (def elem (in pat i)) - (cond - (d-amp? elem) - (do (d-process (in pat (+ i 1)) @[(d-sym "nthnext") g idx] out) (+= i 2)) - (= elem :as) - (do (d-process (in pat (+ i 1)) g out) (+= i 2)) - true - (do (d-process elem @[(d-sym "nth") g idx nil] out) (++ idx) (++ i))))) - -(defn- d-map [pat g out] - (def or-map (get pat :or)) - (def as-sym (get pat :as)) - (when as-sym (array/push out as-sym) (array/push out g)) - (each spec [[:keys :kw] [:strs :str] [:syms :sym]] - (def kw (in spec 0)) (def kind (in spec 1)) (def names (get pat kw)) - (when names - (each s names - (def is-sym (d-plain-sym? s)) - (def local (if is-sym (s :name) (string s))) - # A namespaced symbol in :keys/:syms (x/y) looks up the namespaced key - # but binds the bare local y. - (def nsp (and is-sym (s :ns))) - (def keyform (case kind - :kw (keyword (if nsp (string nsp "/" local) local)) - :str local - :sym @[(d-sym "quote") {:jolt/type :symbol :ns nsp :name local}])) - (def fo (d-find-or or-map local)) - (array/push out (d-sym local)) - (array/push out (if (in fo 0) - @[(d-sym "get") g keyform (in fo 1)] - @[(d-sym "get") g keyform]))))) - (each k (keys pat) - (when (not (keyword? k)) # explicit {pattern key-expr} - (d-process k @[(d-sym "get") g (get pat k)] out)))) - -(set d-process - (fn dp [pat init out] - (cond - (d-plain-sym? pat) (do (array/push out pat) (array/push out init)) - (tuple? pat) (let [g (gensym "_d__")] - (array/push out g) (array/push out init) (d-vec pat g out)) - (and (struct? pat) (nil? (pat :jolt/type))) - (let [g (gensym "_d__")] - (array/push out g) (array/push out init) (d-map pat g out)) - (error "unsupported destructuring pattern")))) - -(defn core-destructure - "Clojure `destructure`: binding vector with patterns -> plain binding vector." - [bindings] - (def out @[]) - (var i 0) (def n (length bindings)) - (while (< i n) (d-process (in bindings i) (in bindings (+ i 1)) out) (+= i 2)) - (tuple/slice out)) - # Build a protocol value (a self-evaluating tagged table). Exposed so the overlay # `defprotocol` can construct one via a fn call rather than embedding a tagged # struct literal (which the interpreter would try to re-evaluate). `methods` is a @@ -3183,7 +3104,6 @@ "remove-all-methods" core-remove-all-methods "prefer-method" core-prefer-method "Object" core-Object - "destructure" core-destructure "make-protocol" core-make-protocol "satisfies?" core-satisfies? "extends?" core-extends? diff --git a/test/spec/destructuring-spec.janet b/test/spec/destructuring-spec.janet index 9db2b6a..31d8afd 100644 --- a/test/spec/destructuring-spec.janet +++ b/test/spec/destructuring-spec.janet @@ -35,7 +35,10 @@ [":strs" "7" "(let [{:strs [a]} {\"a\" 7}] a)"] [":syms" "8" "(let [{:syms [a]} {(quote a) 8}] a)"] ["namespaced :keys" "3" "(let [{:keys [x/y]} {:x/y 3}] y)"] - ["namespaced :syms" "4" "(let [{:syms [p/q]} {(quote p/q) 4}] q)"]) + ["namespaced :syms" "4" "(let [{:syms [p/q]} {(quote p/q) 4}] q)"] + # :keys also accepts keyword elements ({:keys [:a :b]}), binding bare locals. + ["keyword :keys" "3" "(let [{:keys [:a :b]} {:a 1 :b 2}] (+ a b))"] + ["keyword :keys ns" "3" "(let [{:keys [:x/y]} {:x/y 3}] y)"]) (defspec "destructure / keyword args (& {:keys})" ["fn kwargs" "[1 2]" "(do (defn f [& {:keys [a b]}] [a b]) (f :a 1 :b 2))"]