diff --git a/content/blog/12_compiler_let_in_lambda/index.md b/content/blog/12_compiler_let_in_lambda/index.md index 1a50fc9..72f3218 100644 --- a/content/blog/12_compiler_let_in_lambda/index.md +++ b/content/blog/12_compiler_let_in_lambda/index.md @@ -418,6 +418,178 @@ Recall that in this case, we need not have two methods for declaring and generating LLVM, since constructors don't reference other constructors, and are always generated before any function definitions. +#### Visibility +Should we really be turning _all_ free variables in a function definition +into arguments? Consider the following piece of Haskell code: + +```Haskell {linenos=table} +add x y = x + y +mul x y = x * y +something = mul (add 1 3) 3 +``` + +In the definition of `something`, `mul` and `add` occur free. +A very naive lifting algorithm might be tempted to rewrite such a program +as follows: + +```Haskell {linenos=table} +add x y = x + y +mul x y = x * y +something' add mul = mul (add 1 3) 3 +something = something' add mul +``` + +But that's absurd! Not only are `add` and `mul` available globally, +but such a rewrite generates another definition with free variables, +which means we didn't really improve our program in any way. From this +example, we can see that we don't want to be turning reference to global +variables into function parameters. But how can we tell if a variable +we're trying to operate on is global or not? I propose a flag in our +`type_env`, which we'll augment to be used as a symbol table. To do +this, we update the implementation of `type_env` to map variables to +values of a struct `variable_data`: + +{{< codelines "C++" "compiler/12/type_env.hpp" 13 22 >}} + +The `visibility` enum is defined as follows: + +{{< codelines "C++" "compiler/12/type_env.hpp" 10 10 >}} + +As you can see from the above snippet, we also added a `mangled_name` field +to the new `variable_data` struct. We will be using this field shortly. We +also add a few methods to our `type_env`, and end up with the following: + +{{< codelines "C++" "compiler/12/type_env.hpp" 31 44 >}} + +We will come back to `find_free` and `find_free_except`, as well as +`set_mangled_name` and `get_mangled_name`. For now, we just adjust `bind` to +take a visibility parameter that defaults to `local`, and implement +`is_global`: + +{{< codelines "C++" "compiler/12/type_env.cpp" 27 32 >}} + +Remember the `visibility::global` in `parser.y`? This is where that comes in. +Specifically, we recall that `definition_defn::insert_types` is responsible +for placing function types into the environment, making them accessible +during typechecking later. At this time, we already need to know whether +or not the definitions are global or local (so that we can create the binding). +Thus, we add `visibility` as a parameter to `insert_types`: + +{{< codelines "C++" "compiler/12/definition.hpp" 44 44 >}} + +Since we are now moving from manually wrangling definitions towards using +`definition_group`, we make it so that the group itself provides this +argument. To do this, we add the `visibility` field from before to it, +and set it in the parser. One more thing: since constructors never +capture variables, we can always move them straight to the global +scope, and thus, we'll always mark them with `visibility::global`. + +#### Managing Mangled Names +Just mangling names is not enough. Consider the following program: + +```text {linenos=table} +defn packOne x = { + let { + data Packed a = { Pack a } + } in { + Pack x + } +} +defn packTwo x = { + let { + data Packed a = { Pack a } + } in { + Pack x + } +} +``` + +{{< sidenote "right" "lifting-types-note" "Lifting the data type declarations" >}} +We are actually not quite doing something like the following snippet. +The reason for this is that we don't mangle the names for types. I pointed +out this potential issue in a sidenote in the previous post. Since the size +of this post is already balooning, I will not deal with this issue here. +Even at the end of this post, our compiler will not be able to distinguish +between the two Packed types. We will hopefully get to it later. +{{< /sidenote >}} and their constructors into the global +scope gives us something like: + +``` {linenos=table} +data Packed a = { Pack a } +data Packed_1 a = { Pack_1 a } +defn packOne x = { Pack x } +defn packTwo x = { Pack_1 x } +``` + +Notice that we had to rename one of the calls to `Pack` to be a call to +be `Pack_1`. To actually change our AST to reference `Pack_1`, we'd have +to traverse the whole tree, and make sure to keep track of definitions +that could shadow `Pack` further down. This is cumbersome. Instead, we +can mark a variable as referring to a mangled version of itself, and +access this information when needed. To do this, we add the `mangled_name` +field to the `variable_data` struct as we've seen above, and implement +the `set_mangled_name` and `get_mangled_name` methods. The former: + +{{< codelines "C++" "compiler/12/type_env.cpp" 34 37 >}} + +And the latter: + +{{< codelines "C++" "compiler/12/type_env.cpp" 39 45 >}} + +We don't allow the `set_mangled_name` to affect variables that are declared +above the receiving `type_env`, and use the empty string as a 'none' value. +Now, when lifting data type constructors, we'll be able to use +`set_mangled_name` to make sure constructor calls are made correctly. We +will also be able to use this in other cases, like the translation +of local function definitions. + +#### New AST Nodes +Finally, it's time for us to add new AST nodes to our language. +Specifically, these nodes are `ast_let` (for `let/in` expressions) +and `ast_lambda` for lambda functions. We declare them as follows: + +{{< codelines "C++" "compiler/12/ast.hpp" 131 166 >}} + +In `ast_let`, the `definitions` field corresponds to the original definitions +given by the user in the program, and the `in` field corresponds to the +expression which uses these definitions. In the process of lifting, though, +we eventually transfer each of the definitions to the global scope, replacing +their right hand sides with partial applications. After this transformation, +all the data type definitions are effectively gone, and all the function +definitions are converted into the simple form `x = f a1 ... an`. We hold +these post-transformation equations in the `translated_definitions` field, +and it's them that we compile in this node's `compile` method. + +In `ast_lambda`, we allow multiple parameters (like Haskell's `\x y -> x + y`). +We store these parameters in the `params` field, and we store the lambda's +expression in the `body` field. Just like `definition_defn`, +the `ast_lambda` node maintains a separate environment in which its children +have been bound, and a list of variables that occur freely in its body. The +former is used for typechecking, while the latter is used for lifting. +Finally, the `translated` field holds the lambda function's form +after its body has been transformed into a global function. Similarly to +`ast_let`, this node will be in the form `f a1 ... an`. + +The +observant reader will have noticed that we have a new method: `translate`. +This is a new method for all `ast` descendants, and will implement the +steps of moving definitions to the global scope and transforming the +program. Before we get to it, though, let's quickly see the parsing +rules for `ast_let` and `ast_lambda`: + +{{< codelines "text" "compiler/12/parser.y" 107 115 >}} + +This is pretty similar to the rest of the grammar, so I will give this no +further explanation. + +{{< todo >}} +Explain typechecking for lambda functions and let/in expressions. +{{< /todo >}} + +{{< todo >}} +Explain free variable detection for lambda functions and let/in expressions. +{{< /todo >}} + #### Translation While collecting all of the definitions into a global list, we can also do some program transformations. Let's return to our earlier example: