1use crate::{RunOptions, Spec, blocks, run, search, verify};
32use std::path::PathBuf;
33use std::process::ExitCode;
34
35const USAGE: &str = "usage:
36 comnum run SPEC... [--out DIR] [--threads N] [--fast-dev-run] [--profile] [--quiet]
37 comnum check SPEC...
38 comnum verify SPEC... [--out DIR] [--threads N] [--quiet]
39 comnum conditions SPEC
40 comnum blocks [NAME...] [--json]
41 comnum schema
42
43SPEC is a YAML, JSON or TOML specification (see the documentation of the crate comnum-spec).";
44
45#[allow(clippy::struct_excessive_bools, reason = "the flags of the command line")]
46struct Args {
47 command: String,
48 files: Vec<PathBuf>,
49 out: PathBuf,
50 threads: Option<usize>,
51 fast_dev_run: bool,
52 profile: bool,
53 quiet: bool,
54 json: bool,
55}
56
57fn parse_args(args: impl IntoIterator<Item = String>) -> Result<Args, String> {
58 let mut it = args.into_iter();
59 let command = it.next().ok_or_else(|| USAGE.to_string())?;
60 let mut a = Args {
61 command,
62 files: Vec::new(),
63 out: PathBuf::from("results"),
64 threads: None,
65 fast_dev_run: false,
66 profile: false,
67 quiet: false,
68 json: false,
69 };
70 while let Some(arg) = it.next() {
71 match arg.as_str() {
72 "--out" => a.out = PathBuf::from(it.next().ok_or("--out needs a directory")?),
73 "--threads" => {
74 let n = it.next().ok_or("--threads needs a number")?;
75 a.threads = Some(n.parse().map_err(|_| format!("--threads: {n:?} is not a number"))?);
76 }
77 "--fast-dev-run" => a.fast_dev_run = true,
78 "--profile" => a.profile = true,
79 "--quiet" => a.quiet = true,
80 "--json" => a.json = true,
81 "-h" | "--help" => return Err(USAGE.to_string()),
82 s if s.starts_with("--") => return Err(format!("unknown option {s}\n\n{USAGE}")),
83 s => a.files.push(PathBuf::from(s)),
84 }
85 }
86 Ok(a)
87}
88
89fn report_options(spec: &Spec, a: &Args) -> RunOptions {
92 RunOptions {
93 threads: a.threads,
94 progress: Some(!a.quiet),
95 csv: Some(spec.csv.clone().unwrap_or_else(|| a.out.join(format!("{}.csv", spec.name)))),
96 json: Some(spec.json.clone().unwrap_or_else(|| a.out.join(format!("{}.json", spec.name)))),
97 ..RunOptions::default()
98 }
99}
100
101fn written(opts: &RunOptions, a: &Args) {
103 if let (false, Some(csv), Some(json)) = (a.quiet, &opts.csv, &opts.json) {
104 eprintln!("written {} and {}", csv.display(), json.display());
105 }
106}
107
108fn run_one(path: &PathBuf, a: &Args) -> comnum_core::Result<()> {
109 let spec = Spec::from_path(path)?;
110 let opts =
111 RunOptions { fast_dev_run: a.fast_dev_run, profile: a.profile.then_some(true), ..report_options(&spec, a) };
112 if let Some(sp) = &spec.search {
113 if !a.quiet {
114 eprintln!(
115 "{}: {} (search of {} for {} = {:e})",
116 spec.name, spec.description, sp.along, sp.metric, sp.target
117 );
118 }
119 for r in search::search(&spec, &opts)? {
120 println!(
121 "{} | {} = {:.4} [{:.4}, {:.4}] | {} evaluations{}",
122 r.condition.label(),
123 r.along,
124 r.value,
125 r.lo,
126 r.hi,
127 r.evaluations.len(),
128 if r.converged { "" } else { " (not converged)" }
129 );
130 }
131 written(&opts, a);
132 return Ok(());
133 }
134 if !a.quiet {
135 eprintln!("{}: {} ({} conditions)", spec.name, spec.description, spec.conditions().len());
136 }
137 let results = run(&spec, &opts)?;
138 for r in &results {
139 let metrics: Vec<String> = r
140 .record
141 .iter()
142 .map(|(k, e)| {
143 let (lo, hi) = e.interval(spec.confidence);
144 format!("{k}={:.4e} [{lo:.3e}, {hi:.3e}]", e.value())
145 })
146 .collect();
147 println!("{} | {} | {} trials | {}", r.condition.label(), r.status.as_str(), r.trials, metrics.join(" "));
148 }
149 let profile = crate::profile_of(&spec, &results)?;
150 if !profile.is_empty() {
151 println!("\nprofile: mean time per trial over {} conditions", results.len());
152 for row in &profile {
153 println!("{:>24} {:>10.3} ms {:>5.1} %", row.label, row.time_ms, 100.0 * row.share);
154 }
155 }
156 written(&opts, a);
157 Ok(())
158}
159
160fn verify_one(path: &PathBuf, a: &Args) -> comnum_core::Result<usize> {
162 let spec = Spec::from_path(path)?;
163 let opts = report_options(&spec, a);
165 let (_, checks) = verify::verify_spec(&spec, &opts)?;
166 let failed = checks.iter().filter(|c| !c.passed).count();
167 println!("{}: {} checks, {} failed", spec.name, checks.len(), failed);
168 for c in &checks {
169 println!(
170 " {} {} | {} | {}: expected {:.4e}, measured {:.4e} [{:.3e}, {:.3e}]",
171 if c.passed { "ok " } else { "FAIL" },
172 c.name,
173 c.condition,
174 c.metric,
175 c.expected,
176 c.measured.value,
177 c.measured.lo,
178 c.measured.hi
179 );
180 }
181 verify::write_report(&a.out.join(format!("{}.verify.json", spec.name)), &spec, &opts, &checks)?;
182 Ok(failed)
183}
184
185fn check_one(path: &PathBuf) -> comnum_core::Result<()> {
186 let spec = Spec::from_path(path)?;
187 let mut spec = search::first_points(&spec).unwrap_or(spec);
189 (spec.csv, spec.json) = (None, None);
191 let opts = RunOptions { fast_dev_run: true, batch: Some(1), progress: Some(false), ..RunOptions::default() };
192 let r = run(&spec, &opts)?;
193 let names: Vec<&String> = r.first().map(|c| c.record.iter().map(|(k, _)| k).collect()).unwrap_or_default();
194 println!("{}: ok, {} conditions, {} blocks, metrics {:?}", path.display(), r.len(), spec.link.len(), names);
195 Ok(())
196}
197
198fn to_json(v: &impl serde::Serialize) -> comnum_core::Result<String> {
200 serde_json::to_string_pretty(v).map_err(|e| comnum_core::Error::Config(format!("cannot serialise the report: {e}")))
201}
202
203pub fn main(args: impl IntoIterator<Item = String>) -> ExitCode {
206 let a = match parse_args(args) {
207 Ok(a) => a,
208 Err(msg) => {
209 eprintln!("{msg}");
210 return ExitCode::from(2);
211 }
212 };
213 if ["run", "check", "verify", "conditions"].contains(&a.command.as_str()) && a.files.is_empty() {
214 eprintln!("no specification given\n\n{USAGE}");
215 return ExitCode::from(2);
216 }
217 let result: comnum_core::Result<()> = match a.command.as_str() {
218 "blocks" => {
219 let all = blocks::registry();
220 let wanted: Vec<String> = a.files.iter().map(|f| f.display().to_string()).collect();
221 let shown: Vec<_> =
222 all.iter().filter(|b| wanted.is_empty() || wanted.iter().any(|w| w == b.name)).collect();
223 if shown.is_empty() {
224 Err(comnum_core::Error::Config(format!("no block {wanted:?}")))
225 } else if a.json {
226 let all: Vec<_> = shown.iter().map(|b| b.to_json()).collect();
227 to_json(&all).map(|j| println!("{j}"))
228 } else {
229 for b in shown {
230 println!("{}", b.describe());
231 }
232 Ok(())
233 }
234 }
235 "schema" => to_json(&blocks::json_schema()).map(|j| println!("{j}")),
236 "conditions" => a.files.iter().try_for_each(|f| {
237 let spec = Spec::from_path(f)?;
238 for (i, c) in spec.conditions().iter().enumerate() {
239 println!("{i}: {}", c.label());
240 }
241 Ok(())
242 }),
243 "check" => a.files.iter().try_for_each(check_one),
244 "run" => a.files.iter().try_for_each(|f| run_one(f, &a)),
245 "verify" => {
246 let mut failed = 0;
247 let r = a.files.iter().try_for_each(|f| verify_one(f, &a).map(|n| failed += n));
248 if r.is_ok() && failed > 0 {
249 eprintln!("{failed} checks failed");
250 return ExitCode::FAILURE;
251 }
252 r
253 }
254 other => {
255 eprintln!("unknown command {other:?}\n\n{USAGE}");
256 return ExitCode::from(2);
257 }
258 };
259 match result {
260 Ok(()) => ExitCode::SUCCESS,
261 Err(e) => {
262 eprintln!("error: {e}");
263 ExitCode::FAILURE
264 }
265 }
266}