3
votes

I want to make a macro which defines a function outside of the local variable scope, as to catch errors where the programmer (aka me) is incorrectly referencing variables in the local scope. It should only allow variables declared in the first argument of the macro.

For example, the following should compile fine:

(let [ foo 'x
       bar 'y ]
   (my-macro [ foo ] ;; this designates that foo can be used
     (print foo) 
     )
   )

but, the following should fail to compile, because "bar" wasn't declared in the first argument of my-macro:

(let [ foo 'x
       bar 'y ]
   (my-macro [ foo ] ;; only foo is declared here, but bar 
                     ;; is used, so should fail to compile
     (print foo)
     (print bar) ;; <-- should fail here
     )
   )

Besides the error checking, the macro needs to return a vector of the values of the variables used and a function containing the body. This is what I have so far.

(defmacro my-macro 
  [ declared-vars-in-use & body ]
  `[~declared-vars-in-use (fn [~@declared-vars-in-use] 
                               ~@body
                               )
   ]
  )

The only thing I don't know how to do is enforce the compilation error when referencing a symbol from the local scope that isn't declared in my-macro ("bar" in the above example). Forgive me if I'm using incorrect terms, and hopefully you can understand this, I am still a newbie at clojure.

1

1 Answers

1
votes

(Updated with a version of explicit-closure free from the considerable limitations of the original approach.)

The macro below returns a function which closes over the specified locals and accepts specified additional arguments. If you only want a function which doesn't close over any locals at all (in other words, whose body only uses no external locals), which is what my-macro would produce if given the desired ability to prevent access to locals not on the declared-vars-in-use list (as the locals on the list would be shadowed by the parameters), you can simply eval an appropriate fn form, as eval ignores locals. (So, skip close-over and the let around the inner list* in the snippet below.)

(defmacro explicit-closure [close-over params & body]
  (eval (list 'let
              (vec (interleave close-over (repeat nil)))
              (list* 'fn params body)))
  `[~close-over (fn [~@params] ~@body)])

Note that the call to eval here happens during compilation and is only meant to coax the compiler into complaining if the function body references the wrong locals. If the body does not use disallowed locals or otherwise cause the eval call to fail, regular code for the function is emitted with no further checks or eval-induced compilation at runtime.

From the REPL:

(let [foo 1
      bar 2]
  (explicit-closure [foo] [x] (+ foo x)))
;= [[1] #<user$eval1887$fn__1888 user$eval1887$fn__1888@39c4d0cd>]

(let [foo 1
      bar 2]
  (let [[vals f] (explicit-closure [foo] [x] (+ foo x))]
    (prn vals)
    (f 3)))
;=> [1]
;= 4

;;; replace (+ foo x) with (+ foo bar x) in the above
;;; to get a CompilerException