conjure_oxide/utils/
conjure.rs1use 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 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}