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
35    pub json: bool,
36
37    #[arg(long, default_value = "false", help_heading = "JSON")]
38    /// Output JSON schema for the description format.
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    version: Version,
240}
241
242#[derive(Tabled)]
243struct SymbolRow {
244    #[tabled(rename = "Symbol")]
245    symbol: String,
246}
247
248impl From<ExtensionDesc> for ExtensionRow {
249    fn from(desc: ExtensionDesc) -> Self {
250        Self {
251            name: desc.name,
252            version: desc.version,
253        }
254    }
255}
256
257#[derive(Tabled, Default)]
258struct ModuleSummary {
259    #[tabled(display("display::option", "n/a"))]
260    num_nodes: Option<usize>,
261    #[tabled(display("display::option", "n/a"))]
262    entrypoint_node: Option<usize>,
263    #[tabled(display("display::option", "n/a"))]
264    entrypoint_op: Option<String>,
265    #[tabled(display("display::option", "n/a"))]
266    generator: Option<String>,
267}
268
269impl From<&ModuleDesc> for ModuleSummary {
270    fn from(desc: &ModuleDesc) -> Self {
271        let (entrypoint_node, entrypoint_op) = if let Some(ep) = &desc.entrypoint {
272            (
273                Some(ep.node.index()),
274                Some(hugr::envelope::description::op_string(&ep.optype)),
275            )
276        } else {
277            (None, None)
278        };
279        Self {
280            num_nodes: desc.num_nodes,
281            entrypoint_node,
282            entrypoint_op,
283            generator: desc.generator.clone(),
284        }
285    }
286}