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.
Interpretation
Kirin provides a framework for interpreting the IR. There are multiple ways to interpret the IR:
- concrete interpretation, which evaluates the IR using concrete values like CPython.
- abstract interpretation, which evaluates the IR on lattice values. (See also Analysis)
- tree walking, which walks the IR tree and performs actions on each node. (See also Code Generation)
this page will focus on concrete interpretation.
Concrete Interpretation
Function-call like interpretation
The concrete interpreter is essentially a dispatcher of implementations for each statement in the IR. For dialect developers, the main task is to implement a method table, taking the py.binop
dialect as an example:
from kirin import interp # (1)!
from . import stmts # (2)!
from ._dialect import dialect # (3)!
@dialect.register # (4)!
class PyMethodTable(interp.MethodTable): # (4)!
@interp.impl(stmts.Add) # (5)!
def add(self, interp, frame: interp.Frame, stmt: stmts.Add): # (6)!
return (frame.get(stmt.lhs) + frame.get(stmt.rhs),) # (7)!
- Import the
interp
module. - Import the statements module. This is defined similarly as Declaring Statements.
- Import the dialect object. This is defined in a similar way as Declaring Dialect.
- Register the method table with the dialect. This will push the method table to the dialect's registry. By default this will be registered under the key
"main"
, equivalent to@dialect.register(key="main")
. - Mark the method as an implementation of the
Add
statement. This can be dispatched on the type of the statement, e.g to only mark the implementation forAdd(Int, Int)
, you can write@interp.impl(stmts.Add, types.Int, types.Int)
, wheretypes
can be imported byfrom kirin import types
. - While this is enforced, it is recommended to type hint the frame and the statement so you can get the hinting from the IDE. The
@interp.impl
decorator will also type check if the method signature is correct. - In the actual implementation,
frame.get
is used to get the value of the operands. This will return the value of the operand if it is defined in the frame, otherwise it will raise an [InterpreterError
][kirin.exceptions.InterpreterError]. Most of the case, the return value should be atuple
of the results of the statement. In this case, there is only one result, so it is returned as a single-element tuple.
What is a frame?
A frame is a mapping of SSAValue
to their actual values. It represents the state of a function-like statement that cuts the scope of the variables. The frame is passed to the method table so that the interpreter can get the values of the operands from current frame.
Control flow statements
Except these "normal" statements that act more like a function call, there are also control flow statements. For example, the Branch
statement from cf
dialect, defined as follows (see also Declaring Statements):
@statement(dialect=dialect)
class Branch(Statement):
name = "br"
traits = frozenset({IsTerminator()})
arguments: tuple[SSAValue, ...]
successor: Block = info.block()
When interpreting a Branch
statement, instead of actually executing something, we would like to instruct the interpreter to jump to a successor block. This is done by returning a special value interp.Successor
:
@dialect.register
class CfMethods(MethodTable):
@impl(Branch)
def branch(self, interp: Interpreter, frame: Frame, stmt: Branch):
return Successor(stmt.successor, *frame.get_values(stmt.arguments))
Similar to frame.get
, frame.get_values
is a convenience method to get the values of multiple operands at once.
What is a successor?
A successor is a tuple of a block and the values to be passed to the block. The interpreter will use this information to jump to the block and pass the values to the block.
Another special control flow statement is ReturnValue
, unlike interp.Successor
that jumps to another block, ReturnValue
will let interpreter pop the current frame and return the values to the caller or finish the execution:
@dialect.register
class FuncMethods(MethodTable):
@impl(Return)
def return_(self, interp: Interpreter, frame: Frame, stmt: Return):
return interp.ReturnValue(*frame.get_values(stmt.values))
Error handling
Some statements will throw a runtime error, such as cf.Assert
from the cf
dialect, defined as follows:
@statement(dialect=dialect)
class Assert(Statement):
name = "assert"
condition: SSAValue
message: SSAValue = info.argument(String)
When interpreting an Assert
statement, we need to check the condition and raise an error if it is false:
@dialect.register
class CfMethods(MethodTable):
@impl(Assert)
def assert_stmt(self, interp: Interpreter, frame: Frame, stmt: Assert):
if frame.get(stmt.condition) is True:
return ()
if stmt.message:
raise interp.WrapException(AssertionError(frame.get(stmt.message)))
else:
raise interp.WrapException(AssertionError("Assertion failed"))
or raising an [InterpreterError
][kirin.exceptions.InterpreterError]:
@dialect.register
class CfMethods(MethodTable):
@impl(Assert)
def assert_stmt(self, interp: Interpreter, frame: Frame, stmt: Assert):
if frame.get(stmt.condition) is True:
return ()
if stmt.message:
raise InterpreterError(frame.get(stmt.message))
else:
raise InterpreterError("assertion failed")
Running the interpreter
To run the interpreter, you just need to pass the method to [eval
][kirin.interp.Interpreter.eval]:
from kirin.interp import Interpreter
from kirin.prelude import basic
interp = Interpreter(basic)
@basic
def main(a: int, b: int) -> int:
return a + b
interp.eval(main, 1, 2)
Overlaying
One of the most powerful features of the interpreter is overlaying. This allows you to override the implementation of a statement in a dialect by picking different order of method table lookup or even customize the method lookup. This is done by inheriting Interpreter
and define the class variable keys
:
class MyInterpreter(Interpreter):
keys = ["my_overlay", "main"]
When using this new MyInterpreter
, the method lookup will first look for the methods registered in the my_overlay
key. If the method is not found, it will fall back to the main
key. This allows you to override the implementation of a statement in a dialect without modifying the dialect itself.
We will talk about overlaying in abstract interpretation and analysis section which has more use cases.