|
MimIR 0.1
MimIR is my Intermediate Representation
|
This guide summarizes the typical idioms and patterns you will want to use when working with MimIR as a developer.
Let's jump straight into an example.
Driver is usually the first class you create. It owns a few global facilities such as Flags, the Log, and the current World. In this example, the log is configured to write debug output to std::cerr; see also Debugging Features. We then build an AST and a mim::ast::Parser on top of that world.
Next, the parser loads the compile and core plugins, which in turn load the mem plugin. A plugin consists of two parts:
The shared object contains passes, normalizers, and similar runtime components. The .mim file contains axiom declarations and links normalizers to their corresponding axioms. Calling mim::ast::Parser::plugin parses the .mim file and also loads the shared object, while the Driver keeps track of the resulting plugin state.
Now we can build actual code.
Def is the base class for all nodes/expressions in MimIR. Each Def is a node in a graph managed by the World. You can think of the World as a giant hash set that owns all Defs and provides factory methods to create them.
In this example, we construct the main function. In direct style, its type looks like this:
In continuation-passing style (CPS), the same type looks like this:
The type mem.M 0 tracks side effects. Since main introduces variables, we must create it as a mutable lambda; see Immutables vs. Mutables.
The body of main is simple: it invokes the return continuation ret with mem and argc:
It is also important to mark main as external. Otherwise, MimIR may remove it as dead code.
Finally, we optimize the program, emit an LLVM assembly file, compile it via clang, and execute the generated binary with ./hello a b c. We then print its exit code, which should be 4.
MimIR distinguishes between two kinds of Defs: immutables and mutables.
| Immutable | Mutable |
|---|---|
| must be const | may be non-const |
| ops form a DAG | ops may be cyclic |
| no recursion | may be recursive |
| no variables | may have variables; use mim::Def::var / mim::Def::has_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 |
Immutables are usually constructed in one step with World factory methods. The usual pattern is: build all operands first, then create the immutable node with w.app, w.tuple, w.pi, w.sigma, and similar helpers.
For ordinary applications, mim::World::app is the direct building block:
Here, mim::World::annex yields the raw axiom node itself. That is useful when you want to partially apply a curried annex, store it, inspect it, or build the application tree step by step from C++.
If you want the full curried call in one go, prefer mim::World::call:
w.call<Id>(...) starts from w.annex<Id>() and completes the curried application for you, including implicit arguments. So the rule of thumb is:
Mutable binders are typically built in two phases. First, create the mutable node with a mut_* factory or a stub. Then, obtain its variables and fill in the body via mim::Def::set:
Use mim::Def::externalize for roots that must survive cleanup and whole-world rewrites. Top-level entry points, generated wrapper functions, and replacement nodes for former externals all follow this pattern.
MimIR provides several ways to scrutinize Defs. Matching built-ins, i.e. subclasses of Def, works a little differently from matching axioms.
Methods beginning with
Def::isa / Def::as perform a downcast that matches both mutables and immutables:
mim::Def::isa_imm / mim::Def::as_imm only match immutables:
mim::Def::isa_mut / mim::Def::as_mut only match mutables. They also remove the const qualifier, which gives you access to the non-const methods that only make sense for mutables:
If the scrutinee is already a Def*, then Def::isa / Def::as behave the same as mim::Def::isa_mut / mim::Def::as_mut, because the missing const already implies mutability:
Often, you want to match a literal and extract its value. Use Lit::isa / Lit::as:
The following table summarizes the most 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 several specialized checks, usually provided as static methods. They return either a pointer to the matched entity or nullptr.
Here are a few examples:
You can match axioms via
The result is a mim::Axm::isa<Id, D>, which wraps a const D*. Here, Id is the enum corresponding to the matched axiom tag, and D is usually an App, because most axioms inhabit a function type. In other cases, it may wrap a plain Def or some other subclass.
By default, MimIR assumes that an axiom becomes "active" when its final curried argument is applied. For example, matching %mem.load only succeeds on the final App of the curried call
whereas
does not match.
In this example, the wrapped App refers to the final application, so:
Use mim::App::decurry if you want direct access to the preceding application. See the examples below.
If you design an axiom that returns a function, you can fine-tune the trigger point of mim::Axm::isa / mim::Axm::as.
To match an axiom without subtags, such as %mem.load, use:
To match an axiom with subtags, such as %core.wrap, use:
The following table summarizes the most important axiom matches:
| dynamic_cast static_cast | Returns | If def is a ... |
|---|---|---|
| isa<mem::load>(def) as<mem::load>(def) | mim::Axm::isa specialized for mem::load and App | final curried %mem.load application |
| isa<core::wrap>(def) as<core::wrap>(def) | mim::Axm::isa specialized for core::wrap and App | final curried %core.wrap application |
| isa(core::wrap::add, def) as(core::wrap::add, def) | mim::Axm::isa specialized for core::wrap and App | final curried %core.wrap.add application |
In MimIR, there are essentially three ways to talk about the number of elements of something.
The arity is the number of elements available to extract / insert. This number may itself be dynamic, for example in ‹n; 0›.
mim::Def::num_projs equals mim::Def::arity if the arity is a mim::Lit. Otherwise, it yields 1.
This concept mainly exists in the C++ API to give you the illusion of n-ary structure, e.g.
Internally, however, all functions still have exactly one domain and one codomain.
There are also thresholded variants prefixed with t, which take mim::Flags::scalarize_threshold (--scalarize-threshold) into account.
mim::Def::num_tprojs behaves like mim::Def::num_projs, but returns 1 if the arity exceeds the threshold. Similarly, mim::Def::tproj, mim::Def::tprojs, mim::Lam::tvars, and related methods follow the same rule.
See also:
TODO
| Expression | Class | arity | num_projs | num_tprojs |
|---|---|---|---|---|
| (0, 1, 2) | Tuple | 3 | 3 | 3 |
| ‹3; 0› | Pack | 3 | 3 | 3 |
| ‹n; 0› | Pack | n | 1 | 1 |
| [Nat, Bool, Nat] | Sigma | 3 | 3 | 3 |
| «3; Nat» | Arr | 3 | 3 | 3 |
| «n; Nat» | Arr | n | 1 | 1 |
| x: [Nat, Bool] | Var | 2 | 2 | 2 |
| ‹32; 0› | Pack | 32 | 32 | 1 |
The last line assumes mim::Flags::scalarize_threshold = 32.
There are several ways to iterate over a MimIR program. Which one is best depends on what you want to do and how much structure you need during the traversal.
The simplest approach is to start from World::externals and recursively visit Def::deps:
In practice, though, you will usually want to use the phase infrastructure instead.