1
use std::collections::BTreeMap;
2
use std::io::Read;
3
use std::path::Path;
4
use std::string::ToString;
5
use std::sync::{Arc, Mutex, RwLock};
6

            
7
use conjure_core::ast::{Literal, Name};
8
use conjure_core::context::Context;
9
use rand::Rng as _;
10
use serde_json::{from_str, Map, Value as JsonValue};
11
use thiserror::Error as ThisError;
12

            
13
use std::fs::File;
14

            
15
use crate::model_from_json;
16
use crate::solver::adaptors::Minion;
17
use crate::solver::Solver;
18
use crate::utils::json::sort_json_object;
19
use crate::Error as ParseErr;
20
use crate::Model;
21

            
22
use glob::glob;
23

            
24
#[derive(Debug, ThisError)]
25
pub enum EssenceParseError {
26
    #[error("Error running conjure pretty: {0}")]
27
    ConjurePrettyError(String),
28
    #[error("Error running conjure solve: {0}")]
29
    ConjureSolveError(String),
30
    #[error("Error parsing essence file: {0}")]
31
    ParseError(ParseErr),
32
    #[error("Error parsing Conjure solutions file: {0}")]
33
    ConjureSolutionsError(String),
34
    #[error("No solutions file for {0}")]
35
    ConjureNoSolutionsFile(String),
36
}
37

            
38
impl From<ParseErr> for EssenceParseError {
39
    fn from(e: ParseErr) -> Self {
40
        EssenceParseError::ParseError(e)
41
    }
42
}
43

            
44
623
pub fn parse_essence_file(
45
623
    path: &str,
46
623
    filename: &str,
47
623
    extension: &str,
48
623
    context: Arc<RwLock<Context<'static>>>,
49
623
) -> Result<Model, EssenceParseError> {
50
623
    let mut cmd = std::process::Command::new("conjure");
51
623
    let output = match cmd
52
623
        .arg("pretty")
53
623
        .arg("--output-format=astjson")
54
623
        .arg(format!("{path}/{filename}.{extension}"))
55
623
        .output()
56
    {
57
623
        Ok(output) => output,
58
        Err(e) => return Err(EssenceParseError::ConjurePrettyError(e.to_string())),
59
    };
60

            
61
623
    if !output.status.success() {
62
        let stderr_string = String::from_utf8(output.stderr)
63
            .unwrap_or("stderr is not a valid UTF-8 string".to_string());
64
        return Err(EssenceParseError::ConjurePrettyError(stderr_string));
65
623
    }
66

            
67
623
    let astjson = match String::from_utf8(output.stdout) {
68
623
        Ok(astjson) => astjson,
69
        Err(e) => {
70
            return Err(EssenceParseError::ConjurePrettyError(format!(
71
                "Error parsing output from conjure: {:#?}",
72
                e
73
            )))
74
        }
75
    };
76

            
77
623
    let parsed_model = model_from_json(&astjson, context)?;
78
623
    Ok(parsed_model)
79
623
}
80

            
81
616
pub fn get_minion_solutions(
82
616
    model: Model,
83
616
    num_sols: i32,
84
616
) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
85
616
    let solver = Solver::new(Minion::new());
86
616
    println!("Building Minion model...");
87
616
    let solver = solver.load_model(model)?;
88

            
89
616
    println!("Running Minion...");
90
616

            
91
616
    let all_solutions_ref = Arc::new(Mutex::<Vec<BTreeMap<Name, Literal>>>::new(vec![]));
92
616
    let all_solutions_ref_2 = all_solutions_ref.clone();
93
616
    let solver = if num_sols > 0 {
94
        let sols_left = Mutex::new(num_sols);
95

            
96
        #[allow(clippy::unwrap_used)]
97
        solver
98
            .solve(Box::new(move |sols| {
99
                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
100
                (*all_solutions).push(sols.into_iter().collect());
101
                let mut sols_left = sols_left.lock().unwrap();
102
                *sols_left -= 1;
103

            
104
                *sols_left != 0
105
            }))
106
            .unwrap()
107
    } else {
108
        #[allow(clippy::unwrap_used)]
109
616
        solver
110
23506
            .solve(Box::new(move |sols| {
111
23506
                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
112
23506
                (*all_solutions).push(sols.into_iter().collect());
113
23506
                true
114
23506
            }))
115
616
            .unwrap()
116
    };
117

            
118
616
    solver.save_stats_to_context();
