Warning
This page is under construction. The content may be incomplete or incorrect. Submit an issue on GitHub if you need help or want to contribute.
Understanding Kirin IR Declarations
In this section, we will learn about the terminology used in Kirin IR. This will help you understand the structure of the IR and how to write your own compiler using Kirin.
Note
The examples in this section will also contain the equivalent MLIR and xDSL code to help you understand the differences between them if you are already familiar with MLIR or xDSL.
Dialect
The Dialect
object is the main registry of all the statements and attributes that are available in the IR. You can create a dialect by just following:
from kirin import ir
dialect = ir.Dialect("my_dialect") # (1)!
- The
Dialect
object is created with the namemy_dialect
.
Dialect Groups
A dialect group is a collection of dialects that can be used as a decorator for Python frontend. It is used to group multiple dialects together and define the passes, compiler options, and other configurations for the dialects.
from kirin.ir import Method, dialect_group
@dialect_group(
[
base,
binop,
cmp,
unary,
assign,
attr,
boolop,
constant,
indexing,
func,
]
) # (1)!
def python_basic(self): # (2)!
def run_pass(mt: Method) -> None: # (3)!
pass # (4)!
return run_pass
- The
dialect_group
decorator is used to create a dialect group with the specified dialects. In this case, we construct a basic Python dialect that allows some basic operations. - The
python_basic
function is the entry point of the dialect group. It takes aself
argument, which is theDialectGroup
object. This argument is used to access the definition of the dialect group and optionally update the dialect group. - The
run_pass
function is the function that will be called when the dialect group is applied to a given Python function. This is where you can define the passes that will be applied to the method. See the next example.
Note
Unlike MLIR/LLVM, because Kirin focuses on kernel functions, the minimal unit of compilation is a function. Therefore, the compiler pass always passes a ir.Method
object which contains a function-like statement (a statement has ir.traits.CallableStmtInterface
).
The above dialect group python_basic
allows you to use it as following:
@python_basic
def my_function():
pass
However, if we want to run some compilation passes on the function, we need to define some passes in the run_pass
function.
from kirin.passes.fold import Fold
@dialect_group(python_basic) # (1)!
def python(self):
fold_pass = Fold(self) # (2)!
def run_pass(mt: Method, *, verify: bool = True, fold: bool = True) -> None: # (3)!
if verify: # (4)!
mt.verify()
if fold: # (5)!
fold_pass(mt)
return run_pass
- The
dialect_group
decorator can also take a dialect group as an argument. This will use the dialects defined in the given dialect group with different passes. - The
Fold
pass is created when initializing the dialect group. This pass is used later when running therun_pass
function. - The
run_pass
function is the function that will be called when the dialect group is applied to a given Python function. This function takes amt
argument, which is their.Method
object, and optional argumentsverify
,fold
, andaggressive
. - If the
verify
argument isTrue
, the method will be verified. - If the
fold
argument isTrue
, theFold
pass will be applied to the method.
The above dialect group python
allows you to use it as following:
@python(fold=True) # (1)!
def my_function():
pass
- The
fold
argument here is passed to therun_pass
function defined in the dialect group. Looks complicated? Don't worry, the@dialect_group
decorator will handle everything including the type hints!
Statement
In Kirin IR, a statement describes an operation that can be executed. Statements are the building blocks that contain the semantics of the program.
Defining a Statement
While a statement can be hand-written by inheriting ir.Statement
, we provide a python-dataclass
-like decorator statement
and in combine with the info.argument
,info.result
,info.region
, info.block
field specifier to make it easier to define a statement.
from kirin import ir
from kirin.decl import statement, info
@statement # (1)!
class MyStatement(ir.Statement): # (2)!
name = "awesome" # (3)!
traits = frozenset({ir.Pure()}) # (4)!
# blabla, we will talk about this later
- the decorator
@statement
is used to generate implementations for theMyStatement
class based on the fields defined in the class. - The
MyStatement
class inherits fromir.Statement
. - The
name
field is the name of the statement, if your desired name is justmy_statement
, you can omit this field,@statement
will automatically generate the name by converting the class name to snake case. The name is what will be used in text/pretty printing. - The
traits
field is used to specify the traits of the statement. In this case, the statement is pure.
Like a function, a statement can have multiple inputs and outputs.
@statement # (1)!
class Add(ir.Statement):
traits = frozenset({ir.Pure()}) # (2)!
lhs: ir.SSAValue = info.argument(ir.types.Int) # (3)!
rhs: ir.SSAValue = info.argument(ir.types.Int) # (4)!
output: ir.ResultValue = info.result(ir.types.Int) # (5)!
- the decorator
@statement
is used to generate implementations for theMyStatement
class based on the fields defined in the class. - The
traits
field is used to specify the traits of the statement. In this case, the statement is pure. - The
lhs
field is the left-hand side input value of the statement. The field descriptorinfo.argument
is used to specify the type of the input value. - The
rhs
field is the right-hand side input value of the statement. The field descriptorinfo.argument
is used to specify the type of the input value. - The
output
field is the output value of the statement. The field descriptorinfo.result
is used to specify the type of the output value.
A statement can have blocks as successors, which describe the control flow of the program.
@statement
class Branch(Statement):
name = "br"
traits = frozenset({IsTerminator()}) # (1)!
arguments: tuple[SSAValue, ...] # (2)!
successor: Block = info.block() # (3)!
- The
traits
field is used to specify the traits of the statement. In this case, the statement is a terminator. - The
arguments
field is the input values of the statement. Branch can take multiple arguments,tuple[SSAValue, ...]
is used to specify that the field is a tuple ofSSAValue
. Note that only...
is supported because if the number of arguments is known, we recommend specifying them explicitly. - The
successor
field is the block that the statement will go to after execution. The field descriptorinfo.block
is used to specify the type of the field.
It can also have a region that contains other statements, for example, a function statement
@statement
class Function(ir.Statement):
name = "func"
traits = frozenset({SSACFGRegion()}) # (1)!
sym_name: str = info.attribute(property=True) # (2)!
body: Region = info.region(multi=True) # (3)!
- The
traits
field contains theSSACFGRegion
trait, which indicates that the region in the statement is a standard control-flow graph. - The
sym_name
field is the name of the function. In the@statement
decorator, if a field annotated with normal Python types (not an IR node, e.gir.SSAValue
,ir.Block
,ir.Region
), it will be treated as aPyAttr
attribute. - The
body
field is the region that contains the statements of the function. The field descriptorinfo.region
is used to specify this region can contain multiple blocks.
Constructing a Statement
Statements can be constructed in similar ways to constructing a normal Python dataclass
. Taking the previous definitions as an example:
from kirin.dialects.py.constant import Constant
lhs, rhs = Constant(1), Constant(2) # (1)!
add = Add(lhs.result, rhs=rhs.result) # (2)!
- Two [
Constant
][kirin.dialect.py.constant.Constant] statements are created with the value1
and2
. - An
Add
statement is created with thelhs
andrhs
fields set to the results of thelhs
andrhs
statements. Like@dataclass
unless specified bykw_only=True
, the fields are positional.
Block
A block is a sequence of statements that are executed in order. Optionally, a block can have arguments that are passed from the predecessor block and terminates with a terminator statement. Unlike ir.Statement
, the ir.Block
class is final and cannot be extended.
Constructing a Block
Block
takes a Sequence
of statements as an argument, e.g a list of statements.
from kirin import ir
ir.Block() # Block(_args=())
ir.Block([stmt_a, stmt_b])
continue the example from Constructing a Statement, we can construct a block like following:
block = ir.Block()
arg_x = block.args.append_from(ir.types.Any)
arg_y = block.args.append_from(ir.types.Any)
block.stmts.append(Add(arg_x, arg_y))
Note
Every IR node in Kirin has a pretty printer that can be used to print the node in a human-readable format. Just call .print
method. In the above example, we have
^0(%0, %1):
%2 = add(lhs=%0, rhs=%1) : !py.int
Region
A region is a sequence of blocks that are connected by control flow. A region can contain multiple blocks and can be nested within another region via statements that contain a region field. Unlike ir.Statement
, the ir.Region
class is final and cannot be extended.
Constructing a Region
Continuing the example from Constructing a Block, we can construct a region like following:
region = ir.Region([block])
pretty printing the region will give you
{
^0(%1, %2):
│ %0 = add(lhs=%1, rhs=%2) : !py.int
}
SSA Value
An SSA value is a value that is assigned only once in the program. In Kirin IR, an SSA value is represented by the ir.SSAValue
class. Most of the time, one does not need to construct the SSA value directly, as it is automatically created when constructing a statement.
There are 3 types of SSA values:
ir.SSAValue
: the base class of SSA values.ir.ResultValue
: SSA values that are the result of a statement, this object allows you to access the parent statement viaresult.owner
property.ir.BlockArgument
: SSA values that are the arguments of a block.