conjure_oxide/utils/
conjure.rs

1use std::collections::BTreeMap;
2use std::io::Read;
3use std::path::Path;
4use std::string::ToString;
5use std::sync::{Arc, Mutex, RwLock};
6
7use conjure_core::ast::{Literal, Name};
8use conjure_core::context::Context;
9use rand::Rng as _;
10use serde_json::{from_str, Map, Value as JsonValue};
11use thiserror::Error as ThisError;
12
13use std::fs::File;
14
15use crate::model_from_json;
16use crate::solver::adaptors::Minion;
17use crate::solver::Solver;
18use crate::utils::json::sort_json_object;
19use crate::Error as ParseErr;
20use crate::Model;
21
22use glob::glob;
23
24#[derive(Debug, ThisError)]
25pub 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
38impl From<ParseErr> for EssenceParseError {
39    fn from(e: ParseErr) -> Self {
40        EssenceParseError::ParseError(e)
41    }
42}
43
44pub fn parse_essence_file(
45    path: &str,
46    filename: &str,
47    extension: &str,
48    context: Arc<RwLock<Context<'static>>>,
49) -> Result<Model, EssenceParseError> {
50    let mut cmd = std::process::Command::new("conjure");
51    let output = match cmd
52        .arg("pretty")
53        .arg("--output-format=astjson")
54        .arg(format!("{path}/{filename}.{extension}"))
55        .output()
56    {
57        Ok(output) => output,
58        Err(e) => return Err(EssenceParseError::ConjurePrettyError(e.to_string())),
59    };
60
61    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    }
66
67    let astjson = match String::from_utf8(output.stdout) {
68        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    let parsed_model = model_from_json(&astjson, context)?;
78    Ok(parsed_model)
79}
80
81pub fn get_minion_solutions(
82    model: Model,
83    num_sols: i32,
84) -> Result<Vec<BTreeMap<Name, Literal>>, anyhow::Error> {
85    let solver = Solver::new(Minion::new());
86    println!("Building Minion model...");
87    let solver = solver.load_model(model)?;
88
89    println!("Running Minion...");
90
91    let all_solutions_ref = Arc::new(Mutex::<Vec<BTreeMap<Name, Literal>>>::new(vec![]));
92    let all_solutions_ref_2 = all_solutions_ref.clone();
93    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        solver
110            .solve(Box::new(move |sols| {
111                let mut all_solutions = (*all_solutions_ref_2).lock().unwrap();
112                (*all_solutions).push(sols.into_iter().collect());
113                true
114            }))
115            .unwrap()
116    };
117
118    solver.save_stats_to_context();
119
120    #[allow(clippy::unwrap_used)]
121    let sols = (*all_solutions_ref).lock().unwrap();
122
123    Ok((*sols).clone())
124}
125
126#[allow(clippy::unwrap_used)]
127pub 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
212pub fn minion_solutions_to_json(solutions: &Vec<BTreeMap<Name, Literal>>) -> JsonValue {
213    let mut json_solutions = Vec::new();
214    for solution in solutions {
215        let mut json_solution = Map::new();
216        for (var_name, constant) in solution {
217            let serialized_constant = match constant {
218                Literal::Int(i) => JsonValue::Number((*i).into()),
219                Literal::Bool(b) => JsonValue::Bool(*b),
220                _ => panic!("Unsupported constant type"),
221            };
222            json_solution.insert(var_name.to_string(), serialized_constant);
223        }
224        json_solutions.push(JsonValue::Object(json_solution));
225    }
226    let ans = JsonValue::Array(json_solutions);
227    sort_json_object(&ans, true)
228}