-
Notifications
You must be signed in to change notification settings - Fork 8
Distributor Guide Writing Compilation Passes
This page explains how to write a new compilation pass that can be plugged into an RTICX distribution.
A compilation pass is a self-contained crate that implements a subset of RTICX functionality. It transforms user application syntax from a higher to a lower level syntax representation, usually expanding the input so that the next pass or the core pass can understand it.
For example, the software-tasks pass transforms #[sw_task] and spawn() calls into hardware tasks and dispatcher interrupts. The deadline pass converts deadline = D into priority = N. The auto-assign pass infers core = N from shared resource usage.
Every pass implements RticPass from rticx-core:
use proc_macro2::TokenStream as TokenStream2;
use syn::ItemMod;
use rticx_core::{InfoBus, RticPass};
pub trait RticPass {
fn subscribe(&mut self, info_bus: InfoBus);
fn run_pass(
&self,
args: TokenStream2,
app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)>;
fn pass_name(&self) -> &str;
}-
args— the token stream of the#[<distro>::app(...)]attribute arguments. -
app_mod— the annotated module. - The return value is the transformed
(args, app_mod). -
pass_nameis used in error messages to identify which pass failed. -
subscribeis the only place a pass receives a (clonable) handle to the sharedInfoBus(see Using the InfoBus).
Passes are registered as pre-core passes. The RticMacroBuilder subscribes the core backend first, then each pre-core pass in insertion order, before invoking that pass's run_pass:
use rticx_core::RticMacroBuilder;
let mut builder = RticMacroBuilder::new(my_backend);
builder.bind_pre_core_pass(MyPass);
let tokens = builder.build_rtic_macro(args, input);- Pre-core passes run before
rticx-coreparses the module. Use them to expand high-level syntax into core RTICX syntax. - There is no
bind_post_core_passanymore. If a pass needs to react after the core codegen, register it as the last pre-core pass and read therticx_core::App/rticx_core::Analysisentries from theInfoBusonce they have been published.
If a pass needs target-specific information, it can define its own backend trait. The distribution implements this trait and passes the implementation to the pass constructor.
For example, rticx-sw-pass defines SwPassBackend:
pub trait SwPassBackend {
fn queue_path(&self) -> syn::Path;
fn generate_local_pend_fn(&self, core: u32, empty_body_fn: ItemFn) -> ItemFn;
fn generate_cross_pend_fn(&self, core: u32, empty_body_fn: ItemFn) -> Option<ItemFn>;
fn custom_interrupt_path(&self, core: u32) -> Option<syn::Path> { None }
fn subscribe(&mut self, _info_bus: rticx_core::InfoBus) {}
}-
generate_local_pend_fnfills the body of the core-local interrupt-pending function used byspawn. -
generate_cross_pend_fnfills the cross-core interrupt-pending function used byspawn_from. ReturnsNoneon single-core targets. -
custom_interrupt_pathoptionally overrides the default PAC interrupt path. -
subscribeis the default no-op; theSoftwarePasswrapper forwards theInfoBusto its backend, so this is where aSwPassBackendimplementation can grab the bus.
A typical pass crate contains:
- A
Cargo.tomlwithrticx-coreas a dependency. - A public type implementing
RticPass. - Optionally, a public backend trait for target-specific hooks.
- A public constructor that accepts the backend trait implementation.
use proc_macro2::TokenStream as TokenStream2;
use syn::ItemMod;
use rticx_core::{InfoBus, RticPass};
pub struct MyPass;
impl RticPass for MyPass {
fn subscribe(&mut self, _info_bus: InfoBus) {}
fn run_pass(
&self,
args: TokenStream2,
mut app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)> {
// Inspect and transform app_mod and args here
Ok((args, app_mod))
}
fn pass_name(&self) -> &str {
"my-pass"
}
}The InfoBus is the shared, typed information bus that lets passes and backends exchange data during a single macro expansion. An InfoBus is created by RticMacroBuilder and clones are handed out to the core backend and each compilation pass via their subscribe methods before any run_pass is invoked.
Conventions:
- Entry keys are namespaced by the publishing crate and the type name:
crate_name::TypeName. The core pass publishesrticx_core::Appandrticx_core::Analysisafter parsing and analysis. The software-tasks pass publishesrticx_sw_pass::Appandrticx_sw_pass::Analysis(exported as the constantsINFO_APP/INFO_ANALYSIS). - Entries are write-once: a second
publishto an existing key is an error. Pick a key namespace you own to avoid colliding with other passes. - Subscribe ordering matters: the core backend receives the bus first, then each pre-core pass in insertion order, in each case before its
run_passruns. A later pass can thereforegetentries published by an earlier pass.
A pass that wants to publish its own parsed/analyzed data typically stashes the InfoBus clone in subscribe and uses it in run_pass:
pub struct MyPass {
info_bus: Option<rticx_core::InfoBus>,
}
impl RticPass for MyPass {
fn subscribe(&mut self, info_bus: rticx_core::InfoBus) {
self.info_bus = Some(info_bus);
}
fn run_pass(
&self,
args: TokenStream2,
app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)> {
// ...parse/transform...
if let Some(bus) = &self.info_bus {
bus.publish("my_pass::App", my_app)
.expect("no other crate should publish my_pass::App");
}
Ok((args, app_mod))
}
fn pass_name(&self) -> &str { "my-pass" }
}Because passes are pure syntax transformers, you can test them by feeding them a parsed ItemMod and asserting on the output. For passes that change the module items, you can also write trybuild-style compilation tests as distribution examples.
- Writing Distributions — plug your pass into a distribution.
- Distributor Guide Architecture — understand the full pipeline.