/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use std::{any::type_name, io::Write, marker::PhantomData, process::Command}; use anyhow::{anyhow, Context, Result}; use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; use super::Node; /// Bindgen pipeline /// /// Input and Output are root nodes of the input/output IR. This pipeline converts from `Input` to /// `Output` using a series of passes. /// /// See https://mozilla.github.io/uniffi-rs/latest/internals/bindings_ir_pipeline.html for details on /// how this works. pub struct Pipeline { passes: Vec, input: PhantomData, output: PhantomData, } /// A pipeline pass is a function that converts the root node of an IR into another root node pub struct Pass { name: String, func: PassFn, } type PassFn = Box) -> Result>>; pub fn new_pipeline() -> Pipeline { Pipeline { passes: vec![], input: PhantomData, output: PhantomData, } } impl Pipeline { /// Add a pass that converts the current root node into NewOutput /// /// Under the hood, this uses [Node::into_value] and [Node::try_from_value]. pub fn convert_ir_pass(mut self) -> Pipeline { self.passes.push(Pass { name: format!("Convert root to {}", type_name::()), func: Box::new(|mut root| { let root = NewOutput::try_from_value(root.take_into_value()) .map_err(|e| e.into_anyhow())?; Ok(Box::new(root)) }), }); Pipeline { passes: self.passes, input: self.input, output: PhantomData, } } /// Add a pass that mutates nodes in the current IR /// /// This uses [Node::visit_mut] to find all nodes of a given type, then passes those nodes to /// the provided closure to mutate them. pub fn pass(mut self, mut pass_func: F) -> Self where F: FnMut(&mut N) -> Result<()> + 'static, N: Node, { self.passes.push(Pass { name: type_name::().to_string(), func: Box::new(move |mut root| { root.try_visit_descendents_recurse_mut(&mut pass_func)?; Ok(root) }), }); self } /// Execute the pipeline pub fn execute(&mut self, root: Input) -> Result { self.execute_all_passes(root, &mut NullPipelineRecorder) } /// Execute the pipeline, printing out debugging information for each pass /// /// This is used to implement the `pipeline` CLI subcommand pub fn print_passes(&mut self, root: Input, opts: PrintOptions) -> Result<()> { let mut last_output: Option<(tempfile::TempPath, String)> = None; let mut recorder = PipelineCliRecorder::new(opts.clone()); let execute_result = self.execute_all_passes(root, &mut recorder); let count = recorder.passes.len(); for (i, (title, content)) in recorder.passes.into_iter().enumerate() { // Save output for diffing let mut output = tempfile::NamedTempFile::new()?; write!(output, "{content}")?; let output_path = output.into_temp_path(); if opts.matches_pass(&title, i + 1 == count) { match (last_output, opts.no_diff) { (None, _) | (Some(_), true) => { // First pass, print out the content let title = format!(" {title} "); println!("{title:=^78}"); println!("{content}"); } (Some((last_output, last_title)), _) => { // Middle pass, print out the diff from the last run Command::new("diff") .args(["-du", "--color=auto"]) .arg(&last_output) .arg(&output_path) .arg("--label") .arg(&last_title) .arg("--label") .arg(&title) .spawn()? .wait()?; } } println!(); } last_output = Some((output_path, title)); } // Check the result after printing all passes. This gives the user more context when things // go wrong. execute_result?; if matches!(opts.pass.as_deref(), None | Some("final")) { if let Some((output_path, _)) = last_output { println!("{:=^78}", " final "); println!("{}", std::fs::read_to_string(output_path)?); } } Ok(()) } /// Execute each pass in the pipeline and convert `Self::Input` to `Self::Output` /// /// After each pass, call `recorder.report_pass`, passing it the name of the pass and the root node /// after the pass. fn execute_all_passes( &mut self, root: Input, recorder: &mut dyn PipelineRecorder, ) -> Result { recorder.record_pass("initial", &root); let mut root: Box = Box::new(root); for pass in self.passes.iter_mut() { root = (pass.func)(root).with_context(|| format!("pass: {}", pass.name))?; recorder.record_pass(&pass.name, root.as_ref()); } let root = root .to_box_any() .downcast::() .map_err(|_| anyhow!("Output type mismatch"))?; Ok(*root) } } /// Records passes taken in a IR pipeline pub trait PipelineRecorder { /// Record the result of a pass for the pipeline CLI fn record_pass(&mut self, name: &str, node: &dyn Node); } /// Implements PipelineRecorder by doing nothing. This is what's used when we want to just /// generate bindings, not print out the passes for the pipeline CLI struct NullPipelineRecorder; impl PipelineRecorder for NullPipelineRecorder { fn record_pass(&mut self, _name: &str, _node: &dyn Node) {} } /// Implements PipelineRecorder for the pipeline CLI struct PipelineCliRecorder { opts: PrintOptions, passes: Vec<(String, String)>, } impl PipelineCliRecorder { fn new(opts: PrintOptions) -> Self { Self { opts, passes: vec![], } } } impl PipelineRecorder for PipelineCliRecorder { fn record_pass(&mut self, name: &str, node: &dyn Node) { self.passes .push((name.to_string(), pass_content(node, &self.opts))); } } #[derive(Clone)] pub struct PrintOptions { pub pass: Option, pub no_diff: bool, pub filter_type: Option, pub filter_name: Option, } impl PrintOptions { fn matches_pass(&self, title: &str, last: bool) -> bool { match self.pass.as_deref() { None => true, Some("last") => last, Some(p) => title.contains(p), } } fn has_filter(&self) -> bool { self.filter_type.is_some() || self.filter_name.is_some() } fn matches_node(&self, node: &dyn Node, child: &dyn Node) -> bool { if let Some(filter_type) = &self.filter_type { if node.type_name() != Some(filter_type) { return false; } } if let Some(filter_name) = &self.filter_name { let Some(string_value) = child.as_any().downcast_ref::() else { return false; }; if !(string_value.contains(filter_name) || string_value.to_snake_case().contains(filter_name) || string_value.to_shouty_snake_case().contains(filter_name) || string_value.to_lower_camel_case().contains(filter_name) || string_value.to_upper_camel_case().contains(filter_name)) { return false; } } true } } fn pass_content(node: &dyn Node, opts: &PrintOptions) -> String { if !opts.has_filter() { return format!("{node:#?}"); } let mut search = NodeFilterSearch::new(opts); search.search(node); if search.results.is_empty() { return "Empty".to_string(); } let mut content = String::new(); for (path, node_content) in search.results { let path = format!(" {path} "); content.push_str(&format!("{path:-^78}\n{node_content}\n")); } content } // Implements the depth-first-search to handle `pass_content` with a filter struct NodeFilterSearch<'a> { opts: &'a PrintOptions, current_path: Vec, results: Vec<(String, String)>, } impl<'a> NodeFilterSearch<'a> { fn new(opts: &'a PrintOptions) -> Self { Self { opts, current_path: vec!["root".to_string()], results: vec![], } } fn search(&mut self, node: &dyn Node) { // If any child nodes match, then add this node to the results let mut child_match = false; node.visit_children(&mut |_, child| { child_match = child_match || self.opts.matches_node(node, child); Ok(()) }) .unwrap(); if child_match { self.results .push((self.current_path.join(""), format!("{node:#?}"))); } else { // Otherwise, continue recursing node.visit_children(&mut |field_name, child| { self.current_path.push(field_name.to_string()); self.search(child); self.current_path.pop(); Ok(()) }) .unwrap(); } } }