;;; This note is heavily based on http://nullprogram.com/blog/2014/01/04/ ;;; and http://prog-elisp.blogspot.com/2012/05/lexical-scope.html . ;;; Read these first (you may skip the part about defvar in the latter). ;;; ;;; Look at https://www.gnu.org/software/emacs/manual/html_node/elisp/Disassembly.html ;;; for more examples of disassembly, ;;; https://www.emacswiki.org/emacs/ByteCodeEngineering for brief descriptions ;;; of bytecodes. ;; First, we need to be in lisp-interaction-mode for this. ;; So: M-x lisp-interaction-mode. ;; Second, switch emacs into the lexical scoping mode (setq lexical-binding t) t ;; Then let's define a function that makes a lambda depeding on (and ;; therefore closing over) a variable (defun make-counter (x) ; initial value #'(lambda (step) ; counter step. returns current value if 0 (if (eql step 0) x (setq x (+ x step))))) ; increments the value by step overwise make-counter ;; It would look nicer if we made the step argument &optional, so that ;; getting the value of the counter would be accomplished by a call ;; without arguments, and stepping it up would take one-non-zero argument. ;; It would make the bytecode only slightly more complex. ;; A somewhat tedious demo: (setq c1 (make-counter 1)) (closure ((x . 1) t) (step) (if (eql step 0) x (setq x (+ x step)))) ;; if you get something with "(lambda ...) at this step, ;; like "(lambda (step) (if (eql step 0) x (setq x (+ x step))))", ;; then you are _not_ in lexical binding mode (e.g., running a pre-24 Emacs) ;; You should get something starting matching "(closure ..)", like the above. (funcall c1 0) 1 (funcall c1 2) 3 (funcall c1 0) 3 (setq c100 (make-counter 100)) (closure ((x . 100) t) (step) (if (eql step 0) x (setq x (+ x step)))) (funcall c100 0) 100 (funcall c100 1) 101 (funcall c100 0) 101 (funcall c1 0) 3 ;; So, c1 and c100 are indeed closed over different captures of the "x" argument. Yawn. ;; OK, let's now see how that capture actually happens. (disassemble 'make-counter) ;; "not a function", oops. Indeed, it's a closure, a different object) (byte-compile 'make-counter) #[257 "\211C\300\301\302\303\304!\305\"\306\307%\207" [make-byte-code 257 "\301\302\"\203 \300\242\207\300\211\242\\\240\207" vconcat vector [eql 0] 4 "(fn STEP)"] 8 "(fn X)"] ;; This takes some understanding, and you will need to reread ;; http://nullprogram.com/blog/2014/01/04/ starting at "The Byte-code ;; Object" to parse this. ;; - 257 means 'this function takes exactly one required argument, with no &optional args, nor &rest'. ;; - "\211C\300\301\302\303\304^E!\305\"\306\307%\207" is compiled bytecode we'll see shortly ;; - the next argument is a vector of constants for the above bytecode. In turn, ;; it will make a bytecode function with: ;; -- exactly 1 required argument, ;; -- compiled bytecode "\301^A\302\"\203^@\300\242\207\300\211\242^B\\\240\207" ;; -- more constants and string arguments (disassemble (byte-compile 'make-counter)) nil ;; comments in the pasted contents are mine --------- pasted from disasm buffer --------- byte code: doc: ... args: 257 0 dup ; duplicate and listify the argument x 1 list1 ; listify x, top if stack now holds, looking from the top (x), x, step ; why this listification? it's for the sake of setq, implemented via setcar bytecode, ; which needs a quoted x 2 constant make-byte-code 3 constant 257 4 constant "\301^A\302\"\203\n^@\300\242\207\300\211\242^B\\\240\207" ; compiled bytecode for inner lambda returned by make-counter 5 constant vconcat 6 constant vector 7 stack-ref 5 ; get argument x to the top of the stack, ; which was vector, vconcat, "", 257, make-byte-code, (x), x 8 call 1 ; make a vector with it 9 constant [eql 0] ; concatentate with the vector [eql 0], constants needed by the above bytecode 10 call 2 ; and concatenate vectors, getting 11 constant 4 12 constant "\n\n(fn STEP)" ; substitute for the missing make-counter doc string 13 call 5 ; call make-byte-code with all these arguments 14 return --------- end disasm buffer --------- ;; disassembling the bytecode string at (4) is not automated, so we'll have to ;; to improvise, and also guess the result of the _vector_ and _vconcat_ calls. ;; Luckily, it's not hard, since we know that the stack will contains the results of ;; (0) and (1), i.e., (x) and x . So after the constants in (2--6) get pushed, ;; (x) will be in position 5 of the stack. So the constant vector will be [(x) eql 0]. (disassemble #[257 "\301\302\"\203\n\300\242\207\300\211\242\\\240\207" [(x) eql 0] 4 "(fn STEP)"]) nil nil --------- pasted from disasm buffer --------- byte code: doc: (fn STEP) ; for each bytecode, the stack _after_ it is shown: args: 257 ; stack: step 0 constant eql ; stack: eql step 1 stack-ref 1 ; stack: step eql step 2 constant 0 ; stack: 0 step eql step 3 call 2 ; stack: (eql step 0) step 4 goto-if-nil 1 ; if step!=0, goto 10:1 below, consume top of stack 7 constant (x) ; stack: (x) 8 car-safe ; stack: x 9 return 10:1 constant (x) ; stack: (x) step 11 dup ; stack: (x) (x) step 12 car-safe ; stack: x (x) step 13 stack-ref 2 ; stack: step x (x) step 14 plus ; stack: (+ x step) (x) step 15 setcar ; stack: x+step (x) step AND side-effect: x <- (+ x step) 16 return ; stack: x+step step ; x+step, being on top of the stack, is returned --------- end pasted --------- ;;;;;;--------------------------------------------------------------------------- ;;;;; You may wonder about the list1 list-wrapping the value of the closed-over variable. ;;;;; It seems to be how setq is handled by the byte-compiler. Below is my ;;;;; scratch buffer with some notes from poking at this. You can stop reading here. ;;;;; I should've just read the code of Emacs' byte-compile, really: ;;;;; https://www.emacswiki.org/emacs/ByteCodeEngineering --> bytecomp.el, etc. ;;;;; Experimenting with how setq is compiled. (disassemble (byte-compile '(setq x 1))) ;; <--don't run this!! ;; OK, this froze my Emacs. Apparently, disassemble spins forever if (byte-compile ..) fails. ;; I saved the buffer, though, by sending SIGINT to this process with "kill -2", from ;; an external shell, and got my Emacs cursor back. ;; "If in doubt, add lambda". Only functions and lambdas are compiled. (byte-compile '(lambda () (setq x 1))) #[0 "\301\211\207" [x 1] 2 " (fn)"] ;; Warning: assignment to free variable `x' (disassemble (byte-compile '(lambda () (setq x 1)))) nil byte code: doc: ... args: 0 --------- pasted from disasm buffer --------- 0 constant 1 1 dup 2 varset x 3 return ---------- end pasted ---------- (disassemble (byte-compile '(lambda (x) (setq x 1)))) nil --------- pasted from disasm buffer --------- byte code: doc: ... args: 257 0 constant 1 ; 1 x 1 dup ; 1 1 x 2 stack-set 2 ; 1 x 4 return ; 1-> ---------- end pasted ---------- (disassemble (byte-compile '(lambda (x) (progn (setq x '(1 2 3)) (car x))))) nil byte code: doc: ... args: 257 0 constant (1 2 3) ; (1 2 3) x 1 stack-set 1 ; x [ x:=(1 2 3)] 3 dup ; x x 4 car ; 1 x x 5 return ; (defun foo (x) (lambda (n) (setq x n))) ;; foo (byte-compile 'foo) #[257 "\300\301\302\303\304!\305\"\306\307%\207" [make-byte-code 257 "\207" vconcat vector [] 2 " (fn N)"] 7 " (fn X)"] (disassemble (byte-compile 'foo)) nil byte code: doc: ... args: 257 0 constant make-byte-code 1 constant 257 2 constant "\207" ; this bytecode is "return" 3 constant vconcat 4 constant vector 5 stack-ref 5 ; stack: x vector vconcat "\207" 257 make-byte-code 6 call 1 ; stack: [x] vconcat "\207" 257 make-byte-code 7 constant [] ; stack: [] [x] vconcat "\207" 257 make-byte-code 8 call 2 ; stack: [x] "\207" 257 make-byte-code 9 constant 2 ; stack: 2 [x] "\207" 257 make-byte-code 10 constant "\n\n(fn N)" ; stack: "\n\n(fn N)" 2 [x] "\207" 257 make-byte-code 11 call 5 ; (make-byte-code ..) for one-arg "return" ("\207") with constant vector [x] 12 return (disassemble #[257 "\207" [] ]) byte code: args: 257 0 return (defun foo (x) (lambda (n) (progn (setq x n) x))) foo (disassemble (byte-compile 'foo)) nil byte code: doc: ... args: 257 0 dup 1 list1 2 constant make-byte-code 3 constant 257 4 constant "\300^A\240\210\300\242\207" 5 constant vconcat 6 constant vector 7 stack-ref 5 8 call 1 9 constant [] 10 call 2 11 constant 3 12 constant "\n\n(fn N)" 13 call 5 14 return (disassemble #[257 "\300\240\210\300\242\207" [(x)] 3 "foostr"]) nil byte code: doc: foostr args: 257 0 constant (x) ; (x) n 1 stack-ref 1 ; n (x) n 2 setcar ; n n , side-effect x:=n 3 discard ; n 4 constant (x) ; (x) n 5 car-safe ; x n 6 return ; x-> ;;;