from xdsl.ir import MLContext
from xdsl.dialects.arith import Arith
from xdsl.dialects.func import Func
from xdsl.dialects.builtin import Builtin
from xdsl.printer import Printer
# MLContext, containing information about the registered dialects
context = MLContext()
# Some useful dialects
context.register_dialect(Arith)
context.register_dialect(Func)
context.register_dialect(Builtin)
# Printer used to pretty-print MLIR data structures
printer = Printer()
Base ideas of what xDSL is. Example of a small program, and SSA.
Dialects are namespaces that contain a collection of attributes and operations. For instance, the Builtin dialect contains (but not exclusively) the attribute !i32
and the operation builtin.func
.
A dialect is usually a single level of abstraction in the IR, and multiple dialects can be used together in the same MLIR program.
Dialects are currently Python classes registering operations and attributes, and providing simple accessors to their attributes and dialects. This will however change in the near future to provide a better interface to dialects.
Attributes represent compile-time information.
In particular, each SSA-value is associated with an attribute, representing its type.
Each attribute type has a name and belongs in a dialect. The textual representation of attributes is prefixed with !
, and the dialect name.
For instance, the vector
attribute has the format !builtin.vector<T>
, where T
is the expected parameter of the attribute.
In Python, attributes are always expected to be immutable objects heriting from either Data
or ParametrizedAttribute
.
Data
attributes are used to wrap python data structures. For instance, the IntAttr
is an attribute containing an int
, and the StringAttr
is an attribute containing a str
.
Data
attributes are parsed and printed with the format dialect_name.attr_name<custom_format>
, where custom_format
is the format defined by the parser and printer of each Data
attribute.
Note that some attributes, such as StringAttr
, are shortened by the printer, and do not require the use of dialect_name.attr_name
. For instance, builtin.str<"foo">
is shortened to "foo"
.
Here is an example on how to create and print an IntAttr
attribute:
from xdsl.dialects.builtin import IntAttr
# Attribute definitions usually define custom `__init__` to simplify their creation
my_int = IntAttr(42)
printer.print_attribute(my_int)
#int<42>
Note that here, the IntAttr
does not print a dialect prefix. This will be fixed soon-ish.
# Access the data in the IntAttr:
print(my_int.data)
42
Parametrized attributes are attributes containing optionally multiple attributes as parameters.
For instance, the integer
attribute from builtin
is a parametrized attribute and expects two attributes as parameter.
Parametrized attributes are printed with the format !dialect.attr_name<attr_1, ... attr_N>
, where attr_i
are the attribute parameters.
Here is an example on how to create and inspect an integer_type
attribute, which represent a machine integer type. It is parametrized by a single IntAttr
parameter, representing the bitwidth.
from xdsl.dialects.builtin import IntegerType
# Get the int that will be passed as parameter to the integer_type
int_64 = IntAttr(64)
i64 = IntegerType(int_64.data)
printer.print_attribute(i64)
i64
# Get back the parameters of IntegerType
printer.print_attribute(i64.parameters[0])
#int<64>
# Use the custom constructor from IntegerType to construct it
assert IntegerType(64) == i64
Note that parametrized attributes may define invariants that need to be respected.
For instance, constructing an integer_type
with wrong parameters will trigger an error:
# NBVAL_IGNORE_OUTPUT
# pyright: reportGeneralTypeIssues=false
# Try to create an IntegerType with wrong parameters
try:
bad_attr = IntegerType([i64])
except Exception as err:
print(err)
[IntegerType(parameters=[IntAttr(data=64), SignednessAttr(data=<Signedness.SIGNLESS: 0>)], width=IntAttr(data=64), signedness=SignednessAttr(data=<Signedness.SIGNLESS: 0>))] should be of base attribute int
Operations represent the computation that a program can do. They span in all abstraction levels, and can be domain-specific.
For instance, arith.addi
will add two integers, while scf.if
represent an if/else structure.
Operations are composed of:
The format of an operation is: results = dialect_name.op_name(operands) (successors) [attributes] regions
Here is for example how to create a constant operation, representing a constant value:
from xdsl.dialects.builtin import IntegerAttr
from xdsl.dialects.arith import Constant
const_op = Constant.create(
[], [i64], attributes={"value": IntegerAttr.from_int_and_width(62, 64)}
)
printer.print_op(const_op)
%0 = arith.constant 62 : i64
Note that dialects usually define methods to ease the definition of such operations:
const_op2 = Constant.from_attr(IntegerAttr.from_int_and_width(62, 64), i64)
printer.print_op(const_op2)
%1 = arith.constant 62 : i64
We can use the results from the operation to pass them as operands for a later operation. For instance, we will add the constant to itself using the arith.addi
operation:
from xdsl.dialects.arith import Addi
add_op = Addi.create([const_op.results[0], const_op.results[0]], [i64], {})
printer.print_op(const_op)
print()
printer.print_op(add_op)
%0 = arith.constant 62 : i64 %2 = arith.addi %0, %0 : i64
We can also put the operations in regions, which can be then used by other operations (such as func)
from xdsl.ir import Region, Block
my_region = Region([Block([const_op, add_op])])
printer.print_region(my_region)
{ %0 = arith.constant 62 : i64 %2 = arith.addi %0, %0 : i64 }
Functions are created using the builtin.func
op, which contain a single region:
from xdsl.dialects.func import FuncOp
my_func = FuncOp.from_region("my_function", [], [], my_region)
printer.print_op(my_func)
func.func @my_function() { %0 = arith.constant 62 : i64 %2 = arith.addi %0, %0 : i64 }