Working with the IR
Important
Loki is still under active development and has not yet seen a stable release. Interfaces can change at any time, objects may be renamed, or concepts may be re-thought. Make sure to sync your work to the current release frequently by rebasing feature branches and upstreaming more general applicable work in the form of pull requests.
The most important tool for working with Loki’s internal representation are utilities that traverse the IR to find specific nodes or patterns, to modify or replace subtrees, or to annotate the tree. In Loki there exist two types of tree traversal tools, depending on which level of the IR they operate on:
Visitors that traverse the tree of control flow nodes;
Mappers (following Pymbolic’s
pymbolic.mapper
naming convention) that traverse expression trees.
Visitors
Loki’s visitors work by inspecting the type of each IR node they encounter and then selecting the best matching handler method for that node type. This allows implementing visitors that perform tasks either for very specific node types or generally applicable for any node type, depending on the handler’s name.
Loki includes a range of ready-to-use and configurable visitors for many common use cases, such as discovering certain node types, modifying or replacing nodes in the tree, or creating a string representation of the tree. For some use cases it may be easier to implement new visitors tailored to the task.
Searching the tree
The first category of visitors traverses the IR and collects a list of results
subject to certain criteria. In almost all cases FindNodes
is the tool
for that job with some bespoke variants for specific use cases.
|
Find |
|
Find all parent nodes for node |
|
Find repeated nodes of the same type in lists/tuples within a given tree. |
|
Find a pattern of nodes given as tuple/list of types within a given tree. |
A common pattern for using FindNodes
is the following:
for loop in FindNodes((Loop, WhileLoop)).visit(routine.body):
# ...do something with loop...
There are additional visitors that search all expression trees embedded in the control flow IR, which are explained further down.
Transforming the tree
A core feature of Loki is the ability to transform the IR, which is done using
the Transformer
. It is a visitor that rebuilds the tree and replaces
nodes according to a mapper.
|
Visitor class to rebuild the tree and replace nodes according to a mapper. |
A |
|
An enriched |
|
A |
Transformer
is commonly used in conjunction with FindNodes
, with
the latter being used to build the mapper for the first. The following example
removes all loops over the horizontal dimension and replaces them by
their body. This code snippet is a simplified version of a transformation used
in ExtractSCATransformation
:
routine = Subroutine(...)
horizontal = Dimension(...)
...
loop_map = {}
for loop in FindNodes(Loop).visit(routine.body):
if loop.variable == horizontal.variable:
loop_map[loop] = loop.body
routine.body = Transformer(loop_map).visit(routine.body)
Converting the tree to string
The last step in a transformation pipeline is usually to write the transformed
IR to a file. This is a task for Loki’s backends which
themselves are subclasses of loki.visitors.pprint.Stringifier
, yet
another visitor. loki.visitors.pprint.Stringifier
doubles as a
pretty-printer for the IR that is useful for debugging.
|
Convert a given IR tree to a string representation. |
|
Pretty-print the given IR using |
Implementing new visitors
Any new visitor should subclass Visitor
(or any of its subclasses).
The common base class for all visitors is GenericVisitor
, declared in
loki.visitors
that provides the basic functionality for matching
objects to their handler methods. Derived from that is Visitor
which
adds a default handler visit_Node
(for Node
) and functionality
to recurse for all items in a list or tuple and return the combined result.
To define handlers in new visitors, they should define visit_Foo
methods for each class Foo
they want to handle.
If a specific method for a class Foo
is not found, the MRO
of the class is walked in order until a matching method is found (all the
way until, for example, Visitor.visit_Node
applies).
The method signature is:
def visit_Foo(self, o, [*args, **kwargs]):
pass
The handler is responsible for visiting the children (if any) of
the node o
. *args
and **kwargs
may be
used to pass information up and down the call stack. You can also
pass named keyword arguments, e.g.:
def visit_Foo(self, o, parent=None, *args, **kwargs):
pass
Mappers
Mappers are visitors that traverse expression trees.
They are built upon pymbolic.mapper
classes and for that reason use
a slightly different way of determining the handler methods: each expression
tree node (pymbolic.primitives.Expression
) holds a class
attribute mapper_method
with the name of the relevant method.
Loki provides, similarly to control flow tree visitors, ready-to-use mappers
for searching or transforming expression trees, all of which are implemented
in loki.expression.mappers
. In addition,
loki.expression.expr_visitors
provides visitors that apply the same mapper
to all expression trees in the IR.
Searching in expression trees
The equivalent to FindNodes
for expression trees is
ExpressionRetriever
. Using a generic function handle, (almost) arbitrary
conditions can be used as a query that decides whether to include a given node
into the list of results.
A mapper for the expression tree that looks for entries specified by a query. |
Note that mappers operate only on expression trees, i.e. using them directly
is only useful when working with a single property of a control flow node,
such as loki.ir.Assignment.rhs
. If one wanted to search for expression
nodes in all expression trees in the IR, e.g. to find all variables, bespoke
visitors exist that apply ExpressionRetriever
to all expression trees.
Base class visitor to collect specific sub-expressions, eg. |
|
A visitor to collect all expression tree nodes (i.e., |
|
A visitor to collect all |
|
A visitor to collect all variables used in an IR tree |
|
A visitor to collect all |
|
A visitor to collect all literals (which includes |
For example, the following finds all function calls embedded in expressions
(InlineCall
, as opposed to subroutine calls in CallStatement
):
for call in FindInlineCalls().visit(routine.body):
# ...do something with call...
Transforming expression trees
Transformations of the expression tree are done very similar to
Transformer
, using the mapper SubstituteExpressionsMapper
that
is given a map to replace matching expression nodes.
A visitor to traverse and transform an expression tree |
|
A Pymbolic expression mapper (i.e., a visitor for the expression tree) that defines on-the-fly handlers from a given substitution map. |
In the same way that searching can be done on all expression trees in the IR,
transformations can be applied to all expression trees at the same time using
SubstituteExpressions
:
A dedicated visitor to perform expression substitution in all IR nodes |
The following example shows how expression node discovery and substitution can
be combined to replace all occurences of intrinsic function calls.
(The code snippet is taken from replace_intrinsics
, where two dict,
function_map
and symbol_map
, provide the mapping to rename
certain function calls that appear in routine
.)
from loki.expression import symbols as sym
callmap = {}
for c in FindInlineCalls(unique=False).visit(routine.body):
cname = c.name.lower()
if cname in symbol_map:
callmap[c] = sym.Variable(name=symbol_map[cname], scope=routine.scope)
if cname in function_map:
fct_symbol = sym.ProcedureSymbol(function_map[cname], scope=routine.scope)
callmap[c] = sym.InlineCall(fct_symbol, parameters=c.parameters,
kw_parameters=c.kw_parameters)
routine.body = SubstituteExpressions(callmap).visit(routine.body)
Converting expressions to string
Every backend has their own mapper to convert expressions to a source
code string, according to the corresponding language specification.
All build on a common base class LokiStringifyMapper
, which is
also called automatically when converting any expression node to string.
A class derived from the default |