Defensive Multithreaded Coding...
As a result of my recent bout of confusion regarding the priorities of Common Lisp binding mechanisms, I had to come up with something to protect the code from all future REPL hackings that might cause unintentional special bindings to occur. Of course this would only matter if you need to recompile a source after playing at the keyboard. But that situation happens almost all the time for me.I really don’t want a protective mechanism that causes recompiles to fail. I’d much rather have a system that simply defends against future problems. So I came up with a set of macros for ensuring lexical bindings. However, they use symbol-macrolet to avoid the need for a code walker. So in any event, a compile with raise an error signal if any of the mentioned names are already bound at the global level.
Here is an example of use:
;; 1m10s to compare entire Lispworks trees between Dachshund and Malachite!!
(defun compare-system (path node node-path)
(um:ensure-lexical (path node node-path)
(um:llet (tree-a tree-b)
(um:par
(setf tree-a (grand-hash path))
(setf tree-b (bfly:!? (concatenate 'string "eval@" node)
`(grand-hash ,node-path))))
(compare-directory tree-a tree-b))))
This code will execute two filesystem hashing scans in parallel on two machines connected over a secure network channel. Each form inside the um:par clause may be fired off into another thread for execution. They rendezvous at the close of the um:par clause.
But references inside those worker forms are to free vars for PATH, NODE, NODE-PATH, TREE-A, and TREE-B. Hence, if any of these are names of global bindings, then the code would fail except for the use of ENSURE-LEXICAL and the LLET.
You normally don’t know and don’t care whether the names of function args happen to also name global bindings. They are bound on entry, and behave essentially the same inside the body of the function. But any embedded lambda closures that may be performed in another thread really will care if they refer to these function args. Unless you can be sure that the free vars refer to lexically bound vars that refer to the arguments, you will instead end up looking at blank globals belonging to the host thread that runs the lambda closure. Kaboom! ENSURE-LEXICAL and LLET give you some defensive tactics against such errors.
(defmacro llet (bindings &body body)
;; enforce lexical binding by way of alpha conversion
;; of the binding symbols
;;
;; symbol-macrolet will signal an error if one of the symbols
;; in the llet is named in a special declaration
(multiple-value-bind (new-bindings new-body)
(rebindings bindings body)
`(let ,new-bindings
,new-body)))
(defmacro llet* (bindings &body body)
`(llet (,(car bindings))
,@(if (cdr bindings)
`((llet* ,(cdr bindings)
,@body))
body)))
(defmacro ensure-lexical (syms &body body)
;; Use this to ensure that free vars inside body lambda closures
;; refer to lexically bound values.
;;
;; Note that because of the symbol-macrolet, if any symbols in the list
;; are already bound specially, compiling will signal an error.
(let* ((gnames (mapcar (um:compose #'gensym #'string) syms)))
`(let ,(mapcar #'list gnames syms)
(symbol-macrolet ,(mapcar #'list syms gnames)
,@body))))
(defun rebindings (bindings body)
(let* ((names (get-binding-syms bindings))
(gnames (mapcar (um:compose #'gensym #'string) names))
(new-bindings (mapcar #'(lambda (binding gname)
(if (consp binding)
`(,gname ,@(cdr binding))
gname))
bindings gnames)))
(values new-bindings
`(symbol-macrolet ,(mapcar #'list names gnames)
,@body))))
(defun get-binding-sym (binding)
(if (consp binding)
(car binding)
binding))
(defun get-binding-syms (bindings)
(mapcar #'get-binding-sym bindings))
- DM