Skip to content

Commit fe316f2

Browse files
committed
feat(cli): describe sub-command
1 parent d83cebc commit fe316f2

File tree

6 files changed

+580
-1
lines changed

6 files changed

+580
-1
lines changed

Cargo.lock

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hugr-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ clap-verbosity-flag.workspace = true
2121
derive_more = { workspace = true, features = ["display", "error", "from"] }
2222
hugr = { path = "../hugr", version = "0.24.0" }
2323
serde_json.workspace = true
24+
serde = { workspace = true, features = ["derive"] }
2425
clio = { workspace = true, features = ["clap-parse"] }
2526
anyhow.workspace = true
2627
thiserror.workspace = true
2728
tracing = "0.1.41"
2829
tracing-subscriber = { version = "0.3.20", features = ["fmt"] }
30+
tabled = "0.20.0"
2931

3032
[lints]
3133
workspace = true

hugr-cli/src/describe.rs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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+
}

hugr-cli/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ use std::ffi::OsString;
2727
use thiserror::Error;
2828

2929
pub mod convert;
30+
pub mod describe;
3031
pub mod extensions;
3132
pub mod hugr_io;
3233
pub mod mermaid;
3334
pub mod validate;
34-
3535
/// CLI arguments.
3636
#[derive(Parser, Debug)]
3737
#[clap(version = crate_version!(), long_about = None)]
@@ -61,6 +61,9 @@ pub enum CliCommand {
6161
/// External commands
6262
#[command(external_subcommand)]
6363
External(Vec<OsString>),
64+
65+
/// Describe the contents of a HUGR envelope.
66+
Describe(describe::DescribeArgs),
6467
}
6568

6669
/// Error type for the CLI.

hugr-cli/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ fn main() {
3030
CliCommand::GenExtensions(args) => args.run_dump(&hugr::std_extensions::STD_REG),
3131
CliCommand::Mermaid(mut args) => args.run_print(),
3232
CliCommand::Convert(mut args) => args.run_convert(),
33+
CliCommand::Describe(mut args) => args.run_describe(),
3334
CliCommand::External(args) => run_external(args),
3435
_ => Err(anyhow!("Unknown command")),
3536
};
3637

3738
if let Err(err) = result {
3839
error!("{:?}", err);
40+
// TODO include description if verbosity is high enough
3941
std::process::exit(1);
4042
}
4143
}

0 commit comments

Comments
 (0)