MimIR 0.1
MimIR is my Intermediate Representation
|
This guide summaries typical idioms you want to use when working with MimIR as a developer.
Let's jump straight into an example.
Driver is the first class that you want to allocate. It keeps track of a few "global" variables like some Flags or the Log. Here, the log is set up to output to std::cerr
with mim::Log::Level::Debug (see also Debugging Features).
Then, we load the plugins compile and core, which in turn will load the plugin mem. A plugin consists of a shared object (.so
/.dll
) and a .mim
file. The shared object contains Passes, normalizers, and so on. The .mim
file contains the axiom declarations and links the normalizers with the axioms. For this reason, we need to allocate the mim::fe::Parser to parse the .mim
file; mim::fe::Parser::plugin will also load the shared object. The driver keeps track of all plugins.
Next, we create actual code. Def is the base class for all nodes/expressions in MimIR. Each Def is a node in a graph which is maintained in the World. The World is essentially a big hash set where all Defs are tossed into. The World provides factory methods to create all kind of different Defs. Here, we create the main
function. In direct style, its type looks like this:
Converted to continuation-passing style (CPS) this type looks like this:
The %mem.M
type is a type that keeps track of side effects that may occur. Since, main
introduces Variables we must create a mutable Lambda (see Immutables vs. Mutables). The only thing main
is doing, is to invoke its ret
urn continuation with mem
and argc
as argument:
It is also important to make main
external. Otherwise, MimIR will simply remove this function.
We optimize the program, emit an LLVM assembly file, and compile it via clang
. Finally, we execute the generated program with ./hello a b c
and output its exit code - which should be 4
.
There are two different kind of Defs in MimIR: mutables and immutables:
Immutable | Mutable |
---|---|
must be const | may be non-const |
ops form DAG | ops may be cyclic |
no recursion | may be recursive |
no Var | has Var; get with Def::var |
build ops first, then the actual node | build the actual node first, then set the ops |
Hash consed | each new instance is fresh |
Def::rebuild | Def::stub |
MimIR provides different means to scrutinize Defs. Usually, you will encounter a Def as Ref which is just a wrapper for a const Def*
. Its purpose is to resolve "holes" (called Infers in MimIR) that may pop up due to type inference. Matching built-ins, i.e. all subclasses of Def, works differently than matching Axioms.
Methods beginning with
isa
work like a dynamic_cast
with a runtime check and return nullptr
if the cast is not possible, whileas
are more like a static_cast
and assert
via its isa
sibling in the Debug
build that the cast is correct.Def::isa
/Def::as
allows for an upcast that matches both mutables and immutables:
Def::isa_imm/Def::as_imm allows for an upcast and only matches immutables:
Def::isa_mut/Def::as_mut allows for an upcast and only matches mutables. By doing so, it removes the const
qualifier and gives you access to the non-const
methods that only make sense for mutables:
Checking via Def::isa
/Def::as
a Def*
has the same effect as using Def::isa_mut/Def::isa_mut since the scrutinee must be already a mutable due to the lack of the const
qualifier:
Often, you want to match a Literal and grab its content. You can use Lit::isa/Lit::as for this:
The following table summarizes all important casts:
dynamic_cast static_cast | Returns | If def is a ... |
---|---|---|
def->isa<Lam>() def->as<Lam>() | const Lam* | Lam |
def->isa_imm<Lam>() def->as_imm<Lam>() | const Lam* | immutable Lam |
def->isa_mut<Lam>() def->as_mut<Lam>() | Lam* | mutable Lam |
Lit::isa(def) Lit::as(def) | std::optional< nat_t> nat_t | Lit |
Lit::isa<f32>(def) Lit::as<f32>(def) | std::optional< f32> f32 | Lit |
There are also some additional checks available that usually come as static
methods and either return a pointer or Ref
to the checked entity or nullptr
. Here are some examples:
You can match Axioms via
dynamic_cast
with a runtime check and returns a wrapped nullptr
(see below), if the cast is not possible, orstatic_cast
and assert
s via its mim::match sibling in the Debug
build that the cast is correct.This will yield a Match<Id, D>
which just wraps a const D*
. Id
is the enum
of the corresponding tag
of the matched Axiom. Usually, D
will be an App because most Axioms inhabit a function type. Otherwise, it may wrap a Def or other subclasses of it. For instance, matching %mem.M
yields Match<
mem::M, Def>
.
By default, MimIR assumes that the magic of an Axiom happens when applying the final argument to a curried Axiom. For example, matching a %mem.load
will only trigger for the final App of the curried call
while
will not match. The wrapped App inside the Match refers to the last App of the curried call. So in this example
(mem, ptr)
andmim::App::callee() is %mem.load (T, as)
.
Use mim::App::decurry() to directly get the mim::App::callee() as mim::App.
If you want to design an Axiom that returns a function, you can fine-adjust the trigger point of a mim::match / mim::force.
In order to match an Axiom without any subtags like %mem.load
, do this:
In order to match an Axiom with subtags like %core.wrap
, do this:
The following table summarizes all important casts:
dynamic_cast static_cast | Returns | If def is a ... |
---|---|---|
match<mem::load>(def) force<mem::load>(def) | Match< mem::load, App> | %mem.load (T, as) (mem, ptr) |
match<core::wrap>(def) force<core::wrap>(def) | Match< core::wrap, App> | %core.wrap.??? s m (a, b) |
match(core::wrap::add, def) force(core::wrap::add, def) | Match< core::wrap, App> | %core.wrap.add s m (a, b) |
There are essentially three ways of retrieving the number of elements of something in MimIR.
This is the number of elements of to extract/insert a single element. Note that the number of elements may be unknown at compile time such as in ‹n; 0›
.
mim::Def::num_projs is the same as mim::Def::arity, if the arity is a mim::Lit. Otherwise, it is simply 1
. This concept only exists in the C++-API to give the programmer the illusion to work with n-ary functions, e.g.:
But in reality, all functions have exactly one domain and one codomain.
In additition, there are thresholded variants available that are prefixed with a t
and take mim::Flags::scalarize_threshold (--scalarize-threshold
) into account. The method mim::Def::num_tprojs returns the same as mim::Def::num_projs, but will simply yield 1
, if the arity exceeds the threshold. mim::Def::tproj, mim::Def::tprojs, mim::Lam::tvars, etc. work accordingly.
See also:
TODO
Expression | Class | artiy | isa_lit_artiy | as_lit_artiy | num_projs | num_tprojs |
---|---|---|---|---|---|---|
(0, 1, 2) | Tuple | 3 | 3 | 3 | 3 | 3 |
‹3; 0› | Pack | 3 | 3 | 3 | 3 | 3 |
‹n; 0› | Pack | n | std::nullopt | asserts | 1 | 1 |
[Nat, Bool, Nat] | Sigma | 3 | 3 | 3 | 3 | 3 |
«3; Nat» | Arr | 3 | 3 | 3 | 3 | 3 |
«n; Nat» | Arr | n | std::nullopt | asserts | 1 | 1 |
x: [Nat, Bool] | Var | 2 | 2 | 2 | 2 | 2 |
‹32; 0› | Pack | 32 | 32 | 32 | 32 | 1 |
The last line assumes mim::Flags::scalarize_threshold = 32.
There are several ways of doing this. It depends on what exactly you want to achieve and how much structure you need during the traversal. The simplest way is to kick off with World::externals and recursively run over Def::extended_ops like this:
However, you will most likely want to use the pass or the phase infrastructure.