Skip to main content

sva_cli/
solve.rs

1// Concern: runs one solve here: its store, a signal stopping it, progress lines, values written back | Non-concern: the search and the cost (sva-core) | IO: (SolveArgs) -> JSON, an exit, stderr, files
2
3use std::io::Write;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use sva_ast::variable;
9use sva_core::solve::{
10    Point, Problem, Solved, Stopped, Vary, findings, point_json, solve as solved, solved_data,
11};
12use sva_core::{CliError, error_envelope_with, success_envelope};
13use sva_engine::Abandon;
14
15use crate::args::SolveArgs;
16use crate::heard::Heard;
17use crate::store::{On, opened, persisted, wait};
18
19/// The exit a solve stopped by a signal answers, as a shell reports one.
20pub const CANCELLED: u8 = 130;
21
22struct Flagged(Arc<AtomicBool>);
23
24impl Abandon for Flagged {
25    fn abandoned(&self) -> Pin<Box<dyn Future<Output = bool> + '_>> {
26        Box::pin(async { self.0.load(Ordering::Relaxed) })
27    }
28}
29
30/// The envelope and the exit it answers with.
31pub fn solve(args: &SolveArgs) -> Result<(String, u8), CliError> {
32    let dir = crate::composition::composition()?;
33    let heard = Heard::at(&dir)?;
34    let stop = Arc::new(AtomicBool::new(false));
35    let mut warnings = Vec::new();
36    let tier = opened(&args.cache, &mut warnings, On::Flag(&stop))?;
37    let problem = Problem {
38        source: &heard,
39        cost: args.cost.clone(),
40        vary: args.vary.clone(),
41        sets: args.sets.clone(),
42        rate: args.rate,
43        threads: args.threads,
44        stops: args.stops,
45    };
46    let mut heard_point = |point: &Point| {
47        // A reader that closed stderr loses the progress, never the answer.
48        let _ = writeln!(std::io::stderr(), "{}", point_json(point));
49    };
50    let outcome = wait(solved(&problem, tier, &mut heard_point, &Flagged(stop)));
51    persisted(tier, &mut warnings);
52    let solved = outcome?;
53    warnings.extend(findings(&solved, &args.vary));
54    let stopped = solved.stopped_by == Stopped::Cancelled;
55    if stopped {
56        let data = solved_data(&solved, &args.vary, "");
57        let message = format!(
58            "the solve was stopped by a signal after {} render(s); `data` holds the best so far, \
59             and no file was written",
60            solved.renders
61        );
62        let json = error_envelope_with("cancelled", &message, &data, &warnings);
63        return Ok((json, CANCELLED));
64    }
65    if !args.write {
66        let data = solved_data(&solved, &args.vary, "");
67        return Ok((success_envelope(&data, &warnings), 0));
68    }
69    let changes = changed(&dir, &solved, &args.vary)?;
70    let listed = sva_core::json::list(&changes, |c| {
71        format!(
72            "{{ \"path\": \"{}\", \"from\": \"{}\", \"to\": \"{}\" }}",
73            sva_core::json::escape(&c.path),
74            sva_core::json::escape(&c.from),
75            sva_core::json::escape(&c.to)
76        )
77    });
78    if !changes.is_empty() && !args.confirm {
79        let data = solved_data(&solved, &args.vary, &format!(", \"would_write\": {listed}"));
80        let message = format!(
81            "`--write` would change {} variable file(s), and replacing a file needs \
82             `--confirm`; nothing was written",
83            changes.len()
84        );
85        let json = error_envelope_with("conflict", &message, &data, &warnings);
86        return Ok((json, 4));
87    }
88    for change in &changes {
89        std::fs::write(dir.join(&change.path), &change.text)
90            .map_err(|e| CliError::Io(format!("could not write `{}`: {e}", change.path)))?;
91    }
92    let data = solved_data(&solved, &args.vary, &format!(", \"written\": {listed}"));
93    Ok((success_envelope(&data, &warnings), 0))
94}
95
96struct Change {
97    path: String,
98    from: String,
99    to: String,
100    text: String,
101}
102
103/// Each variable whose file would hold another value, its comments kept.
104fn changed(dir: &std::path::Path, solved: &Solved, vary: &[Vary]) -> Result<Vec<Change>, CliError> {
105    let mut out = Vec::new();
106    for ((name, _, to), _) in solved.best.values.iter().zip(vary) {
107        let path = variable(name);
108        let text = std::fs::read_to_string(dir.join(&path))
109            .map_err(|e| CliError::Io(format!("could not read `{path}`: {e}")))?;
110        let at = sva_ast::body_at(&text);
111        let body = &text[at..];
112        let from = sva_ast::strip_line_comment(body).trim().to_string();
113        if from == *to {
114            continue;
115        }
116        let comment = body.find(';').map(|c| format!(" {}", body[c..].trim_end()));
117        out.push(Change {
118            text: format!("{}{to}{}\n", &text[..at], comment.unwrap_or_default()),
119            path,
120            from,
121            to: to.clone(),
122        });
123    }
124    Ok(out)
125}