Start working on part 13 of compiler series.
This commit is contained in:
parent
fe1e0a6de0
commit
1f6b4bef74
214
content/blog/13_compiler_cleanup_optimization/index.md
Normal file
214
content/blog/13_compiler_cleanup_optimization/index.md
Normal file
|
@ -0,0 +1,214 @@
|
|||
---
|
||||
title: Compiling a Functional Language Using C++, Part 13 - More Improvements
|
||||
date: 2020-09-10T18:50:02-07:00
|
||||
tags: ["C and C++", "Functional Languages", "Compilers"]
|
||||
description: "In this post, we clean up our compiler and add some basic optimizations."
|
||||
---
|
||||
|
||||
In [part 12]({{< relref "12_compiler_let_in_lambda" >}}), we added `let/in`
|
||||
and lambda expressions to our compiler. At the end of that post, I mentioned
|
||||
that before we move on to bigger and better things, I wanted to take a
|
||||
step back and clean up the compiler.
|
||||
|
||||
Recently, I got around to doing that. Unfortunately, I also got around to doing
|
||||
a lot more. Furthermore, I managed to make the changes in such a way that I
|
||||
can't cleanly separate the 'cleanup' and 'optimization' portions of my work.
|
||||
This is partially due to the way in which I organize code, where each post
|
||||
is associated with a version of the compiler with the necessary changes.
|
||||
Because of all this, instead of making this post about the cleanup, and the
|
||||
next post about the optimizations, I have to merge them into one.
|
||||
|
||||
So, this post is split into two major portions: cleanup, which deals mostly
|
||||
with touching up exceptions and improving the 'name mangling' logic, and
|
||||
optimizations, which deals with adding special treatment to booleans,
|
||||
unboxing integers, and implementing more binary operators.
|
||||
|
||||
### Section 1: Cleanup
|
||||
|
||||
The previous post was
|
||||
{{< sidenote "right" "long-note" "rather long," >}}
|
||||
Probably not as long as this one, though! I really need to get the
|
||||
size of my posts under control.
|
||||
{{< /sidenote >}} which led me to omit
|
||||
a rather important aspect of the compiler: proper error reporting.
|
||||
Once again our compiler has instances of `throw 0`, which is a cheap way
|
||||
of avoiding properly handling a runtime error. Before we move on,
|
||||
it's best to get rid of such blatantly lazy code.
|
||||
|
||||
Our existing exceptions (mostly type errors) can use some work, too.
|
||||
Even the most descriptive issues our compiler reports -- unification errors --
|
||||
don't include the crucial information of _where_ the error is. For large
|
||||
programs, this means having to painstakingly read through the entire file
|
||||
to try figure out which subexpression could possibly have an incorrect type.
|
||||
This is far from the ideal debugging experience.
|
||||
|
||||
Addressing all this is a multi-step change in itself. We want to:
|
||||
|
||||
* Replace all `throw 0` code with actual exceptions.
|
||||
* Replace some exceptions that shouldn't be possible for a user to trigger
|
||||
with assertions.
|
||||
* Keep track of source locations of each subexpression, so that we may
|
||||
be able to print it if it causes an error.
|
||||
* Be able to print out said source locations at will. This isn't
|
||||
a _necessity_, but virtually all "big" compilers do this. Instead
|
||||
of reporting that an error occurs on a particular line, we will
|
||||
actually print the line.
|
||||
|
||||
Let's start with gathering the actual location data.
|
||||
|
||||
#### Bison's Locations
|
||||
Bison actually has some rather nice support for location tracking. It can
|
||||
automatically assemble the "from" and "to" locations of a nonterminal
|
||||
from the locations of children, which would be very tedious to write
|
||||
by hand. We enable this feature using the following option:
|
||||
|
||||
{{< codelines "text" "compiler/13/parser.y" 50 50 >}}
|
||||
|
||||
There's just one hitch, though. Sure, Bison can compute bigger
|
||||
locations from smaller ones, but it must get the smaller ones
|
||||
from somewhere. Since Bison operates on _tokens_, rather
|
||||
than _characters_, it effectively doesn't interact with the source
|
||||
text at all, and can't determine from which line or column a token
|
||||
originated. The task of determining the locations of input tokens
|
||||
is delegated to the tokenizer -- Flex, in our case. Flex, on the
|
||||
other hand, doesn't doesn't have a built-in mechanism for tracking
|
||||
locations. Fortunately, Bison provides a `yy::location` class that
|
||||
includes most of the needed functionality.
|
||||
|
||||
A `yy::location` consists of `begin` and `end` source position,
|
||||
which themselves are represented using lines and columns. It
|
||||
also has the following methods:
|
||||
|
||||
* `yy::location::columns(int)` advances the `end` position by
|
||||
the given number of columns, while `begin` stays the same.
|
||||
If `begin` and `end` both point to the beginning of a token,
|
||||
then `columns(token_length)` will move `end` to the token's end,
|
||||
and thus make the whole `location` contain the token.
|
||||
* `yy::location::lines(int)` behaves similarly to `columns`,
|
||||
except that it advances `end` by the given number of lines,
|
||||
rather than columns.
|
||||
* `yy::location::step()` moves `begin` to where `end` is. This
|
||||
is useful for when we've finished processing a token, and want
|
||||
to move on to the next one.
|
||||
|
||||
For Flex specifically, `yyleng` has the length of the token
|
||||
currently being processed. Rather than adding the calls
|
||||
to `columns` and `step` to every rule, we can define the
|
||||
`YY_USER_ACTION` macro, which is run before each token
|
||||
is processed.
|
||||
|
||||
{{< codelines "C++" "compiler/13/scanner.l" 12 12 >}}
|
||||
|
||||
We'll see why we are using `drv` soon; for now, you can treat
|
||||
`location` as if it were a global variable declared in the
|
||||
tokenizer. Before processing each token, we ensure that
|
||||
`location` has its `begin` and `end` at the same position,
|
||||
and then advance `end` by `yyleng` columns. This is sufficient
|
||||
to make `location` represent our token's source position.
|
||||
|
||||
So now we have a "global" variable `location` that gives
|
||||
us the source position of the current token. To get it
|
||||
to Bison, we have to pass it as an argument to each
|
||||
of the `make_TOKEN` calls. Here are a few sample lines
|
||||
that should give you the general idea:
|
||||
|
||||
{{< codelines "C++" "compiler/13/scanner.l" 41 44 >}}
|
||||
|
||||
That last line is actually new. Previously, we somehow
|
||||
got away without explicitly sending the EOF token to Bison.
|
||||
I suspect that this was due to some kind of implicit conversion
|
||||
of the Flex macro `YY_NULL` into a token; now that we have
|
||||
to pass a position to every token constructor, such an implicit
|
||||
conversion is probably impossible.
|
||||
|
||||
Now we have Bison computing source locations for each nonterminal.
|
||||
However, at the moment, we still aren't using them. To change that,
|
||||
we need to add a `yy::location` argument to each of our `ast` nodes,
|
||||
as well as to the `pattern` subclasses, `definition_defn` and
|
||||
`definition_data`. To avoid breaking all the code that creates
|
||||
AST nodes and definitions outside of the parser, we'll make this
|
||||
argument optional. Inside of `ast.hpp`, we define it as follows:
|
||||
|
||||
{{< codelines "C++" "compiler/13/ast.hpp" 16 16 >}}
|
||||
|
||||
Then, we add a constructor to `ast` as follows:
|
||||
|
||||
{{< codelines "C++" "compiler/13/ast.hpp" 18 18 >}}
|
||||
|
||||
Note that it's not default here, since `ast` itself is an
|
||||
abstract class, and thus will never be constructed directly.
|
||||
It is in the subclasses of `ast` that we provide a default
|
||||
value. The change is rather mechanical, but here's an example
|
||||
from `ast_binop`:
|
||||
|
||||
{{< codelines "C++" "compiler/13/ast.hpp" 98 99 >}}
|
||||
|
||||
#### Line Offsets, File Input, and the Parse Driver
|
||||
There are three more challenges with printing out the line
|
||||
of code where an error occurred. First of all, to
|
||||
print out a line of code, we need to have that line of code
|
||||
available to us. We do not currently meet this requirement:
|
||||
our compiler reads code form `stdin` (as is default for Flex),
|
||||
and `stdin` doesn't always support rewinding. This, in turn,
|
||||
means that once Flex has read a character from the input,
|
||||
it may not be possible to go back and retrieve that character
|
||||
again.
|
||||
|
||||
Second, even if we do have have the entire stream or buffer
|
||||
available to us, to retrieve an offset and length within
|
||||
that buffer from just a line and column number would be a lot
|
||||
of work. A naive approach would be to iterate through
|
||||
the input again, once more keeping track of lines and columns,
|
||||
and print the desired line once we reach it. However, this
|
||||
would lead us to redo a lot of work that our tokenizer
|
||||
is already doing.
|
||||
|
||||
Third, Flex's input mechanism, even if it it's configured
|
||||
not to read from `stdin`, uses a global file descriptor called
|
||||
`yyin`. However, we're better off minimizing global state (especially
|
||||
if we want to read, parse, and compile multiple files in
|
||||
the future). While we're configuring Flex's input mechanism,
|
||||
we may as well fix this, too.
|
||||
|
||||
There are several approaches to fixing the first issue. One possible
|
||||
way is to store the content of `stdin` into a temporary file. Then,
|
||||
it's possible to read from the file multiple times by using
|
||||
the C functions `fseek` and `rewind`. However, since we're
|
||||
working with files, why not just work directly with the files
|
||||
created by the user? Instead of reading from `stdin`, we may
|
||||
as well take in a path to a file via `argv`, and read from there.
|
||||
Also, instead of `fseek` and `rewind`, we can just read the file
|
||||
into memory, and access it like a normal character buffer.
|
||||
|
||||
To address the second issue, we can keep a mapping of line numbers
|
||||
to their locations in the source buffer. This is rather easy to
|
||||
maintain using an array: the first element of the array is 0,
|
||||
which is the beginning of any line in any source file. From there,
|
||||
every time we encounter the character `\n`, we can push
|
||||
the current source location to the top, marking it as
|
||||
the beginning of another line. Where exactly we store this
|
||||
array is as yet unclear, since we're trying to avoid global variables.
|
||||
|
||||
Finally, begin addressing the third issue, we can use Flex's `reentrant`
|
||||
option, which makes it so that all of the tokenizer's state is stored in an
|
||||
opaque `yyscan_t` structure, rather than in global variables. This way,
|
||||
we can configure `yyin` without setting a global variable, which is a step
|
||||
in the right direction. We'll work on this momentarily.
|
||||
|
||||
Our tokenizing and parsing stack has more global variables
|
||||
than just those specific to Flex. Among these variables is `global_defs`,
|
||||
which receives all the top-level function and data type definitions. We
|
||||
will also need some way of accessing the `yy::location` instance, and
|
||||
a way of storing our file input in memory. Fortunately, we're not
|
||||
the only ones to have ever come across the issue of creating non-global
|
||||
state: the Bison documentation has a
|
||||
[section in its C++ guide](https://www.gnu.org/software/bison/manual/html_node/Calc_002b_002b-Parsing-Driver.html) that describes a technique for manipulating
|
||||
state -- "parsing context", in their words. This technique involves the
|
||||
creation of a _parsing driver_.
|
||||
|
||||
The parsing driver is a class (or struct) that holds all the parse-related
|
||||
state. We can arrange for this class to be available to our tokenizing
|
||||
and parsing functions, which will allow us to use it pretty much like we'd
|
||||
use a global variable. We can define it as follows:
|
||||
|
||||
{{< codelines "C++" "compiler/13/parse_driver.hpp" 14 34 >}}
|
Loading…
Reference in New Issue
Block a user