|
| 1 | +//! Convert between different HUGR envelope formats. |
| 2 | +use std::io::Write; |
| 3 | + |
| 4 | +use crate::hugr_io::HugrInputArgs; |
| 5 | +use anyhow::Result; |
| 6 | +use clap::Parser; |
| 7 | +use clio::Output; |
| 8 | +use hugr::NodeIndex; |
| 9 | +use hugr::envelope::ReadError; |
| 10 | +use hugr::envelope::description::{ExtensionDesc, ModuleDesc, PackageDesc}; |
| 11 | +use hugr::extension::Version; |
| 12 | +use hugr::package::Package; |
| 13 | +use tabled::Tabled; |
| 14 | +use tabled::derive::display; |
| 15 | + |
| 16 | +/// Convert between different HUGR envelope formats. |
| 17 | +#[derive(Parser, Debug)] |
| 18 | +#[clap(version = "1.0", long_about = None)] |
| 19 | +#[clap(about = "Describe the contents of a HUGR envelope.")] |
| 20 | +#[group(id = "hugr")] |
| 21 | +#[non_exhaustive] |
| 22 | +pub struct DescribeArgs { |
| 23 | + /// Hugr input. |
| 24 | + #[command(flatten)] |
| 25 | + pub input_args: HugrInputArgs, |
| 26 | + /// enumerate packaged extensions |
| 27 | + #[arg(long, default_value = "false")] |
| 28 | + pub packaged_extensions: bool, |
| 29 | + |
| 30 | + #[command(flatten)] |
| 31 | + /// Configure module description |
| 32 | + pub module_args: ModuleArgs, |
| 33 | + |
| 34 | + #[arg(long, default_value = "false")] |
| 35 | + /// Output in json format |
| 36 | + pub json: bool, |
| 37 | + |
| 38 | + /// Output file. Use '-' for stdout. |
| 39 | + #[clap(short, long, value_parser, default_value = "-")] |
| 40 | + pub output: Output, |
| 41 | +} |
| 42 | + |
| 43 | +/// Arguments for reading a HUGR input. |
| 44 | +#[derive(Debug, clap::Args)] |
| 45 | +pub struct ModuleArgs { |
| 46 | + #[arg(long, default_value = "false")] |
| 47 | + /// Don't display resolved extensions used by the module. |
| 48 | + pub no_resolved_extensions: bool, |
| 49 | + |
| 50 | + #[arg(long, default_value = "false")] |
| 51 | + /// Display public symbols in the module. |
| 52 | + pub public_symbols: bool, |
| 53 | + |
| 54 | + #[arg(long, default_value = "false")] |
| 55 | + /// Display claimed extensions set by generator in module metadata. |
| 56 | + pub generator_claimed_extensions: bool, |
| 57 | +} |
| 58 | +impl ModuleArgs { |
| 59 | + fn filter_module(&self, module: &mut ModuleDesc) { |
| 60 | + if self.no_resolved_extensions { |
| 61 | + module.used_extensions_resolved = None; |
| 62 | + } |
| 63 | + if !self.public_symbols { |
| 64 | + module.public_symbols = None; |
| 65 | + } |
| 66 | + if !self.generator_claimed_extensions { |
| 67 | + module.used_extensions_generator = None; |
| 68 | + } |
| 69 | + } |
| 70 | +} |
| 71 | +impl DescribeArgs { |
| 72 | + /// Convert a HUGR between different envelope formats |
| 73 | + pub fn run_describe(&mut self) -> Result<()> { |
| 74 | + let (mut desc, res) = match self.input_args.get_described_package() { |
| 75 | + Ok((d, p)) => (d, Ok(p)), |
| 76 | + Err(crate::CliError::ReadEnvelope(ReadError::Payload { |
| 77 | + source, |
| 78 | + partial_description, |
| 79 | + })) => (partial_description, Err(source)), // keep error for later |
| 80 | + Err(e) => return Err(e.into()), |
| 81 | + }; |
| 82 | + |
| 83 | + // clear fields that have not been requested |
| 84 | + for module in desc.modules.iter_mut().flatten() { |
| 85 | + self.module_args.filter_module(module); |
| 86 | + } |
| 87 | + |
| 88 | + let res = res.map_err(anyhow::Error::from); |
| 89 | + if self.json { |
| 90 | + if !self.packaged_extensions { |
| 91 | + desc.packaged_extensions.clear(); |
| 92 | + } |
| 93 | + self.output_json(desc, &res)?; |
| 94 | + } else { |
| 95 | + self.print_description(desc)?; |
| 96 | + } |
| 97 | + |
| 98 | + // bubble up any errors |
| 99 | + res.map(|_| ()) |
| 100 | + } |
| 101 | + |
| 102 | + fn print_description(&mut self, desc: PackageDesc) -> Result<()> { |
| 103 | + let header = desc.header(); |
| 104 | + writeln!( |
| 105 | + self.output, |
| 106 | + "{header}\nPackage contains {} module(s) and {} extension(s)", |
| 107 | + desc.n_modules(), |
| 108 | + desc.n_packaged_extensions() |
| 109 | + )?; |
| 110 | + let summaries: Vec<ModuleSummary> = desc |
| 111 | + .modules |
| 112 | + .iter() |
| 113 | + .map(|m| m.as_ref().map(Into::into).unwrap_or_default()) |
| 114 | + .collect(); |
| 115 | + let summary_table = tabled::Table::builder(summaries).index().build(); |
| 116 | + writeln!(self.output, "{summary_table}")?; |
| 117 | + |
| 118 | + for (i, module) in desc.modules.into_iter().enumerate() { |
| 119 | + writeln!(self.output, "\nModule {i}:")?; |
| 120 | + if let Some(module) = module { |
| 121 | + self.display_module(module)?; |
| 122 | + } |
| 123 | + } |
| 124 | + if self.packaged_extensions { |
| 125 | + writeln!(self.output, "Packaged extensions:")?; |
| 126 | + let ext_rows: Vec<ExtensionRow> = desc |
| 127 | + .packaged_extensions |
| 128 | + .into_iter() |
| 129 | + .flatten() |
| 130 | + .map(Into::into) |
| 131 | + .collect(); |
| 132 | + let ext_table = tabled::Table::new(ext_rows); |
| 133 | + writeln!(self.output, "{ext_table}")?; |
| 134 | + } |
| 135 | + Ok(()) |
| 136 | + } |
| 137 | + |
| 138 | + fn output_json(&mut self, package_desc: PackageDesc, res: &Result<Package>) -> Result<()> { |
| 139 | + let err_str = res.as_ref().err().map(|e| format!("{e:?}")); |
| 140 | + let json_desc = JsonDescription { |
| 141 | + package_desc, |
| 142 | + error: err_str, |
| 143 | + }; |
| 144 | + serde_json::to_writer_pretty(&mut self.output, &json_desc)?; |
| 145 | + Ok(()) |
| 146 | + } |
| 147 | + |
| 148 | + fn display_module(&mut self, desc: ModuleDesc) -> Result<()> { |
| 149 | + if let Some(exts) = desc.used_extensions_resolved { |
| 150 | + let ext_rows: Vec<ExtensionRow> = exts.into_iter().map(Into::into).collect(); |
| 151 | + let ext_table = tabled::Table::new(ext_rows); |
| 152 | + writeln!(self.output, "Resolved extensions:\n{ext_table}")?; |
| 153 | + } |
| 154 | + |
| 155 | + if let Some(syms) = desc.public_symbols { |
| 156 | + let sym_table = tabled::Table::new(syms.into_iter().map(|s| SymbolRow { symbol: s })); |
| 157 | + writeln!(self.output, "Public symbols:\n{sym_table}")?; |
| 158 | + } |
| 159 | + |
| 160 | + if let Some(exts) = desc.used_extensions_generator { |
| 161 | + let ext_rows: Vec<ExtensionRow> = exts.into_iter().map(Into::into).collect(); |
| 162 | + let ext_table = tabled::Table::new(ext_rows); |
| 163 | + writeln!(self.output, "Generator claimed extensions:\n{ext_table}")?; |
| 164 | + } |
| 165 | + |
| 166 | + Ok(()) |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +#[derive(serde::Serialize)] |
| 171 | +struct JsonDescription { |
| 172 | + #[serde(flatten)] |
| 173 | + package_desc: PackageDesc, |
| 174 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 175 | + error: Option<String>, |
| 176 | +} |
| 177 | + |
| 178 | +#[derive(Tabled)] |
| 179 | +struct ExtensionRow { |
| 180 | + name: String, |
| 181 | + version: Version, |
| 182 | +} |
| 183 | + |
| 184 | +#[derive(Tabled)] |
| 185 | +struct SymbolRow { |
| 186 | + #[tabled(rename = "Symbol")] |
| 187 | + symbol: String, |
| 188 | +} |
| 189 | + |
| 190 | +impl From<ExtensionDesc> for ExtensionRow { |
| 191 | + fn from(desc: ExtensionDesc) -> Self { |
| 192 | + Self { |
| 193 | + name: desc.name, |
| 194 | + version: desc.version, |
| 195 | + } |
| 196 | + } |
| 197 | +} |
| 198 | + |
| 199 | +#[derive(Tabled, Default)] |
| 200 | +struct ModuleSummary { |
| 201 | + #[tabled(display("display::option", "n/a"))] |
| 202 | + num_nodes: Option<usize>, |
| 203 | + #[tabled(display("display::option", "n/a"))] |
| 204 | + entrypoint_node: Option<usize>, |
| 205 | + #[tabled(display("display::option", "n/a"))] |
| 206 | + entrypoint_op: Option<String>, |
| 207 | + #[tabled(display("display::option", "n/a"))] |
| 208 | + generator: Option<String>, |
| 209 | +} |
| 210 | + |
| 211 | +impl From<&ModuleDesc> for ModuleSummary { |
| 212 | + fn from(desc: &ModuleDesc) -> Self { |
| 213 | + let (entrypoint_node, entrypoint_op) = if let Some(ep) = &desc.entrypoint { |
| 214 | + ( |
| 215 | + Some(ep.node.index()), |
| 216 | + Some(hugr::envelope::description::op_string(&ep.optype)), |
| 217 | + ) |
| 218 | + } else { |
| 219 | + (None, None) |
| 220 | + }; |
| 221 | + Self { |
| 222 | + num_nodes: desc.num_nodes, |
| 223 | + entrypoint_node, |
| 224 | + entrypoint_op, |
| 225 | + generator: desc.generator.clone(), |
| 226 | + } |
| 227 | + } |
| 228 | +} |
0 commit comments