Reputation: 5020
How do you find all the free variables in a Clojure expression?
By free variables, I mean all the symbols that are not defined in the local environment (including all local environments defined inside the expression), not defined in the global environment (all symbols in the current namespace, including all those imported from other namespaces), and not a Clojure primitive.
For example, there is one free variable in this expression:
(fn [ub]
(* (rand-int ub) scaling-factor))
and that is scaling-factor
. fn
, *
, and rand-int
are all defined in the global environment. ub
is defined in the scope that it occurs within, so it's a bound variable, too (i.e. not free).
I suppose I could write this myself—it doesn't look too hard—but I'm hoping that there's some standard way to do it that I should use, or a standard way to access the Clojure compiler to do this (since the Clojure compiler surely has to do this, too). One potential pitfall for a naïve implementation is that all macros within the expression must be fully expanded, since macros can introduce new free variables.
Upvotes: 5
Views: 348
Reputation: 4513
You can analyze the form with tools.analyzer.jvm
passing the specific callback to handle symbols that cannot be resolved.
Something like this:
(require '[clojure.tools.analyzer.jvm :as ana.jvm])
(def free-variables (atom #{}))
(defn save-and-replace-with-nil [_ s _]
(swap! free-variables conj s)
;; replacing unresolved symbol with `nil`
;; in order to keep AST valid
{:op :const
:env {}
:type :nil
:literal? true
:val nil
:form nil
:top-level true
:o-tag nil
:tag nil})
(ana.jvm/analyze
'(fn [ub]
(* (rand-int ub) scaling-factor))
(ana.jvm/empty-env)
{:passes-opts
(assoc ana.jvm/default-passes-opts
:validate/unresolvable-symbol-handler save-and-replace-with-nil)})
(println @free-variables) ;; => #{scaling-factor}
It will also handle macroexpansion correctly:
(defmacro produce-free-var []
`(do unresolved))
(ana.jvm/analyze
'(let [x :foo] (produce-free-var))
(ana.jvm/empty-env)
{:passes-opts
(assoc ana.jvm/default-passes-opts
:validate/unresolvable-symbol-handler save-and-replace-with-nil)})
(println @free-variables) ;; => #{scaling-factor unresolved}
Upvotes: 7