Skip to content

Commit 9adf015

Browse files
authored
doc: add more explaination of each dialect in home page (#263)
fix #260
1 parent fbb0140 commit 9adf015

File tree

3 files changed

+282
-0
lines changed

3 files changed

+282
-0
lines changed

docs/dialects/cf.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,122 @@
33
on [GitHub](https://github.com/QuEraComputing/kirin/issues/new) if you need help or want to
44
contribute.
55

6+
# The Control Flow Dialect
7+
8+
The control flow dialect provides the most generic control flow semantics via [`cf.Branch`][kirin.dialects.cf.Branch] and [`cf.ConditionalBranch`][kirin.dialects.cf.ConditionalBranch].
9+
10+
## `cf.Branch`
11+
12+
the [`cf.Branch`][kirin.dialects.cf.Branch] statement is used to mark how basic block branches to another basic block without condition. This represents an edge on the control flow graph (CFG).
13+
14+
```mlir
15+
^1(%2):
16+
│ %y = py.constant.constant 1 : !py.int
17+
│ cf.br ^3(%y)
18+
^2(%3):
19+
│ %y_1 = py.constant.constant 2 : !py.int
20+
│ cf.br ^3(%y_1)
21+
^3(%y_2):
22+
│ func.return %y_2
23+
```
24+
25+
**Definition** the `cf.Branch` statement takes a successor block and its argument. The `cf.Branch` is a terminator thus it should always be the last statement of a block.
26+
27+
!!! note
28+
[`ir.Statement`][kirin.ir.Statement] does not own any [`ir.Block`][kirin.ir.Block], the [`ir.Region`][kirin.ir.Region] owns blocks. The [`ir.Statement`][kirin.ir.Statement] will only own [`ir.Region`][kirin.ir.Region]. In Kirin, we use similar design as LLVM/MLIR where the phi nodes in SSA form are replaced by block arguments.
29+
30+
## `cf.ConditionalBranch`
31+
32+
The [`cf.ConditionalBranch`][kirin.dialects.cf.ConditionalBranch] statement represents a conditional branching statement that looks like following (the `cf.cond_br` statement):
33+
34+
```mlir
35+
^0(%main_self, %x):
36+
│ %0 = py.constant.constant 1 : !py.int
37+
│ %1 = py.cmp.gt(lhs=%x, rhs=%0) : !py.bool
38+
│ cf.cond_br %1 goto ^1(%1) else ^2(%1)
39+
```
40+
41+
**Definition**, [`cf.ConditionalBranch`][kirin.dialects.cf.ConditionalBranch] takes a boolean condition `cond` of type [`ir.SSAValue`][kirin.ir.SSAValue] and:
42+
43+
- then successor and its argument
44+
- else successor and its argument
45+
46+
this statement is also a terminator, which means it must be the last statement of a block.
47+
48+
## Combining together - lowering from Python
49+
50+
Now combining these two statemente together, we can represent most of the Python control flows, e.g `if-else` and `for`-loops. These two statement basically just provides a basic way describing the edges on a control flow graph (CFG) by assuming the node only has one or two outgoing edges.
51+
52+
As an example, the following Python program:
53+
54+
```python
55+
from kirin.prelude import basic_no_opt
56+
57+
@basic_no_opt
58+
def main(x):
59+
if x > 1:
60+
y = 1
61+
else:
62+
y = 2
63+
return y
64+
```
65+
66+
will be lowered to the following SSA form in `cf` dialect:
67+
68+
```mlir
69+
func.func main(!Any) -> !Any {
70+
^0(%main_self, %x):
71+
│ %0 = py.constant.constant 1 : !py.int
72+
│ %1 = py.cmp.gt(lhs=%x, rhs=%0) : !py.bool
73+
│ cf.cond_br %1 goto ^1(%1) else ^2(%1)
74+
^1(%2):
75+
│ %y = py.constant.constant 1 : !py.int
76+
│ cf.br ^3(%y)
77+
^2(%3):
78+
│ %y_1 = py.constant.constant 2 : !py.int
79+
│ cf.br ^3(%y_1)
80+
^3(%y_2):
81+
│ func.return %y_2
82+
} // func.func main
83+
```
84+
85+
And similarly, we can lower a `for`-loop into the `cf` dialect:
86+
87+
```python
88+
@basic_no_opt
89+
def main(x):
90+
for i in range(5):
91+
x = x + i
92+
return x
93+
```
94+
95+
will be lowered into the following SSA form:
96+
97+
```mlir
98+
func.func main(!Any) -> !Any {
99+
^0(%main_self, %x_1):
100+
│ %0 = py.constant.constant 0 : !py.int
101+
│ %1 = py.constant.constant 5 : !py.int
102+
│ %2 = py.constant.constant 1 : !py.int
103+
│ %3 = py.range.range(start=%0, stop=%1, step=%2) : !py.range
104+
│ %4 = py.iterable.iter(value=%3) : !Any
105+
│ %5 = py.constant.constant None : !py.NoneType
106+
│ %6 = py.iterable.next(iter=%4) : !Any
107+
│ %7 = py.cmp.is(lhs=%6, rhs=%5) : !py.bool
108+
│ cf.cond_br %7 goto ^2(%x_1) else ^1(%6, %x_1)
109+
^1(%i, %x_2):
110+
│ %x = py.binop.add(%x_2, %i) : ~T
111+
│ %8 = py.iterable.next(iter=%4) : !Any
112+
│ %9 = py.cmp.is(lhs=%8, rhs=%5) : !py.bool
113+
│ cf.cond_br %9 goto ^2(%x) else ^1(%8, %x)
114+
^2(%x_3):
115+
│ func.return %x_3
116+
} // func.func main
117+
```
118+
119+
However, as you may already notice, lowering from Python directly to `cf` dialect will lose some of the high-level information such as the control flow is actually a for-loop. This information can be useful when one wants to perform some optimization. This is why we are taking the same route as MLIR with a structural IR (via [`ir.Region`][kirin.ir.Region]s). For the interested readers, please proceed to [Structural Control Flow](scf.md) for further reading.
120+
121+
## API Reference
6122

7123
::: kirin.dialects.cf.stmts
8124
options:

docs/dialects/func.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,92 @@
33
on [GitHub](https://github.com/QuEraComputing/kirin/issues/new) if you need help or want to
44
contribute.
55

6+
# The Function Dialect
7+
8+
The function dialect provides a set of statements to model semantics of Python-like functions, that means:
9+
10+
- `def <name>(<args>*, <kwargs>*)` like function declarations
11+
- nested functions (namely closures)
12+
- high-order functions (functions can be used as arguments)
13+
- dynamically/statically calling a function or closure
14+
15+
## `func.Return`
16+
17+
This is a simple statement that models the `return` statement in a function declaration. While this is a very simple statement, it is worth noting that this statement only accepts **one argument** of type [ir.SSAValue][kirin.ir.SSAValue] because in Python (and most of other languages) functions always have a single return value, and multiple return values are represented by returning a `tuple`.
18+
19+
## `func.Function`
20+
21+
This is the most fundamental statement that models a Python function.
22+
23+
**Definition** The [`func.Function`][kirin.dialects.func.Function] takes no arguments, but contains a special `str` attribute (thus stored as `PyAttr`) that can be used as a symbolic reference within a symbol table. The `func.Function` also takes a `func.Signature` attribute to store the signature of corresponding function declaration. Last, it contains a [`ir.Region`][kirin.ir.Region] that represents the function body. The [`ir.Region`][kirin.ir.Region] follows the SSACFG convention where the blocks in the region forms a control flow graph.
24+
25+
!!! note "Differences with MLIR"
26+
As Kirin's priority is writing eDSL as kernel functions in Python. To support high-order functions the entry block arguments always have their first argument `self` of type [`types.MethodType`][kirin.types.MethodType]. This is a design inspired by [Julia](https://julialang.org)'s IR design.
27+
28+
As an example, the following Python function
29+
30+
```python
31+
from kirin.prelude import basic_no_opt
32+
33+
@basic_no_opt
34+
def main(x):
35+
return x
36+
```
37+
38+
will be lowered into the following IR, where `main_self` referencing the function itself.
39+
40+
```mlir
41+
func.func main(!Any) -> !Any {
42+
^0(%main_self, %x):
43+
│ func.return %x
44+
} // func.func main
45+
```
46+
47+
the function can be terminated by a [`func.Return`][kirin.dialects.func.Return] statement. All blocks in the function region must have terminators. In the lowering process, if the block is not terminated, a `func.Return` will be attached to return `None` in the function body. Thus `func.Function` can only have a single return value.
48+
49+
## `func.Call` and `func.Invoke`
50+
51+
These two statements models the most common call convention in Python with consideration of compilation:
52+
53+
- `func.Call` models dynamic calls where the **callee is unknown at compile time**, thus of type [`ir.SSAValue`][kirin.ir.SSAValue]
54+
- `func.Invoke` models static calls where the **callee is known at compile time**, thus of type [`ir.Method`][kirin.ir.Method]
55+
56+
they both take `inputs` which is a tuple of [`ir.SSAValue`][kirin.ir.SSAValue] as argument. Because we assume all functions will only return a single value, `func.Call` and `func.Invoke` only have a single result.
57+
58+
## `func.Lambda`
59+
60+
This statement models nested functions (a.k.a closures). While most definitions are similar to `func.Function` the key difference is `func.Lambda` takes a tuple of [`ir.SSAValue`][kirin.ir.SSAValue] arguments as `captured`. This models the captured variables for a nested function, e.g
61+
62+
the following Python function containing a closure inside with variable `x` being captured:
63+
64+
```python
65+
from kirin import basic_no_opt
66+
67+
@basic_no_opt
68+
def main(x):
69+
def closure():
70+
return x
71+
return closure
72+
```
73+
74+
will be lowered into
75+
76+
```mlir
77+
func.func main(!Any) -> !Any {
78+
^0(%main_self, %x):
79+
│ %closure = func.lambda closure(%x) -> !Any {
80+
│ │ ^1(%closure_self):
81+
│ │ │ %x_1 = func.getfield(%closure_self, 0) : !Any
82+
│ │ │ func.return %x_1
83+
│ } // func.lambda closure
84+
│ func.return %closure
85+
} // func.func main
86+
```
87+
88+
Unlike `func.Function` this statement also has a result value which points to the closure itself. Inside the closure body, we insert [`func.GetField`][kirin.dialects.func.GetField] to unpack captured variables into the closure body.
89+
90+
## API Reference
91+
692
::: kirin.dialects.func.stmts
793
options:
894
show_if_no_docstring: true

docs/dialects/ilist.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,86 @@
44
contribute.
55

66

7+
# The Immutable List Dialect
8+
9+
The immutable list dialect models a Python-like `list` but is immutable. There are many good reasons to have an immutable list for compilation, especially when a list is immutable, we can assume a lot of statements to be pure and foldable (and thus can be inlined or simplified without extra analysis/rewrites).
10+
11+
!!! note "JAX"
12+
This is also the reason why JAX picks an immutable array semantic.
13+
14+
!!! note "Why not use tuple?"
15+
Tuple can take multiple items of different types, but list can only take items of the same type. Thus they have different trade-offs when doing analysis such as type inference of iterating through a tuple/list.
16+
17+
## Runtime
18+
19+
This dialect provides a runtime object `IList` which is a simple Python class wraps a Python list. This object can be used as a compile-time value by providing an implementation of `__hash__` that returns the object id. This means common simplifications like Common Subexpression Elimination (CSE) will not detect duplicated `IList` unless the `ir.SSAValue` points to identical `IList` object.
20+
21+
!!! warning "Implementation Details"
22+
this is an implementation detail of `IList`, we can switch to a more efficient runtime in the future where the memory layout is optimized based on the assumption of items in same type and immutabiility.
23+
24+
The `IList` runtime object implements most of the Python `Sequence` interface, such as `__getitem__`, `__iter__`, `__len__` etc.
25+
26+
## `New`
27+
28+
This statements take a tuple of [`ir.SSAValue`][kirin.ir.SSAValue] and creates an `IList` as result.
29+
30+
!!! note "Syntax Sugar in Lowering"
31+
The syntax `[a, b, c]` will be lowered into `New` statement as a syntax sugar when `ilist` dialect is used (thus in conflict with mutable Python list). This may change in the future to give developers more freedom to choose what to lower from.
32+
33+
## `Map`
34+
35+
This statements take a high-order function (a function object) of signature `[[ElemT], OutT]` and apply it on a given `IList[ElemT, Len]` object then return a new `IList[OutT, Len]`.
36+
37+
For example:
38+
39+
```python
40+
@basic_no_opt
41+
def main(x: ilist.IList[float, Literal[5]]):
42+
def closure(a):
43+
return a + 1
44+
return ilist.map(closure, x)
45+
```
46+
47+
will be lowerd into the following
48+
49+
```mlir
50+
func.func main(!py.IList[!py.float, 5]) -> !Any {
51+
^0(%main_self, %x):
52+
│ %closure = func.lambda closure() -> !Any {
53+
│ │ ^1(%closure_self, %a):
54+
│ │ │ %1 = py.constant.constant 1 : !py.int
55+
│ │ │ %2 = py.binop.add(%a, %1) : ~T
56+
│ │ │ func.return %2
57+
│ } // func.lambda closure
58+
│ %0 = py.ilist.map(fn=%closure, collection=%x : !py.IList[!py.float, 5]) : !py.IList[~OutElemT, ~ListLen]
59+
│ func.return %0
60+
} // func.func main
61+
```
62+
63+
## `Foldl` and `Foldr`
64+
65+
These two statements represents applying a binary operator `+` (any binary operator) on an `IList` with a different reduction order, e.g given `[a, b, c]`, `Foldl` represents `((a + b) + c)` and `Foldr` represents `(a + (b + c))`.
66+
67+
## `Scan`
68+
69+
While the actual implementation is not the same, this statement represents the same semantics as the following function:
70+
71+
```python
72+
def scan(fn, xs, init):
73+
carry = init
74+
ys = []
75+
for elem in xs:
76+
carry, y = fn(carry, elem)
77+
ys.append(y)
78+
return carry, ys
79+
```
80+
81+
## `ForEach`
82+
83+
this represents a `for`-loop without any loop variables (variables pass through each loop iteration).
84+
85+
## API Reference
86+
787
::: kirin.dialects.ilist.stmts
888
options:
989
show_if_no_docstring: true

0 commit comments

Comments
 (0)