codon/docs/advanced/ir.md

341 lines
13 KiB
Markdown
Raw Normal View History

After type checking but before native code generation, the Codon compiler
makes use of a new [intermediate representation](https://en.wikipedia.org/wiki/Intermediate_representation)
called SIR, where a number of higher-level optimizations, transformations and analyses take place.
SIR offers a comprehensive framework for writing new optimizations or
analyses without having to deal with cumbersome abstract syntax trees (ASTs).
In this section we'll give an overview of SIR, discuss the types of things
you might want to use it for, and give a few examples.
# At a glance
Here is a small (simplified) example showcasing SIR in action. Consider the code:
``` python
def fib(n):
if n < 2:
return 1
else:
return fib(n - 1) + fib(n - 2)
```
When instantiated with an `int` argument, the following IR gets produced (the
names have been cleaned up for simplicity):
``` lisp
(bodied_func
'"fib[int]"
(type '"fib[int]")
(args (var '"n" (type '"int") (global false)))
(vars)
(series
(if (call '"int.__lt__[int,int]" '"n" 2)
(series (return 1))
(series
(return
(call
'"int.__add__[int,int]"
(call
'"fib[int]"
(call '"int.__sub__[int,int]" '"n" 1))
(call
'"fib[int]"
(call '"int.__sub__[int,int]" '"n" 2))))))))
```
A few interesting points to consider:
- SIR is hierarchical like ASTS, but unlike ASTs it uses a vastly reduced
set of nodes, making it much easier to work with and reason about.
- Operators are expressed as function calls. In fact, SIR has no explicit
concept of `+`, `-`, etc. and instead expresses these via their corresponding
magic methods (`__add__`, `__sub__`, etc.).
- SIR has no concept of generic types. By the time SIR is generated, all types
need to have been resolved.
# Structure
SIR is comprised of a set of *nodes*, each with a specific semantic meaning.
There are nodes for representing constants (e.g. `42`), instructions (e.g. `call`)
control flow (e.g. `if`), types (e.g. `int`) and so on.
Here is a table showing the different types of nodes, LLVM IR equivalents,
and some examples:
| Node | LLVM equivalent | Examples |
|----------|-----------------|-------------------------------------------|
| `Node` | n/a | all of the below |
| `Module` | `Module` | n/a |
| `Type` | `Type` | `IntType`, `FuncType`, `RefType` |
| `Var` | `AllocaInst` | `Var`, `Func` |
| `Func` | `Function` | `BodiedFunc`, `ExternalFunc`, `LLVMFunc` |
| `Value` | `Value` | all of the below |
| `Const` | `Constant` | `IntConst`, `FloatConst`, `StringConst` |
| `Instr` | `Instruction` | `CallInstr`, `TernaryInstr`, `ThrowInstr` |
| `Flow` | n/a | `IfFlow`, `WhileFlow`, `ForFlow` |
# Uses
SIR provides a framework for doing program optimizations, analyses and transformations.
These operations are collectively known as IR *passes*.
A number of built-in passes and other functionalities are provided by SIR. These can be
used as building blocks to create new passes. Examples include:
- Control-flow graph creation
- Reaching definitions
- Dominator analysis
- Side effect analysis
- Constant propagation and folding
- Canonicalization
- Inlining and outlining
- Python-specific optimizations targeting several common Python idioms
We're regularly adding new standard passes, so this list is always growing.
## An example
Let's look at a real example. Imagine we want to write a pass that transforms expressions
of the form `<int const> + <int const>` into a single `<int const>` denoting the result.
In other words, a simple form of constant folding that only looks at addition on integers.
The resulting pass would like this:
``` cpp
#include "codon/sir/transform/pass.h"
using namespace codon::ir;
class MyAddFolder : public transform::OperatorPass {
public:
static const std::string KEY;
std::string getKey() const override { return KEY; }
void handle(CallInstr *v) override {
auto *f = util::getFunc(v->getCallee());
if (!f || f->getUnmangledName() != "__add__" || v->numArgs() != 2)
return;
auto *lhs = cast<IntConst>(v->front());
auto *rhs = cast<IntConst>(v->back());
if (lhs && rhs) {
auto sum = lhs->getVal() + rhs->getVal();
v->replaceAll(v->getModule()->getInt(sum));
}
}
};
const std::string MyAddFolder::KEY = "my-add-folder";
```
So how does this actually work, and what do the different components mean? Here
are some notable points:
- Most passes can inherit from `transform::OperatorPass`. `OperatorPass` is a combination
of an `Operator` and a `Pass`. An `Operator` is a utility visitor that provides hooks for
handling all the different node types (i.e. through the `handle()` methods). `Pass` is the
base class representing a generic pass, which simply provides a `run()` method that takes
a module.
- Because of this, `MyAddFolder::handle(CallInstr *)` will be called on every call instruction
in the module.
- Within our `handle()`, we first check to see if the function being called is `__add__`, indicating
addition (in practice there would be a more specific check to make sure this is *the* `__add__`),
and if so we extract the first and second arguments.
- We cast these arguments to `IntConst`. If the results are non-null, then both arguments were in fact
integer constants, meaning we can replace the original call instruction with a new constant that
represents the result of the addition. In SIR, all nodes are "replaceable" via a `replaceAll()` method.
- Lastly, notice that all passes have a `KEY` field to uniquely identify them.
## Bidirectionality
An important and often very useful feature of SIR is that it is *bidirectional*, meaning it's possible
to return to the type checking stage to generate new IR nodes that were not initially present in the
module. For example, imagine that your pass needs to use a `List` with some new element type; that list's
methods need to be instantiated by the type checker for use in SIR. In practice this bidirectionality
often lets you write large parts of your optimization or transformation in Codon, and pull out the necessary
functions or types as needed in the pass.
SIR's `Module` class has three methods to enable this feature:
``` cpp
/// Gets or realizes a function.
/// @param funcName the function name
/// @param args the argument types
/// @param generics the generics
/// @param module the module of the function
/// @return the function or nullptr
Func *getOrRealizeFunc(const std::string &funcName, std::vector<types::Type *> args,
std::vector<types::Generic> generics = {},
const std::string &module = "");
/// Gets or realizes a method.
/// @param parent the parent class
/// @param methodName the method name
/// @param rType the return type
/// @param args the argument types
/// @param generics the generics
/// @return the method or nullptr
Func *getOrRealizeMethod(types::Type *parent, const std::string &methodName,
std::vector<types::Type *> args,
std::vector<types::Generic> generics = {});
/// Gets or realizes a type.
/// @param typeName the type name
/// @param generics the generics
/// @param module the module of the type
/// @return the function or nullptr
types::Type *getOrRealizeType(const std::string &typeName,
std::vector<types::Generic> generics = {},
const std::string &module = "");
```
Let's see bidirectionality in action. Consider the following Codon code:
``` python
def foo(x):
return x*3 + x
def validate(x, y):
assert y == x*4
a = foo(10)
b = foo(1.5)
c = foo('a')
```
Assume we want our pass to insert a call to `validate()` after each assignment that takes the assigned variable
and the argument passed to `foo()`. We would do something like the following:
``` cpp
#include "codon/sir/transform/pass.h"
using namespace codon::ir;
class ValidateFoo : public transform::OperatorPass {
public:
static const std::string KEY;
std::string getKey() const override { return KEY; }
void handle(AssignInstr *v) {
auto *M = v->getModule();
auto *var = v->getLhs();
auto *call = cast<CallInstr>(v->getRhs());
if (!call)
return;
auto *foo = util::getFunc(call->getCallee());
if (!foo || foo->getUnmangledName() != "foo")
return;
auto *arg1 = call->front(); // argument of 'foo' call
auto *arg2 = M->Nr<VarValue>(var); // result of 'foo' call
auto *validate =
M->getOrRealizeFunc("validate", {arg1->getType(), arg2->getType()});
auto *validateCall = util::call(validate, {arg1, arg2});
insertAfter(validateCall); // call 'validate' after 'foo'
}
};
const std::string ValidateFoo::KEY = "validate-foo";
```
Note that `insertAfter` is a conveience method of `Operator` that inserts the given node "after" the node
being visited (along with `insertBefore` which inserts *before* the node being visited).
Running this pass on the snippet above, we would get:
``` python
a = foo(10)
validate(10, a)
b = foo(1.5)
validate(1.5, b)
c = foo('a')
validate('a', c)
```
Notice that we used `getOrRealizeFunc` to create three different instances of `validate`: one for `int`
arguments, one for `float` arguments and finally one for `str` arguments.
# Extending the IR
SIR is extensible, and it is possible to add new constants, instructions, flows and types. This can be
done by subclassing the corresponding *custom* base class; to create a custom type, for example, you
would subclass `CustomType`. Let's look at an example where we extend SIR to add a 32-bit float type:
``` cpp
using namespace codon::ir;
#include "codon/sir/dsl/nodes.h"
#include "codon/sir/llvm/llvisitor.h"
class Builder : public dsl::codegen::TypeBuilder {
public:
llvm::Type *buildType(LLVMVisitor *v) override {
return v->getBuilder()->getFloatTy();
}
llvm::DIType *buildDebugType(LLVMVisitor *v) override {
auto *module = v->getModule();
auto &layout = module->getDataLayout();
auto &db = v->getDebugInfo();
auto *t = buildType(v);
return db.builder->createBasicType(
"float_32",
layout.getTypeAllocSizeInBits(t),
llvm::dwarf::DW_ATE_float);
}
};
class Float32 : public dsl::CustomType {
public:
std::unique_ptr<TypeBuilder> getBuilder() const override {
return std::make_unique<Builder>();
}
};
```
Notice that, in order to specify how to generate code for our `Float32` type, we create a `TypeBuilder`
subclass with methods for building the corresponding LLVM IR type. There is also a `ValueBuilder` for
new constants and converting them to LLVM IR, as well as a `CFBuilder` for new instructions and creating
control-flow graphs out of them.
{% hint style="info" %}
When subclassing nodes other than types (e.g. instructions, flows, etc.), be sure to use the `AcceptorExtend`
[CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) class, as in
`class MyNewInstr : public AcceptorExtend<MyNewInstr, dsl::CustomInstr>`.
{% endhint %}
# Utilities
The `codon/ir/util/` directory has a number of utility and generally helpful functions, for things like
cloning IR, inlining/outlining, matching and more. `codon/ir/util/irtools.h` in particular has many helpful
functions for performing various common tasks. If you're working with SIR, be sure to take a look at these
functions to make your life easier!
Dynamic Polymorphism (#58) * Use Static[] for static inheritance * Support .seq extension * Fix #36 * Polymorphic typechecking; vtables [wip] * v-table dispatch [wip] * vtable routing [wip; bug] * vtable routing [MVP] * Fix texts * Add union type support * Update FAQs * Clarify * Add BSL license * Add makeUnion * Add IR UnionType * Update union representation in LLVM * Update README * Update README.md * Update README * Update README.md * Add benchmarks * Add more benchmarks and README * Add primes benchmark * Update benchmarks * Fix cpp * Clean up list * Update faq.md * Add binary trees benchmark * Add fannkuch benchmark * Fix paths * Add PyPy * Abort on fail * More benchmarks * Add cpp word_count * Update set_partition cpp * Add nbody cpp * Add TAQ cpp; fix word_count timing * Update CODEOWNERS * Update README * Update README.md * Update CODEOWNERS * Fix bench script * Update binary_trees.cpp * Update taq.cpp * Fix primes benchmark * Add mandelbrot benchmark * Fix OpenMP init * Add Module::unsafeGetUnionType * UnionType [wip] [skip ci] * Integrate IR unions and Union * UnionType refactor [skip ci] * Update README.md * Update docs * UnionType [wip] [skip ci] * UnionType and automatic unions * Add Slack * Update faq.md * Refactor types * New error reporting [wip] * New error reporting [wip] * peglib updates [wip] [skip_ci] * Fix parsing issues * Fix parsing issues * Fix error reporting issues * Make sure random module matches Python * Update releases.md * Fix tests * Fix #59 * Fix #57 * Fix #50 * Fix #49 * Fix #26; Fix #51; Fix #47; Fix #49 * Fix collection extension methods * Fix #62 * Handle *args/**kwargs with Callable[]; Fix #43 * Fix #43 * Fix Ptr.__sub__; Fix polymorphism issues * Add typeinfo * clang-format * Upgrade fmtlib to v9; Use CPM for fmtlib; format spec support; __format__ support * Use CPM for semver and toml++ * Remove extension check * Revamp str methods * Update str.zfill * Fix thunk crashes [wip] [skip_ci] * Fix str.__reversed__ * Fix count_with_max * Fix vtable memory allocation issues * Add poly AST tests * Use PDQsort when stability does not matter * Fix dotted imports; Fix issues * Fix kwargs passing to Python * Fix #61 * Fix #37 * Add isinstance support for unions; Union methods return Union type if different * clang-format * Nicely format error tracebacks * Fix build issues; clang-format * Fix OpenMP init * Fix OpenMP init * Update README.md * Fix tests * Update license [skip ci] * Update license [ci skip] * Add copyright header to all source files * Fix super(); Fix error recovery in ClassStmt * Clean up whitespace [ci skip] * Use Python 3.9 on CI * Print info in random test * Fix single unions * Update random_test.codon * Fix polymorhic thunk instantiation * Fix random test * Add operator.attrgetter and operator.methodcaller * Add code documentation * Update documentation * Update README.md * Fix tests * Fix random init Co-authored-by: A. R. Shajii <ars@ars.me>
2022-12-05 08:45:21 +08:00
# Standard pass pipeline
These standard sets of passes are run in `release`-mode:
- Python-specific optimizations: a series of passes to optimize common Python patterns and
idioms. Examples include dictionary updates of the form `d[k] = d.get(k, x) <op> y`, and
optimizing them to do just *one* access into the dictionary, as well as optimizing repeated
string concatenations or various I/O patterns.
- Imperative `for`-loop lowering: loops of the form `for i in range(a, b, c)` (with `c` constant)
are lowered to a special IR node, since these loops are important for e.g. multithreading later.
- A series of program analyses whose results are available to later passes:
- [Control-flow analysis](https://en.wikipedia.org/wiki/Control_flow_analysis)
- [Reaching definition analysis](https://en.wikipedia.org/wiki/Reaching_definition)
- [Dominator analysis](https://en.wikipedia.org/wiki/Dominator_(graph_theory))
- [Capture (or escape) analysis](https://en.wikipedia.org/wiki/Escape_analysis)
- Parallel loop lowering for multithreading or GPU
- Constant propagation and folding. This also includes dead code elimination and (in non-JIT mode)
global variable demotion.
Codon plugins can inject their own passes into the pipeline as well.