Playing with Flame IR
Now that we have a working Flame-based compiler, we can use its command-line options to take a look at the on-disk representation of Flame IR. This representation can be helpful when building a compiler, because it allows you to visualize the code generated by your front-end for a given input file. Comparing the IR to the input source code is often a useful and effective way to track down bugs.
Note: if you just want to build a Brainfuck compiler with Flame, then I guess you can skip this part. If you want to understand how Flame works, then I recommend that you read on. If you're not sure what to do, then you can always skip this chapter and come back later.
Compiling to textual IR
The following command will compile a program to textual Flame IR.
$ flame-brainfuck.exe tests/mirror/mirror.bf -platform ir -S -runtime clr
These flags can be interpreted like so:
- the
-platform iroption tells the compiler to use the on-disk IR back-end, - the
-Sflag tells the IR back-end to produce textual IR – it produces binary IR files by default – and, the
-runtime clroption tells the compiler to target the CLR runtime. This means that the compiler will generate an IR assembly, with the intention to compile that to a CLR assembly eventually.The practical consequences of this flag are that the CLR's type system will be used, and that the BCL will be imported as standard library. If we hadn't passed in the
-runtime clrflag, then we wouldn't be able to findSystem.Console. Specifically, we'd get the following output, plus colors.$ flame-brainfuck.exe tests/mirror/mirror.bf -platform ir -S warning: unknown runtime: no runtime was associated with 'ir'. You can specify one explicitly by passing '-runtime' followed by some known runtime identifier. [-Wunknown-runtime] warning: unknown environment: no environment was associated with 'ir'. You can specify one explicitly by passing '-environment' followed by some known runtime identifier. [-Wunknown-environment] warning: missing dependency: could not resolve runtime library 'PortableRT'. [-Wmissing-dependency] warning: missing dependency: could not resolve runtime library 'System'. [-Wmissing-dependency] warning: missing dependency: could not resolve runtime library 'System.Core'. [-Wmissing-dependency] error: missing dependency: could not resolve type 'System.Console'.The initial cascade of warnings informs us that we have not specified a standard library or a type system, and that we couldn't find the default runtime libraries. This is followed by an error – we coded this one ourselves!
Inspecting the IR
Now that we've generated textual IR, we can open it in a text editor or print it to the console. The resulting file's called tests/mirror/bin/mirror.fir.
$ cat tests/mirror/bin/mirror.fir
#external_dependency(mscorlib);
#type_table({
// 0: System.Console
#type_reference(@System.Console);
// 1: System.String
#string;
// 2: mirror.Program
#type_reference(@mirror.Program);
// 3: System.String[]
#array_type(#type_table_reference(1), 1);
// 4: System.Void
#void;
});
#method_table({
// 0: static System.Void System.Console.WriteLine(System.String)
#method_reference(#type_table_reference(0), WriteLine, @true, { }, #void, {
#type_table_reference(1);
});
// 1: static System.Void mirror.Program.Main(System.String[])
#method_reference(#type_table_reference(2), Main, @true, { }, #void, {
#type_table_reference(3);
});
});
#field_table({ });
#assembly(#member(mirror), #version(1, 0, 0, 0), #method_table_reference(1), { }, {
#namespace(#member(mirror), {
#type_definition(#member(Program, #public, #static_type), { }, { }, { }, {
#method(#member(Main, #public), { }, @true, #type_table_reference(4), {
#param(#member(args), #type_table_reference(3));
}, { }, {
#ignore(#invoke(#get_delegate(#method_table_reference(0), @null), #const_string("Hello World!")));
#return();
});
});
}, { });
});
I don't think going into the details of textual IR here is helpful – the finer details of Flame IR tend to change as Flame evolves – but I suppose a high-level overview won't hurt.
LES and Loyc trees
The first thing you might notice by glancing at the file above is that it's really LES, a syntax for Loyc trees. Fun fact: the binary IR format contains the exact same Loyc trees, but they're formatted using the binary Loyc tree encoding, which is (usually) more compact.
If you don't know what LES or Loyc are, then that's fine: they're not essential to getting a basic understanding of how Flame works – though I'd still recommend you head over to the Loyc website and take a look. Loyc is a pretty cool project!
Type, method and field tables
The second thing you might notice is that over half of the IR consists of tables. Flame IR defines references to types, methods and fields in tables. These tables are indexed when an element they contain is required. For example, #type_table_reference(0) refers to the first element in the type table, i.e.,
// 0: System.Console
#type_reference(@System.Console);
So #type_table_reference(0) is really just a shortcut for the System.Console type.
The rationale behind this design is that real-life programs tend to use the same types, methods and fields over and over again. When these entities are stored in tables, an IR parser need only resolve them once. This is significantly more efficient than resolving types every time they are encountered.
The illusive #member nodes
Flame IR is chock-full of #member nodes. These consist of a name followed by a zero or more attributes. Let's take a look at some of the constructs we've defined, and their associated #member nodes.
#type_definition(#member(Program, #public, #static_type), ...)definespublic static class Program.#method(#member(Main, #public), ...)definespublicmethodMain.
#member nodes are everywhere in Flame IR because both names and attributes are pervasive in Flame IR, and they almost always occur together.
Inspecting Main's method body
Let's take a relatively close look at Main's method body.
{
#ignore(#invoke(#get_delegate(#method_table_reference(0), @null), #const_string("Hello World!")));
#return();
}
What's interesting about this snippet of code is that it corresponds almost directly to the in-memory IR we generated in GetMainBody. We have a block that contains two statements. The first loads a delegate to void System.Console.WriteLine(string) with a null receiver object, and invokes it with a single string literal argument. The second statement returns control to the caller.
Note: loading a delegate and then invoking it is how Flame handles method calls. This representation is for uniformity only; the CLR back-end will optimize the code sequence above to a direct call. So there's no performance hit at all.
We could probably delve even more deeply into the details of Flame IR. Let's not. Flame IR is a human-readable representation by and for computers; you don't need to know every detail of Flame IR to be able to read it. You can usually just wing it.
So what can I do with this IR file?
Here's a neat trick: you can make your compiler compile the IR down to a CLR assembly. All Flame-based compilers have a Flame IR front-end (IProjectHandler) registered by default. So you can just type the following to produce IR for a program, and then turn that IR document into a CLR assembly.
$ flame-brainfuck.exe tests/mirror/mirror.bf -platform ir -S -runtime clr
$ flame-brainfuck.exe tests/mirror/bin/mirror.fir -platform clr -o tests/mirror/bin/mirror.exe
Notice the -o option. It'll will instruct your compiler to write the resulting assembly to tests/mirror/bin/mirror.exe instead of tests/mirror/bin/bin/mirror.exe, as tests/mirror/bin/mirror.fir is already in a bin directory.
Conclusion
Now that you know that Flame IR exists and that it might sometimes prove useful, we can continue and turn our compiler into an actual Brainfuck compiler.