Modifying the IR
modify_ir is the single entry point an extension implements. Understanding when and how it runs is essential for writing correct extensions.
When It Runs
modify_ir is invoked by the IR constructor:
- IR items are built from the transpiled source.
modify_irruns for each extension, in declaration order.- Type instantiation occurs (for templates), substituting
TypeParamnodes.
This means extensions see the fully constructed IR but operate before any type-parameter substitution. Anything an extension injects containing TypeParam nodes will be substituted automatically during instantiation.
modify_ir runs on every wgsl_source() and instantiate() call — not once at definition time.
Walking the Module
ir::Module is:
#![allow(unused)]
fn main() {
pub struct Module {
pub name: String,
pub items: Vec<Item>,
pub attrs: Vec<Attribute>,
}
}
ir::Item is an enum with variants: Struct, Fn, Const, Uniform, Storage, Workgroup, Sampler, Texture, Impl, Enum.
Inside function bodies, ir::Stmt includes a Macro { name, args } variant for unrecognized statement macros (see Statement Macro Lowering). Extensions that claim macros via MACROS walk function blocks and replace Stmt::Macro nodes with lowered IR.
To iterate and mutate items, match on the variant:
#![allow(unused)]
fn main() {
use wgsl_rs::WgslExtension;
use wgsl_rs::ir::{Item, Module};
pub struct SlabItemExt;
impl WgslExtension for SlabItemExt {
fn modify_ir(module: &mut Module) {
let slab_structs: Vec<String> = module
.items
.iter()
.filter_map(|item| match item {
Item::Struct(s) => {
let derives_slab = s.attrs.iter().any(|a| {
a.path == "derive" && a.args.iter().any(|arg| arg == "SlabItem")
});
derives_slab.then(|| s.name.clone())
}
_ => None,
})
.collect();
for name in slab_structs {
module.items.push(slab_read_fn(&name));
module.items.push(slab_write_fn(&name));
}
}
}
fn slab_read_fn(struct_name: &str) -> Item {
// Build an ir::Item::Fn that reads a slab item at a given offset.
// ...
}
fn slab_write_fn(struct_name: &str) -> Item {
// Build an ir::Item::Fn that writes a slab item at a given offset.
// ...
}
}
Type Substitution
Extensions do not need to handle type parameters themselves. If an injected function references a TypeParam node, the instantiation pass substitutes it with the concrete type from each instantiate() call. Do not attempt to outsmart this by string-replacing type names — operate on IR nodes and let substitution handle generics.
See IR Attributes for how to filter items by attribute, and Examples for the full SlabItemExt and other worked examples.