Adding a New Pass
All passes in the compiler are stored in the calyx/src/passes
directory.
To add a new pass, we need to do a couple of things:
- Define a pass struct and implement the required traits.
- Expose the pass using in the
passes
module. - Register the pass in the compiler.
It is possible to add passes outside the compiler tree, but we haven't needed to do this yet, so we will not cover it here.
Defining a Pass Struct
We first define a Rust structure that will manage the state of the pass:
#![allow(unused)] fn main() { pub struct NewPass; }
A pass needs to implement the Named
and Visitor
traits.
The former defines the name, description, and pass-specific options of the pass.
#![allow(unused)] fn main() { impl Named for NewPass { fn name(&self) -> &'static str { "new-pass" } ... } impl Visitor for NewPass { ... } }
The pass name provided in used in the compiler's driver and needs to be unique for each registered pass.
The Visitor Trait
The visitor trait allows us to define the behavior of the pass. The visitor visits each control operator in each component and performs some action. Furthermore, it also allows us to control the order in which components are visited.
Component Iteration Order
The Order
struct allows us to control the order in which components are visited:
Post
: Iterate the subcomponents of a component before the component itself.Pre
: Iterate the subcomponents of a component after the component itself.No
: Iterate the components in any order.
Visiting Components
Most passes will attempt to transform the structural part of the program (wires
or cells
), the control
schedule, or both.
The Visitor
trait is flexible enough to allow all of these patterns and efficiently traverse the program.
For a control program like this:
seq {
one;
if cond { two } else { three }
invoke foo(..)
}
The following sequence of Visitor
methods are called:
- start
- start_seq
- enable // group one
- start_if
- enable // group two
- enable // group three
- end_if
- invoke // invocation
- finish_seq
- finish
Each non-leaf control operator defines both a start_*
and finish_*
method which allows us to encode top-down and bottom-up traversal patterns.
Each method returns an Action
value which allows us to control the traversal of the program.
For example, Action::Stop
will immediately stop the traversal of the program while Action::SkipChildren
will skip the traversal of the children of the current control operator.
Registering the Pass
The final step is to register the pass in the compiler.
We use the PassManager
to register the pass defined in the default_passes.rs
file.
Registering a pass is as simple as calling the register pass:
#![allow(unused)] fn main() { pm.register_pass::<NewPass>(); }
Once done, the pass is accessible from the command line:
cargo run -- -p new-pass <file>
This will run -p new-pass
on the input file.
In order to run this pass in the default pipeline, we need to add it to the all
alias (which is called when no -p
option is provided).
The all
alias is itself defined using other aliases which separate the pipeline into different phases.
For example, if NewPass
needs to run before the compilation passes, we can add it to the pre-opt
alias.
Some Useful Links
The compiler has a ton of shared infrastructure that can be useful:
ir::Context
: The top-level data structure that holds a complete Calyx program.- Rewriter: Helps with consistent renaming of ports, cells, groups, and comb groups in a component.
analysis
: Provides a number of useful analysis that can be used within a pass.- IR macros: Macros useful for adding cells (
structure!
), guards (guard!
) and assignments (build_assignments!
) to component.
Also, check out how to visualize passes for development and debugging.