Declarations
Understanding Kirin IR Declarations
Section titled “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.
Dialect
Section titled “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 irdialect = ir.Dialect("my_dialect") # (1)!- The
Dialectobject is created with the namemy_dialect.
Dialect Groups
Section titled “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_groupdecorator 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_basicfunction is the entry point of the dialect group. It takes aselfargument, which is theDialectGroupobject. This argument is used to access the definition of the dialect group and optionally update the dialect group. - The
run_passfunction 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.
The above dialect group python_basic allows you to use it as following:
@python_basicdef my_function(): passHowever, 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_groupdecorator can also take a dialect group as an argument. This will use the dialects defined in the given dialect group with different passes. - The
Foldpass is created when initializing the dialect group. This pass is used later when running therun_passfunction. - The
run_passfunction is the function that will be called when the dialect group is applied to a given Python function. This function takes amtargument, which is their.Methodobject, and optional argumentsverify,fold, andaggressive. - If the
verifyargument isTrue, the method will be verified. - If the
foldargument isTrue, theFoldpass 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
foldargument here is passed to therun_passfunction defined in the dialect group. Looks complicated? Don’t worry, the@dialect_groupdecorator will handle everything including the type hints!
Statement
Section titled “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
Section titled “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 irfrom 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
@statementis used to generate implementations for theMyStatementclass based on the fields defined in the class. - The
MyStatementclass inherits fromir.Statement. - The
namefield is the name of the statement, if your desired name is justmy_statement, you can omit this field,@statementwill automatically generate the name by converting the class name to snake case. The name is what will be used in text/pretty printing. - The
traitsfield is used to specify the traits of the statement. In this case, the statement is pure.
class MyOp<string mnemonic, list<Trait> traits = []> : Op<MyDialect, mnemonic, traits>;
def dialect_MyOp : MyOp<"awesome"> { let summary = "my awesome statement";
let description = [{my awesome statement}];
let arguments = ( // some inputs, e.g ins F64ElementsAttr:$value from the MLIR Toy example );
let results = ( // some outputs, e.g outs F64Tensor from the MLIR Toy example ); }from xdsl.irdl import IRDLOperation, irdl_op_definition, traits_def
@irdl_op_definitionclass MyStatement(IRDLOperation): name = "awesome" traits = traits_def(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
@statementis used to generate implementations for theMyStatementclass based on the fields defined in the class. - The
traitsfield is used to specify the traits of the statement. In this case, the statement is pure. - The
lhsfield is the left-hand side input value of the statement. The field descriptorinfo.argumentis used to specify the type of the input value. - The
rhsfield is the right-hand side input value of the statement. The field descriptorinfo.argumentis used to specify the type of the input value. - The
outputfield is the output value of the statement. The field descriptorinfo.resultis used to specify the type of the output value.
def dialect_Add : MyOp<"add"> { let summary = "my add statement";
let description = [{my add statement}];
let arguments = ( AnySignlessInteger:$lhs, AnySignlessInteger:$rhs );
let results = (outs I64:$output);}from xdsl.irdl import IRDLOperation, irdl_op_definition, traits_def
@irdl_op_definitionclass Add(IRDLOperation): T: ClassVar = VarConstraint("T", signlessIntegerLike) lhs = operand_def(T) rhs = operand_def(T) result = result_def(T) assembly_format = "$lhs `,` $rhs attr-dict `:` type($result)"A statement can have blocks as successors, which describe the control flow of the program.
@statementclass Branch(Statement): name = "br" traits = frozenset({IsTerminator()}) # (1)!
arguments: tuple[SSAValue, ...] # (2)! successor: Block = info.block() # (3)!- The
traitsfield is used to specify the traits of the statement. In this case, the statement is a terminator. - The
argumentsfield 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
successorfield is the block that the statement will go to after execution. The field descriptorinfo.blockis used to specify the type of the field.
def BranchOp : CF_Op<"br", [ DeclareOpInterfaceMethods<BranchOpInterface, ["getSuccessorForOperands"]>, Pure, Terminator ]> { let arguments = (ins Variadic<AnyType>:$destOperands); let successors = (successor AnySuccessor:$dest);
let builders = [ OpBuilder<(ins "Block *":$dest, CArg<"ValueRange", "{}">:$destOperands), [{ $_state.addSuccessors(dest); $_state.addOperands(destOperands); }]>];}from xdsl.irdl import IRDLOperation, irdl_op_definition, traits_def
@irdl_op_definitionclass BranchOp(IRDLOperation): """Branch operation"""
name = "cf.br"
arguments = var_operand_def() successor = successor_def()
traits = traits_def(IsTerminator(), BranchOpHasCanonicalizationPatterns())
def __init__(self, dest: Block, *ops: Operation | SSAValue): super().__init__(operands=[[op for op in ops]], successors=[dest])It can also have a region that contains other statements, for example, a function statement (simplified).
@statementclass Function(ir.Statement): name = "func" traits = frozenset({SSACFGRegion()}) # (1)! sym_name: str = info.attribute() # (2)! body: Region = info.region(multi=True) # (3)!- The
traitsfield contains theSSACFGRegiontrait, which indicates that the region in the statement is a standard control-flow graph. - The
sym_namefield is the name of the function. In the@statementdecorator, if a field annotated with normal Python types (not an IR node, e.gir.SSAValue,ir.Block,ir.Region), it will be treated as aPyAttrattribute. - The
bodyfield is the region that contains the statements of the function. The field descriptorinfo.regionis used to specify this region can contain multiple blocks.
def FuncOp : Func_Op<"func", [ AffineScope, AutomaticAllocationScope, FunctionOpInterface, IsolatedFromAbove, OpAsmOpInterface]> { let arguments = (ins SymbolNameAttr:$sym_name); let regions = (region AnyRegion:$body);
let builders = [OpBuilder<(ins "StringRef":$name, "FunctionType":$type, CArg<"ArrayRef<NamedAttribute>", "{}">:$attrs, CArg<"ArrayRef<DictionaryAttr>", "{}">:$argAttrs) >]; let hasCustomAssemblyFormat = 1;}@irdl_op_definitionclass FuncOp(IRDLOperation): name = "func.func" traits = traits_def( IsolatedFromAbove(), SymbolOpInterface(), FuncOpCallableInterface() ) body = region_def() sym_name = prop_def(StringAttr)
def __init__( self, name: str, region: Region | type[Region.DEFAULT] = Region.DEFAULT, ): properties: dict[str, Attribute | None] = {"sym_name": StringAttr(name)} super().__init__(properties=properties, regions=[region])Constructing a Statement
Section titled “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
Constantstatements are created with the value1and2. - An
Addstatement is created with thelhsandrhsfields set to the results of thelhsandrhsstatements. Like@dataclassunless specified bykw_only=True, the fields are positional.
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
Section titled “Constructing a Block”Block takes a Sequence of statements as an argument, e.g a list of statements.
from kirin import irir.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))Region
Section titled “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
Section titled “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
Section titled “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.ownerproperty.ir.BlockArgument: SSA values that are the arguments of a block.