Elisp's VM is a fairly straightforward virtual machine. Its C code is easy enough to read, and illustrates the basic pattern of VMs for byte-compiled languages really well. In particular, it makes handling of closures really transparent. The VM works closely with the bytecode compiler, written in Elisp, http://cvs.savannah.gnu.org/viewvc/emacs/emacs/lisp/emacs-lisp/bytecomp.el?view=markup The compiler is substantially more complex than the VM; this is where code analysis happens, and the function byte-compile is defined. If you wonder where the staged call to "make-byte-code" comes from, it's in there, in byte-compile-byte-code-maker. But back to the VM. Look at http://cvs.savannah.gnu.org/viewvc/emacs/emacs/src/bytecode.c?view=markup Ignore METER* #ifdefs, they are for profiling the code. First come the #defines for the opcodes. They will be used in a huge switch(op){}, typical of VMs, in the bytecode execution entry point, Fbyte_code. Fbyte_code is really the "CPU emulator", sitting in an endless fetch-decode-execute loop; the opcodes are instructions, the byte_stack structures its "RAM banks". Fbyte_code is a C function, but is defined with a macro: ------------------------------------------------------------ DEFUN ("byte-code", Fbyte_code, Sbyte_code, 3, 3, 0, doc: /* Function used internally in byte-compiled code. The first argument, BYTESTR, is a string of byte code; the second, VECTOR, a vector of constants; the third, MAXDEPTH, the maximum stack depth used in this function. If the third argument is incorrect, Emacs may crash. */) (bytestr, vector, maxdepth) Lisp_Object bytestr, vector, maxdepth; { ------------------------------------------------------------ Look at the structure byte_stack and the byte_stack_list. Things to notice: - each stack has its own "program counter", which scans through the string containing the opcodes - stacks are stacks of Lisp_Object pointers. In LISP, of course, you always pass around references to objects like symbols or bignums or strings or cons cells; a cons cell consists of the two of those, etc. So all slots on the stack are same size, native pointer-size. Lisp_Object is defined in http://cvs.savannah.gnu.org/viewvc/emacs/emacs/src/lisp.h?view=markup It's a union type, wrapping a pointer, with a bunch of macros for extracting and converting the pointer to the actual type, like XCONS, XSYMBOL, XSTRING, XVECTOR, etc. The integer tag stored in Lisp_Object is given by enum Lisp_Type and determines what the wrapped pointer means and how to access the object through it. Essentially, this is a way of implementing a runtime type system where C's one is no adequate. We'll see this in Ruby, etc. - each stack has its own Lisp_Object constants, which is the "constants vector" that serves the code as the environment; linking up the bytecode string and this environment is what makes a "closure". - the actual space for the stack is allocated in the stack frame during the call to Fbyte_code, and this automatically deallocated when it returns. Maxdepth is used just then: see below, alloca( maxdepth * sizeof(Lisp_Object*)) You can think of byte_stack as a set of "registers" of the "virtual CPU", such as the PC or the SP. The "RAM" to which they point is in Fbyte_code's frame and on the heap. Look at the simple macros FETCH, PUSH, POP, TOP, DISCARD. Note that they go by pointers in "registers", to and from "RAM" (the stack). One simplification is that stacks get created and destroyed with every entry to Fbyte_code and every exit from it (caused by the Breturn opcode). Observe how at the beginning of Fbyte_code new stack is alloca()-ed and linked to the byte_stack_list; and how it's popped off byte_stack_list at exit from Fbyte_code. Search for all occurrences of byte_stack_list, and see the logic of it. In the notes of Elisp bytecode, I referred to a separate "control stack" for Elisp functions. Now you see that this control stack is simply the same stack that C used, and is subsumed by it, just as the Elisp data stacks are allocated within Fbyte_code's C stack frames (and automatically deallocated with them. It's an interesting design that piggy-backs on C for managing control and some memory management! on entry to Fbyte_code: stack.byte_string = bytestr; stack.pc = stack.byte_string_start = SDATA (bytestr); stack.constants = vector; stack.bottom = (Lisp_Object *) alloca (XFASTINT (maxdepth) // <---- * sizeof (Lisp_Object)); top = stack.bottom - 1; stack.top = NULL; stack.next = byte_stack_list; byte_stack_list = &stack; on exit from Fbyte_code: exit: byte_stack_list = byte_stack_list->next; return result; // <-- the Lisp_Object pointer on top of the stack } More about the execution flow of byte-compiled code: notice that the Bcall family of opcodes in the Fbyte_code switch(op) results in Ffuncall(): case Bcall: case Bcall+1: case Bcall+2: TOP = Ffuncall (op + 1, &TOP); AFTER_POTENTIAL_GC (); break; Ffuncall is defined in src/eval.c, http://cvs.savannah.gnu.org/viewvc/emacs/emacs/src/eval.c?view=markup and has many cases for calling internal C functions (go through some occurrences of DEFUN in this file to see some). In the case when the function passed in is a compiled bytecode object (and in a couple of other cases as well, such as "apply"---check them out) Ffuncall calls funcall_lambda: if (COMPILEDP (fun)) val = funcall_lambda (fun, numargs, args + 1); funcall_lambda, in turn, does some checking on the number of arguments, signals errors or created some bindings, and then, for the byte-compiled object case, calls val = Fbyte_code (AREF (fun, COMPILED_BYTECODE), AREF (fun, COMPILED_CONSTANTS), AREF (fun, COMPILED_STACK_DEPTH)); ---and we are back in the VM, with a new C stack frame for Fbyte_code and a new data stack pushed on the byte_stack_list by this new invocation of Fbyte_code. This new byte_stack is created from the byte-compiled object #[...] and gets the "constants vector" it stores, getting its closed variables back. Thus does Elisp bytecode implement closures. This mechanism combines with the no-lookup "stack-ref" way of how working on argument data on the stack is compiled---so, of course, the bytecode compiler has to do its part. Overall, the internals of Elisp are nicely readable, once you get get past some macros like DEFUN that complicate navigation a bit. I suggest you read through implementations of simple LISP functions that deal with cons cells, to get a hang of what such a code base implementing a simpler LISP looks like. This was the world before optimizations like SBCL's.