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

            
61
175
    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
175
    }
66

            
67
175
    let astjson = match String::from_utf8(output.stdout) {
68
175
        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
175
    let parsed_model = model_from_json(&astjson, context)?;
78
175
    Ok(parsed_model)
79
175
}
80

            
81
175
pub fn get_minion_solutions(model: Model) -> Result<Vec<HashMap<Name, Literal>>, anyhow::Error> {
82
175
    let solver = Solver::new(Minion::new());
83
175

            
84
175
    println!("Building Minion model...");
85
175
    let solver = solver.load_model(model)?;
86

            
87
175
    println!("Running Minion...");
88
175

            
89
175
    let all_solutions_ref = Arc::new(Mutex::<Vec<HashMap<Name, Literal>>>::new(vec![]));
90
175
    let all_solutions_ref_2 = all_solutions_ref.clone();
91
175
    #[allow(clippy::unwrap_used)]
92
175
    let solver = solver
93
1825
        .solve(Box::new(move |sols| {
94
1825
            let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
95
1825
            (*all_solutions).push(sols);
96
1825
            true
97
1825
        }))
98
175
        .unwrap();
99
175

            
100
175
    solver.save_stats_to_context();
101
175

            
102
175
    #[allow(clippy::unwrap_used)]
103
175
    let sols = (*all_solutions_ref).lock().unwrap();
104
175

            
105
175
    Ok((*sols).clone())
106
175
}
107

            
108
#[allow(clippy::unwrap_used)]
109
pub fn get_solutions_from_conjure(
110
    essence_file: &str,
111
) -> Result<Vec<HashMap<Name, Literal>>, EssenceParseError> {
112
    // this is ran in parallel, and we have no guarantee by rust that invocations to this function
113
    // don't share the same tmp dir.
114
    let mut rng = rand::thread_rng();
115
    let rand: i8 = rng.gen();
116

            
117
    let mut tmp_dir = std::env::temp_dir();
118
    tmp_dir.push(Path::new(&rand.to_string()));
119

            
120
    let mut cmd = std::process::Command::new("conjure");
121
    let output = cmd
122
        .arg("solve")
123
        .arg("--output-format=json")
124
        .arg("--solutions-in-one-file")
125
        .arg("--number-of-solutions=all")
126
        .arg("--copy-solutions=no")
127
        .arg("-o")
128
        .arg(&tmp_dir)
129
        .arg(essence_file)
130
        .output()
131
        .map_err(|e| EssenceParseError::ConjureSolveError(e.to_string()))?;
132

            
133
    if !output.status.success() {
134
        return Err(EssenceParseError::ConjureSolveError(format!(
135
            "conjure solve exited with failure: {}",
136
            String::from_utf8(output.stderr).unwrap()
137
        )));
138
    }
139

            
140
    let solutions_files: Vec<_> = glob(&format!("{}/*.solutions.json", tmp_dir.display()))
141
        .unwrap()
142
        .collect();
143

            
144
    if solutions_files.is_empty() {
145
        return Err(EssenceParseError::ConjureNoSolutionsFile(
146
            tmp_dir.display().to_string(),
147
        ));
148
    }
149

            
150
    let solutions_file = solutions_files[0].as_ref().unwrap();
151
    let mut file = File::open(solutions_file).unwrap();
152

            
153
    let mut json_str = String::new();
154
    file.read_to_string(&mut json_str).unwrap();
155
    let mut json: JsonValue =
156
        from_str(&json_str).map_err(|e| EssenceParseError::ConjureSolutionsError(e.to_string()))?;
157
    json.sort_all_objects();
158

            
159
    let solutions = json
160
        .as_array()
161
        .ok_or(EssenceParseError::ConjureSolutionsError(
162
            "expected solutions to be an array".to_owned(),
163
        ))?;
164

            
165
    let mut solutions_set: Vec<HashMap<Name, Literal>> = Vec::new();
166

            
167
    for solution in solutions {
168
        let mut solution_map = HashMap::new();
169
        let solution = solution
170
            .as_object()
171
            .ok_or(EssenceParseError::ConjureSolutionsError(
172
                "invalid json".to_owned(),
173
            ))?;
174
        for (name, value) in solution {
175
            let name = Name::UserName(name.to_owned());
176
            let value = match value {
177
                JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
178
                JsonValue::Number(n) => Ok(Literal::Int(n.as_i64().unwrap().try_into().unwrap())),
179
                a => Err(EssenceParseError::ConjureSolutionsError(
180
                    format!("expected constant, got {}", a).to_owned(),
181
                )),
182
            }?;
183
            solution_map.insert(name, value);
184
        }
185
        solutions_set.push(solution_map);
186
    }
187

            
188
    Ok(solutions_set)
189
}
190

            
191
175
pub fn minion_solutions_to_json(solutions: &Vec<HashMap<Name, Literal>>) -> JsonValue {
192
175
    let mut json_solutions = Vec::new();
193
2000
    for solution in solutions {
194
1825
        let mut json_solution = Map::new();
195
6735
        for (var_name, constant) in solution {
196
4910
            let serialized_constant = match constant {
197
4910
                Literal::Int(i) => JsonValue::Number((*i).into()),
198
                Literal::Bool(b) => JsonValue::Bool(*b),
199
            };
200
4910
            json_solution.insert(var_name.to_string(), serialized_constant);
201
        }
202
1825
        json_solutions.push(JsonValue::Object(json_solution));
203
    }
204
175
    let ans = JsonValue::Array(json_solutions);
205
175
    sort_json_object(&ans, true)
206
175
}