Skip to main content

hugr_cli/
describe.rs

1//! Describe the contents of HUGR packages.
2use crate::hugr_io::HugrInputArgs;
3use anyhow::Result;
4use clap::Parser;
5use clio::Output;
6use hugr::NodeIndex;
7use hugr::envelope::ReadError;
8use hugr::envelope::description::{ExtensionDesc, ModuleDesc, PackageDesc};
9use hugr::extension::Version;
10use hugr::package::Package;
11use std::io::{Read, Write};
12use tabled::Tabled;
13use tabled::derive::display;
14
15/// Describe the contents of a serialized HUGR package.
16#[derive(Parser, Debug)]
17#[clap(version = "1.0", long_about = None)]
18#[clap(about = "Describe the contents of a HUGR envelope.")]
19#[group(id = "hugr")]
20#[non_exhaustive]
21pub struct DescribeArgs {
22    /// Hugr input.
23    #[command(flatten)]
24    pub input_args: HugrInputArgs,
25    /// Enumerate packaged extensions
26    #[arg(long, default_value = "false", help_heading = "Filter")]
27    pub packaged_extensions: bool,
28
29    #[command(flatten)]
30    /// Configure module description
31    pub module_args: ModuleArgs,
32
33    #[arg(long, default_value = "false", help_heading = "JSON")]
34    /// Output in json format (deprecated)
35    pub json: bool,
36
37    #[arg(long, default_value = "false", help_heading = "JSON")]
38    /// Output JSON schema for the description format (deprecated).
39    /// Can't be combined with --json.
40    pub json_schema: bool,
41
42    /// Output file. Use '-' for stdout.
43    #[clap(short, long, value_parser, default_value = "-")]
44    pub output: Output,
45}
46
47/// Arguments for reading a HUGR input.
48#[derive(Debug, clap::Args)]
49pub struct ModuleArgs {
50    #[arg(long, default_value = "false", help_heading = "Filter")]
51    /// Don't display resolved extensions used by the module.
52    pub no_resolved_extensions: bool,
53
54    #[arg(long, default_value = "false", help_heading = "Filter")]
55    /// Display public symbols in the module.
56    pub public_symbols: bool,
57
58    #[arg(long, default_value = "false", help_heading = "Filter")]
59    /// Display claimed extensions set by generator in module metadata.
60    pub generator_claimed_extensions: bool,
61}
62impl ModuleArgs {
63    fn filter_module(&self, module: &mut ModuleDesc) {
64        if self.no_resolved_extensions {
65            module.used_extensions_resolved = None;
66        }
67        if !self.public_symbols {
68            module.public_symbols = None;
69        }
70        if !self.generator_claimed_extensions {
71            module.used_extensions_generator = None;
72        }
73    }
74}
75impl DescribeArgs {
76    /// Load and describe the HUGR package with optional input/output overrides.
77    ///
78    /// # Arguments
79    ///
80    /// * `input_override` - Optional reader to use instead of the CLI input argument.
81    /// * `output_override` - Optional writer to use instead of the CLI output argument.
82    pub fn run_describe_with_io<R: Read, W: Write>(
83        &mut self,
84        input_override: Option<R>,
85        mut output_override: Option<W>,
86    ) -> Result<()> {
87        if self.json_schema {
88            let schema = schemars::schema_for!(PackageDescriptionJson);
89            let schema_json = serde_json::to_string_pretty(&schema)?;
90            if let Some(ref mut writer) = output_override {
91                writeln!(writer, "{schema_json}")?;
92            } else {
93                writeln!(self.output, "{schema_json}")?;
94            }
95            return Ok(());
96        }
97
98        let (mut desc, res) = match self
99            .input_args
100            .get_described_package_with_reader(input_override)
101        {
102            Ok((desc, pkg)) => (desc, Ok(pkg)),
103            Err(crate::CliError::ReadEnvelope(ReadError::Payload {
104                source,
105                partial_description,
106            })) => (partial_description, Err(source)),
107            Err(e) => return Err(e.into()),
108        };
109
110        // clear fields that have not been requested
111        for module in desc.modules.iter_mut().flatten() {
112            self.module_args.filter_module(module);
113        }
114
115        let res = res.map_err(anyhow::Error::from);
116
117        let writer: &mut dyn Write = if let Some(ref mut w) = output_override {
118            w
119        } else {
120            &mut self.output
121        };
122
123        if self.json {
124            if !self.packaged_extensions {
125                desc.packaged_extensions.clear();
126            }
127            output_json(desc, &res, writer)?;
128        } else {
129            print_description(desc, self.packaged_extensions, writer)?;
130        }
131
132        // bubble up any errors
133        res.map(|_| ())
134    }
135
136    /// Load and describe the HUGR package.
137    pub fn run_describe(&mut self) -> Result<()> {
138        self.run_describe_with_io(None::<&[u8]>, None::<Vec<u8>>)
139    }
140}
141
142/// Print a human-readable description of a package.
143fn print_description<W: Write + ?Sized>(
144    desc: PackageDesc,
145    show_packaged_extensions: bool,
146    writer: &mut W,
147) -> Result<()> {
148    let header = desc.header();
149    let n_modules = desc.n_modules();
150    let n_extensions = desc.n_packaged_extensions();
151    let module_str = if n_modules == 1 { "module" } else { "modules" };
152    let extension_str = if n_extensions == 1 {
153        "extension"
154    } else {
155        "extensions"
156    };
157
158    writeln!(
159        writer,
160        "{header}\nPackage contains {n_modules} {module_str} and {n_extensions} {extension_str}",
161    )?;
162
163    let summaries: Vec<ModuleSummary> = desc
164        .modules
165        .iter()
166        .map(|m| m.as_ref().map(Into::into).unwrap_or_default())
167        .collect();
168    let summary_table = tabled::Table::builder(summaries).index().build();
169    writeln!(writer, "{summary_table}")?;
170
171    for (i, module) in desc.modules.into_iter().enumerate() {
172        writeln!(writer, "\nModule {i}:")?;
173        if let Some(module) = module {
174            display_module(module, writer)?;
175        }
176    }
177    if show_packaged_extensions {
178        writeln!(writer, "Packaged extensions:")?;
179        let ext_rows: Vec<ExtensionRow> = desc
180            .packaged_extensions
181            .into_iter()
182            .flatten()
183            .map(Into::into)
184            .collect();
185        let ext_table = tabled::Table::new(ext_rows);
186        writeln!(writer, "{ext_table}")?;
187    }
188    Ok(())
189}
190
191/// Output a package description as JSON.
192fn output_json<W: Write + ?Sized>(
193    package_desc: PackageDesc,
194    res: &Result<Package>,
195    writer: &mut W,
196) -> Result<()> {
197    let err_str = res.as_ref().err().map(|e| format!("{e:?}"));
198    let json_desc = PackageDescriptionJson {
199        package_desc,
200        error: err_str,
201    };
202    serde_json::to_writer_pretty(writer, &json_desc)?;
203    Ok(())
204}
205
206/// Display information about a single module.
207fn display_module<W: Write + ?Sized>(desc: ModuleDesc, writer: &mut W) -> Result<()> {
208    if let Some(exts) = desc.used_extensions_resolved {
209        let ext_rows: Vec<ExtensionRow> = exts.into_iter().map(Into::into).collect();
210        let ext_table = tabled::Table::new(ext_rows);
211        writeln!(writer, "Resolved extensions:\n{ext_table}")?;
212    }
213
214    if let Some(syms) = desc.public_symbols {
215        let sym_table = tabled::Table::new(syms.into_iter().map(|s| SymbolRow { symbol: s }));
216        writeln!(writer, "Public symbols:\n{sym_table}")?;
217    }
218
219    if let Some(exts) = desc.used_extensions_generator {
220        let ext_rows: Vec<ExtensionRow> = exts.into_iter().map(Into::into).collect();
221        let ext_table = tabled::Table::new(ext_rows);
222        writeln!(writer, "Generator claimed extensions:\n{ext_table}")?;
223    }
224
225    Ok(())
226}
227
228#[derive(serde::Serialize, schemars::JsonSchema)]
229struct PackageDescriptionJson {
230    #[serde(flatten)]
231    package_desc: PackageDesc,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    error: Option<String>,
234}
235
236#[derive(Tabled)]
237struct ExtensionRow {
238    name: String,
239    #[tabled(format("{}", self.version.as_ref().map_or_else(String::new, ToString::to_string)))]
240    version: Option<Version>,
241}
242
243#[derive(Tabled)]
244struct SymbolRow {
245    #[tabled(rename = "Symbol")]
246    symbol: String,
247}
248
249impl From<ExtensionDesc> for ExtensionRow {
250    fn from(desc: ExtensionDesc) -> Self {
251        // TODO: Remove this once `hugr-rs 0.27.0` is released and `ExtensionDesc::version` is made optional.
252        let version = if desc.version == Version::new(0, 0, 0) {
253            None
254        } else {
255            Some(desc.version)
256        };
257        Self {
258            name: desc.name,
259            version,
260        }
261    }
262}
263
264#[derive(Tabled, Default)]
265struct ModuleSummary {
266    #[tabled(display("display::option", "n/a"))]
267    num_nodes: Option<usize>,
268    #[tabled(display("display::option", "n/a"))]
269    entrypoint_node: Option<usize>,
270    #[tabled(display("display::option", "n/a"))]
271    entrypoint_op: Option<String>,
272    #[tabled(display("display::option", "n/a"))]
273    generator: Option<String>,
274}
275
276impl From<&ModuleDesc> for ModuleSummary {
277    fn from(desc: &ModuleDesc) -> Self {
278        let (entrypoint_node, entrypoint_op) = if let Some(ep) = &desc.entrypoint {
279            (
280                Some(ep.node.index()),
281                Some(hugr::envelope::description::op_string(&ep.optype)),
282            )
283        } else {
284            (None, None)
285        };
286        Self {
287            num_nodes: desc.num_nodes,
288            entrypoint_node,
289            entrypoint_op,
290            generator: desc.generator.clone(),
291        }
292    }
293}