|
| 1 | +# Compiler: Design |
| 2 | + |
| 3 | +Drawing from the [VM design](./chapter-interp-vm-design.md), the compiler must |
| 4 | +support the following language constructs: |
| 5 | + |
| 6 | +* function definitions |
| 7 | +* anonymous functions |
| 8 | +* function calls |
| 9 | +* lexical scoping |
| 10 | +* closures |
| 11 | +* local variables |
| 12 | +* global variables |
| 13 | +* expressions |
| 14 | + |
| 15 | +This is a minimally critical set of features that any further language |
| 16 | +constructs can be built on while ensuring that our compiler remains easy to |
| 17 | +understand for the purposes of this book. |
| 18 | + |
| 19 | +[Our parser, recall](./chapter-interp-parsing.md), reads in s-expression syntax |
| 20 | +and produces a nested `Pair` and `Symbol` based abstract syntax tree. Adding |
| 21 | +other types - integers, strings, arrays etc - is mostly a matter of expanding |
| 22 | +the parser. The compiler as described here, being for a dynamically typed |
| 23 | +language, will support them without refactoring. |
| 24 | + |
| 25 | +## Eval/apply |
| 26 | + |
| 27 | +Our compiler design is based on the _eval/apply_ pattern. |
| 28 | + |
| 29 | +In this pattern we recursively descend into the `Pair` AST, calling _eval_ on |
| 30 | +the root node of the expression to be compiled. |
| 31 | + |
| 32 | +_Eval_ is, of course, short for "evaluate" - we want to evaluate the given |
| 33 | +expression. In the case of a compiler, we don't want the result yet, rather |
| 34 | +the sequence of instructions that will generate the result. |
| 35 | + |
| 36 | +More concretely, _eval_ looks at the node in the AST it is given and if it |
| 37 | +resolves to fetching a value for a variable, it generates that instruction; |
| 38 | +otherwise if it is a compound expression, the arguments are evaluated and then |
| 39 | +the function and arguments are passed to _apply_, which generates appropriate |
| 40 | +function call instructions. |
| 41 | + |
| 42 | +### Implementing Eval |
| 43 | + |
| 44 | +_Eval_ looks at the given node and attempts to generate an instruction for it |
| 45 | +that would resolve the node to a value - that is, evaluate it. |
| 46 | + |
| 47 | +#### Symbols |
| 48 | + |
| 49 | +If the node is a special symbol, such as `nil` or `true`, then it is treated as |
| 50 | +a literal and an instruction is generated to load that literal symbol into the |
| 51 | +next available register. |
| 52 | + |
| 53 | +Otherwise if the node is any other symbol, it is assumed to be bound to a value |
| 54 | +(it must be a variable) and an instruction is generated for fetching the value |
| 55 | +into a register. |
| 56 | + |
| 57 | +Variables come in three kinds: local, nonlocal or global. |
| 58 | + |
| 59 | +**Local**: the symbol has been declared earlier in the expression (either it is |
| 60 | +a function parameter or it was declared using `let`) and the compiler already |
| 61 | +has a record of it. The symbol is already associated with a local register |
| 62 | +index and a simple register copy instruction is generated. |
| 63 | + |
| 64 | +**Nonlocal**: the symbol has been bound in a parent nesting function. Again, |
| 65 | +the compiler already has a record of the declaration, which register is |
| 66 | +associated with the symbol and which relative call frame will contain that |
| 67 | +register. An upvalue lookup instruction is generated. |
| 68 | + |
| 69 | +**Global**: if the symbol isn't found as a local binding or a nonlocal binding, |
| 70 | +it is assumed to be a global, and a late-binding global lookup instruction is |
| 71 | +generated. In the event the programmer has misspelled a variable name, this is |
| 72 | +possibly the instruction that will be generated and the programmer will see an |
| 73 | +unknown-variable error at runtime. |
| 74 | + |
| 75 | +#### Expressions and function calls |
| 76 | + |
| 77 | +When _eval_ is passed a `Pair`, this represents the beginning of an expression, |
| 78 | +a function call. A composition of things. |
| 79 | + |
| 80 | +In s-expression syntax, all expressions and function calls looks like |
| 81 | +`(function_name arg1 arg2)`. That is parsed into a `Pair` tree, which takes |
| 82 | +the form: |
| 83 | + |
| 84 | +``` |
| 85 | +Pair( |
| 86 | + Symbol(function_name), |
| 87 | + Pair( |
| 88 | + Symbol(arg1), |
| 89 | + Pair( |
| 90 | + Symbol(arg2), |
| 91 | + nil |
| 92 | + ) |
| 93 | + ) |
| 94 | +) |
| 95 | +``` |
| 96 | + |
| 97 | +It is _apply_'s job to handle this case, so _eval_ extracts the first and |
| 98 | +second values from the outermost `Pair` and passes them into apply. In more |
| 99 | +general terms, _eval_ calls _apply_ with the function name and the argument |
| 100 | +list and leaves the rest up to _apply_. |
| 101 | + |
| 102 | +### Implementing Apply |
| 103 | + |
| 104 | +_Apply_ takes a function name and a list of arguments. First it recurses into |
| 105 | +_eval_ for each argument expression, then generates instructions to call the |
| 106 | +function with the argument results. |
| 107 | + |
| 108 | +#### Calling functions |
| 109 | + |
| 110 | +Functions are either built into to the language and VM or are |
| 111 | +library/user-defined functions composed of other functions. |
| 112 | + |
| 113 | +In every case, the simplified pattern for function calls is: |
| 114 | + |
| 115 | +* allocate a register to write the return value into |
| 116 | +* _eval_ each of the arguments in sequence, allocating their resulting values |
| 117 | + into consequent registers |
| 118 | +* compile the function call opcode, giving it the number of argument registers |
| 119 | + it should expect |
| 120 | + |
| 121 | +Compiling a call to a builtin function might translate directly to a dedicated |
| 122 | +bytecode operation. For example, querying whether a value is `nil` with builtin |
| 123 | +function `nil?` compiles 1:1 to a bytecode operation that directly represents |
| 124 | +that query. |
| 125 | + |
| 126 | +Compiling a call to a user defined function is a more involved. In it's more |
| 127 | +general form, supporting first class functions and closures, a function call |
| 128 | +requires two additional pointers to be placed in registers. The complete |
| 129 | +function call register allocation looks like this: |
| 130 | + |
| 131 | +| Register | Use | |
| 132 | +|----------|-----| |
| 133 | +| 0 | reserved for return value | |
| 134 | +| 1 | reserved for closure environment pointer | |
| 135 | +| 2 | first argument | |
| 136 | +| 3 | second argument | |
| 137 | +| ... | | |
| 138 | +| n | function pointer | |
| 139 | + |
| 140 | +If a closure is called, the closure object itself contains a pointer to it's |
| 141 | +environment and the function to call and those pointers can be copied over to |
| 142 | +registers. Otherwise, the closure environment pointer will be a `nil` pointer. |
| 143 | + |
| 144 | +The VM, when entering a new function, will represent the return value register |
| 145 | +always as the zeroth register. |
| 146 | + |
| 147 | +When the function call returns, all registers except the return value are |
| 148 | +discarded. |
| 149 | + |
| 150 | +#### Compiling functions |
| 151 | + |
| 152 | +Let's look at a simple function definition: |
| 153 | + |
| 154 | +``` |
| 155 | +(def is_true (x) |
| 156 | + (is? x true)) |
| 157 | +``` |
| 158 | + |
| 159 | +This function has a name `is_true`, takes one argument `x` and evaluates one |
| 160 | +expression `(is? x true)`. |
| 161 | + |
| 162 | +The same function may be written without a name: |
| 163 | + |
| 164 | +``` |
| 165 | +(lambda (x) (is? x true)) |
| 166 | +``` |
| 167 | + |
| 168 | +Compiling a function requires a few inputs: |
| 169 | + |
| 170 | +* an optional reference to a parent nesting function |
| 171 | +* an optional function name |
| 172 | +* a list of argument names |
| 173 | +* a list of expressions that will compute the return value |
| 174 | + |
| 175 | +The desired output is a data structure that combines: |
| 176 | + |
| 177 | +* the optional function name |
| 178 | +* the argument names |
| 179 | +* the compiled bytecode |
| 180 | + |
| 181 | +First, a scope structure is established. A scope is a lexical block in which |
| 182 | +variables are bound and unbound. In the compiler, this structure is simply a |
| 183 | +mapping of variable name to the register number that contains the value. |
| 184 | + |
| 185 | +The first variables to be bound in the function's scope are the argument names. |
| 186 | +The compiler, given the list of argument names to the function and the order in |
| 187 | +which the arguments are given, associates each argument name with the register |
| 188 | +number that will contain it's value. As we saw above, these are predictably and |
| 189 | +reliably registers 2 and upward, one for each argument. |
| 190 | + |
| 191 | +A scope may have a parent scope if the function is defined within another |
| 192 | +function. This is how nonlocal variable references will be looked up. We will |
| 193 | +go further into that when we discuss closures. |
| 194 | + |
| 195 | +The second step is to _eval_ each expression in the function, assigning the |
| 196 | +result to register 0, the preallocated return value register. The result of |
| 197 | +compiling each expression via _eval_ is bytecode. |
| 198 | + |
| 199 | +Thirdly and finally, a function object is instantiated, given it's name, the |
| 200 | +argument names and the bytecode. |
| 201 | + |
| 202 | +#### Compiling closures |
| 203 | + |
| 204 | +During compilation of the expressions within a function, if any of those |
| 205 | +expressions reference nonlocal variables (that is, variables not declared |
| 206 | +within the scope of the function) then the function object needs additional |
| 207 | +data to describe how to access those nonlocal variables at runtime. |
| 208 | + |
| 209 | +In the below example, the anonymous inner function references the parameter |
| 210 | +`n` to the outer function, `n`. When the inner function is returned, the value |
| 211 | +of `n` must be carried with it even after the stack scope of the outer function |
| 212 | +is popped and later overwritten with values for other functions. |
| 213 | + |
| 214 | +``` |
| 215 | +(def make_adder (n) |
| 216 | + (lambda (x) (+ x n)) |
| 217 | +) |
| 218 | +``` |
| 219 | + |
| 220 | +_Eval_, when presented with a symbol to evaluate that has not been declared in |
| 221 | +the function scope, searches outer scopes next. If a binding is found in an |
| 222 | +outer scope, a nonlocal reference is added to the function's _local_ scope |
| 223 | +that points to the outer scope and a `GetUpvalue` instruction is compiled. |
| 224 | + |
| 225 | +This nonlocal reference is a combination of two values: a count of stack |
| 226 | +frames to skip over to find the outer scope variable and the register offset in |
| 227 | +that stack frame. |
| 228 | + |
| 229 | +Non-local references are added to the function object that is returned by the |
| 230 | +function compiler. The VM will use these to identify the absolute location on |
| 231 | +the stack where a nonlocal variable should be read from and create upvalue |
| 232 | +objects at runtime when a variable is closed over. |
| 233 | + |
| 234 | +#### Compiling let |
| 235 | + |
| 236 | +Let is the declaration of variables and assigning values: the binding of |
| 237 | +values, or the results of expressions, to symbols. Secondly, it provides |
| 238 | +space to evaluate expressions that incorporate those variables. |
| 239 | + |
| 240 | +Here we bind the result of `(make_adder 3)` - a function - to the symbol |
| 241 | +`add_3` and then call `add_3` with argument `4`. |
| 242 | + |
| 243 | +``` |
| 244 | +(let ((add_3 (make_adder 3))) |
| 245 | + (add_3 4)) |
| 246 | +``` |
| 247 | + |
| 248 | +The result of the entire `let` expression should be `7`. |
| 249 | + |
| 250 | +Compiling `let` simply introduces additional scopes within a function scope. |
| 251 | +That is, instead of a function containing a single scope for all it's |
| 252 | +variables, scopes are nested. A stack of scopes is needed, with the parameters |
| 253 | +occupying the outermost scope. |
| 254 | + |
| 255 | +First a new scope is pushed on to the scope stack and each symbol being bound |
| 256 | +is added to the new scope. |
| 257 | + |
| 258 | +To generate code, a result register is reserved and a register for each binding |
| 259 | +is reserved. |
| 260 | + |
| 261 | +Finally, each expression is evaluated and the scope is popped, removing the |
| 262 | +bindings from view. |
| 263 | + |
| 264 | +## Register allocation |
| 265 | + |
| 266 | +A function call may make use of no more than 256 registers. Recall from earlier |
| 267 | +that the 0th register is reserved for the function return value and subsequent |
| 268 | +registers are reserved for the function arguments. |
| 269 | + |
| 270 | +Beyond these initial registers the compiler uses a simple strategy in register |
| 271 | +allocation: if a variable (a parameter or a `let` binding) is declared, it is |
| 272 | +allocated a register based on a stack discipline. Thus, variables are |
| 273 | +essentially pushed and popped off the register stack as they come into and out |
| 274 | +of scope. |
| 275 | + |
| 276 | +This strategy primarily ensures code simplicity - there is no register |
| 277 | +allocation optimization. |
| 278 | + |
| 279 | +## C'est tout! |
| 280 | + |
| 281 | +That covers the VM and compiler design at an overview level. We've glossed over |
| 282 | +a lot of detail but the next chapters will expose the implementation detail. |
| 283 | +Get ready! |
0 commit comments