Next: , Previous: , Up: A Virtual Machine for Guile   [Contents][Index]

9.3.5 Compiled Procedures are VM Programs

By default, when you enter in expressions at Guile’s REPL, they are first compiled to VM object code, then that VM object code is executed to produce a value. If the expression evaluates to a procedure, the result of this process is a compiled procedure.

A compiled procedure is a compound object, consisting of its bytecode, a reference to any captured lexical variables, an object array, and some metadata such as the procedure’s arity, name, and documentation. You can pick apart these pieces with the accessors in (system vm program). See Compiled Procedures, for a full API reference.

The object array of a compiled procedure, also known as the object table, holds all Scheme objects whose values are known not to change across invocations of the procedure: constant strings, symbols, etc. The object table of a program is initialized right before a program is loaded with load-program. See Loading Instructions, for more information.

Variable objects are one such type of constant object: when a global binding is defined, a variable object is associated to it and that object will remain constant over time, even if the value bound to it changes. Therefore, toplevel bindings only need to be looked up once. Thereafter, references to the corresponding toplevel variables from within the program are then performed via the toplevel-ref instruction, which uses the object vector, and are almost as fast as local variable references.

We can see how these concepts tie together by disassembling the foo function we defined earlier to see what is going on:

scheme@(guile-user)> (define (foo a) (lambda (b) (list foo a b)))
scheme@(guile-user)> ,x foo
   0    (assert-nargs-ee/locals 1)      
   2    (object-ref 1)                  ;; #<procedure 8ebec20 at <current input>:0:17 (b)>
   4    (local-ref 0)                   ;; `a'
   6    (make-closure 0 1)              
   9    (return)                        

Disassembly of #<procedure 8ebec20 at <current input>:0:17 (b)>:

   0    (assert-nargs-ee/locals 1)      
   2    (toplevel-ref 1)                ;; `foo'
   4    (free-ref 0)                    ;; (closure variable)
   6    (local-ref 0)                   ;; `b'
   8    (list 0 3)                      ;; 3 elements         at (unknown file):0:29
  11    (return)                        

First there’s some prelude, where foo checks that it was called with only 1 argument. Then at ip 2, we load up the compiled lambda. Ip 4 loads up ‘a’, so that it can be captured into a closure by at ip 6—binding code (from the compiled lambda) with data (the free-variable vector). Finally we return the closure.

The second stanza disassembles the compiled lambda. After the prelude, we note that toplevel variables are resolved relative to the module that was current when the procedure was created. This lookup occurs lazily, at the first time the variable is actually referenced, and the location of the lookup is cached so that future references are very cheap. See Top-Level Environment Instructions, for more details.

Then we see a reference to a free variable, corresponding to a. The disassembler doesn’t have enough information to give a name to that variable, so it just marks it as being a “closure variable”. Finally we see the reference to b, then the list opcode, an inline implementation of the list scheme routine.

Next: , Previous: , Up: A Virtual Machine for Guile   [Contents][Index]