119
616

            
120
616
    #[allow(clippy::unwrap_used)]
121
616
    let sols = (*all_solutions_ref).lock().unwrap();
122
616

            
123
616
    Ok((*sols).clone())
124
616
}
125

            
126
#[allow(clippy::unwrap_used)]
127
pub fn get_solutions_from_conjure(
128
    essence_file: &str,
129
) -> Result<Vec<BTreeMap<Name, Literal>>, EssenceParseError> {
130
    // this is ran in parallel, and we have no guarantee by rust that invocations to this function
131
    // don't share the same tmp dir.
132
    let mut rng = rand::rng();
133
    let rand: i8 = rng.random();
134

            
135
    let mut tmp_dir = std::env::temp_dir();
136
    tmp_dir.push(Path::new(&rand.to_string()));
137

            
138
    let mut cmd = std::process::Command::new("conjure");
139
    let output = cmd
140
        .arg("solve")
141
        .arg("--output-format=json")
142
        .arg("--solutions-in-one-file")
143
        .arg("--number-of-solutions=all")
144
        .arg("--copy-solutions=no")
145
        .arg("-o")
146
        .arg(&tmp_dir)
147
        .arg(essence_file)
148
        .output()
149
        .map_err(|e| EssenceParseError::ConjureSolveError(e.to_string()))?;
150

            
151
    if !output.status.success() {
152
        return Err(EssenceParseError::ConjureSolveError(format!(
153
            "conjure solve exited with failure: {}",
154
            String::from_utf8(output.stderr).unwrap()
155
        )));
156
    }
157

            
158
    let solutions_files: Vec<_> = glob(&format!("{}/*.solutions.json", tmp_dir.display()))
159
        .unwrap()
160
        .collect();
161

            
162
    if solutions_files.is_empty() {
163
        return Err(EssenceParseError::ConjureNoSolutionsFile(
164
            tmp_dir.display().to_string(),
165
        ));
166
    }
167

            
168
    let solutions_file = solutions_files[0].as_ref().unwrap();
169
    let mut file = File::open(solutions_file).unwrap();
170

            
171
    let mut json_str = String::new();
172
    file.read_to_string(&mut json_str).unwrap();
173
    let mut json: JsonValue =
174
        from_str(&json_str).map_err(|e| EssenceParseError::ConjureSolutionsError(e.to_string()))?;
175
    json.sort_all_objects();
176

            
177
    let solutions = json
178
        .as_array()
179
        .ok_or(EssenceParseError::ConjureSolutionsError(
180
            "expected solutions to be an array".to_owned(),
181
        ))?;
182

            
183
    let mut solutions_set: Vec<BTreeMap<Name, Literal>> = Vec::new();
184

            
185
    for solution in solutions {
186
        let mut solution_map = BTreeMap::new();
187
        let solution = solution
188
            .as_object()
189
            .ok_or(EssenceParseError::ConjureSolutionsError(
190
                "invalid json".to_owned(),
191
            ))?;
192
        for (name, value) in solution {
193
            let name = Name::UserName(name.to_owned());
194
            let value = match value {
195
                JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
196
                JsonValue::Number(n) => Ok(Literal::Int(n.as_i64().unwrap().try_into().unwrap())),
197
                a => Err(EssenceParseError::ConjureSolutionsError(
198
                    format!("expected constant, got {}", a).to_owned(),
199
                )),
200
            }?;
201
            solution_map.insert(name, value);
202
        }
203

            
204
        if !solution.is_empty() {
205
            solutions_set.push(solution_map);
206
        }
207
    }
208

            
209
    Ok(solutions_set)
210
}
211

            
212
616
pub fn minion_solutions_to_json(solutions: &Vec<BTreeMap<Name, Literal>>) -> JsonValue {
213
616
    let mut json_solutions = Vec::new();
214
24122
    for solution in solutions {
215
23506
        let mut json_solution = Map::new();
216
205765
        for (var_name, constant) in solution {
217
182259
            let serialized_constant = match constant {
218
182259
                Literal::Int(i) => JsonValue::Number((*i).into()),
219
                Literal::Bool(b) => JsonValue::Bool(*b),
220
            };
221
182259
            json_solution.insert(var_name.to_string(), serialized_constant);
222
        }
223
23506
        json_solutions.push(JsonValue::Object(json_solution));
224
    }
225
616
    let ans = JsonValue::Array(json_solutions);
226
616
    sort_json_object(&ans, true)
227
616
}