diff --git a/conjure_oxide/src/ast.rs b/conjure_oxide/src/ast.rs index cbe95cbf4..b5bb941bd 100644 --- a/conjure_oxide/src/ast.rs +++ b/conjure_oxide/src/ast.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::fmt::Display; #[serde_as] -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Model { #[serde_as(as = "Vec<(_, _)>")] pub variables: HashMap, @@ -36,13 +36,13 @@ impl Default for Model { } } -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] pub enum Name { UserName(String), MachineName(i32), } -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DecisionVariable { pub domain: Domain, } @@ -87,9 +87,15 @@ pub enum Range { pub enum Expression { ConstantInt(i32), Reference(Name), + Sum(Vec), + Eq(Box, Box), + Neq(Box, Box), Geq(Box, Box), + Leq(Box, Box), + Gt(Box, Box), + Lt(Box, Box), // Flattened Constraints SumGeq(Vec, Box), diff --git a/conjure_oxide/src/parse.rs b/conjure_oxide/src/parse.rs index 1ed89e24a..0bfc7bcfd 100644 --- a/conjure_oxide/src/parse.rs +++ b/conjure_oxide/src/parse.rs @@ -1,16 +1,20 @@ -use crate::ast::{DecisionVariable, Domain, Model, Name, Range}; +use std::collections::HashMap; + +use serde_json::Value; + +use crate::ast::{DecisionVariable, Domain, Expression, Model, Name, Range}; use crate::error::{Error, Result}; use serde_json::Value as JsonValue; pub fn parse_json(str: &str) -> Result { let mut m = Model::new(); let v: JsonValue = serde_json::from_str(str)?; - let constraints = v["mStatements"] + let statements = v["mStatements"] .as_array() .ok_or(Error::Parse("mStatements is not an array".to_owned()))?; - for con in constraints { - let entry = con + for statement in statements { + let entry = statement .as_object() .ok_or(Error::Parse("mStatements contains a non-object".to_owned()))? .iter() @@ -23,12 +27,18 @@ pub fn parse_json(str: &str) -> Result { let (name, var) = parse_variable(entry.1)?; m.add_variable(name, var); } - "SuchThat" => parse_constraint(entry.1)?, - _ => { - return Err(Error::Parse( - "mStatements contains an unknown object".to_owned(), - )) + "SuchThat" => { + let constraints: Vec = entry + .1 + .as_array() + .unwrap() + .iter() + .flat_map(parse_expression) + .collect(); + m.constraints.extend(constraints); + // println!("Nb constraints {}", m.constraints.len()); } + otherwise => panic!("Unhandled Statement {:#?}", otherwise), } } @@ -90,14 +100,13 @@ fn parse_int_domain(v: &JsonValue) -> Result { .as_array() .ok_or(Error::Parse("RangeBounded is not an array".to_owned()))?; let mut nums = Vec::new(); - for i in 0..2 { - let num = - &arr[i]["Constant"]["ConstantInt"][1] - .as_i64() - .ok_or(Error::Parse( - "Could not parse int domain constant".to_owned(), - ))?; - let num32 = i32::try_from(*num).map_err(|_| { + for item in arr.iter() { + let num = item["Constant"]["ConstantInt"][1] + .as_i64() + .ok_or(Error::Parse( + "Could not parse int domain constant".to_owned(), + ))?; + let num32 = i32::try_from(num).map_err(|_| { Error::Parse("Could not parse int domain constant".to_owned()) })?; nums.push(num32); @@ -124,12 +133,93 @@ fn parse_int_domain(v: &JsonValue) -> Result { Ok(Domain::IntDomain(ranges)) } -fn parse_constraint(_obj: &JsonValue) -> Result<()> { - Ok(()) +// this needs an explicit type signature to force the closures to have the same type +type BinOp = Box, Box) -> Expression>; + +fn parse_expression(obj: &JsonValue) -> Option { + let binary_operators: HashMap<&str, BinOp> = [ + ("MkOpEq", Box::new(Expression::Eq) as Box _>), + ( + "MkOpNeq", + Box::new(Expression::Neq) as Box _>, + ), + ( + "MkOpGeq", + Box::new(Expression::Geq) as Box _>, + ), + ( + "MkOpLeq", + Box::new(Expression::Leq) as Box _>, + ), + ("MkOpGt", Box::new(Expression::Gt) as Box _>), + ("MkOpLt", Box::new(Expression::Lt) as Box _>), + ] + .into_iter() + .collect(); + + let mut binary_operator_names = binary_operators.iter().map(|x| x.0); + + match obj { + Value::Object(op) if op.contains_key("Op") => match &op["Op"] { + Value::Object(bin_op) if binary_operator_names.any(|key| bin_op.contains_key(*key)) => { + parse_bin_op(bin_op, binary_operators) + } + Value::Object(op_sum) if op_sum.contains_key("MkOpSum") => parse_sum(op_sum), + otherwise => panic!("Unhandled Op {:#?}", otherwise), + }, + Value::Object(refe) if refe.contains_key("Reference") => { + let name = refe["Reference"].as_array()?[0].as_object()?["Name"].as_str()?; + Some(Expression::Reference(Name::UserName(name.to_string()))) + } + Value::Object(constant) if constant.contains_key("Constant") => parse_constant(constant), + otherwise => panic!("Unhandled Expression {:#?}", otherwise), + } +} + +fn parse_sum(op_sum: &serde_json::Map) -> Option { + let args = &op_sum["MkOpSum"]["AbstractLiteral"]["AbsLitMatrix"][1]; + let args_parsed: Vec = args + .as_array()? + .iter() + .map(|x| parse_expression(x).unwrap()) + .collect(); + Some(Expression::Sum(args_parsed)) +} + +fn parse_bin_op( + bin_op: &serde_json::Map, + binary_operators: HashMap<&str, BinOp>, +) -> Option { + // we know there is a single key value pair in this object + // extract the value, ignore the key + let (key, value) = bin_op.into_iter().next()?; + + let constructor = binary_operators.get(key.as_str())?; + + match &value { + Value::Array(bin_op_args) if bin_op_args.len() == 2 => { + let arg1 = parse_expression(&bin_op_args[0])?; + let arg2 = parse_expression(&bin_op_args[1])?; + Some(constructor(Box::new(arg1), Box::new(arg2))) + } + otherwise => panic!("Unhandled parse_bin_op {:#?}", otherwise), + } +} + +fn parse_constant(constant: &serde_json::Map) -> Option { + match &constant["Constant"] { + Value::Object(int) if int.contains_key("ConstantInt") => Some(Expression::ConstantInt( + int["ConstantInt"].as_array()?[1] + .as_i64()? + .try_into() + .unwrap(), + )), + otherwise => panic!("Unhandled parse_constant {:#?}", otherwise), + } } impl Model { - pub fn from_json(str: &String) -> Result { + pub fn from_json(str: &str) -> Result { parse_json(str) } } diff --git a/conjure_oxide/tests/generated_tests.rs b/conjure_oxide/tests/generated_tests.rs index 03d86a8c8..fbb8d49ea 100644 --- a/conjure_oxide/tests/generated_tests.rs +++ b/conjure_oxide/tests/generated_tests.rs @@ -1,10 +1,10 @@ +use conjure_oxide::ast::Model; +use serde_json::Value; use std::env; use std::error::Error; use std::fs::File; use std::io::prelude::*; -use conjure_oxide::ast::Model; - use std::path::Path; fn main() { @@ -37,10 +37,15 @@ fn integration_test(path: &str, essence_base: &str) -> Result<(), Box // "parsing" astjson as Model let generated_mdl = Model::from_json(&astjson)?; + // a consistent sorting of the keys of json objects + // only required for the generated version + // since the expected version will already be sorted + let generated_json = sort_json_object(&serde_json::to_value(generated_mdl.clone())?); + // serialise to file - let generated_json = serde_json::to_string_pretty(&generated_mdl)?; + let generated_json_str = serde_json::to_string_pretty(&generated_json)?; File::create(format!("{path}/{essence_base}.generated.serialised.json"))? - .write_all(generated_json.as_bytes())?; + .write_all(generated_json_str.as_bytes())?; if std::env::var("ACCEPT").map_or(false, |v| v == "true") { std::fs::copy( @@ -55,8 +60,7 @@ fn integration_test(path: &str, essence_base: &str) -> Result<(), Box let expected_str = std::fs::read_to_string(format!("{path}/{essence_base}.expected.serialised.json"))?; - let mut expected_mdl: Model = serde_json::from_str(&expected_str)?; - expected_mdl.constraints = Vec::new(); // TODO - remove this line once we parse constraints + let expected_mdl: Model = serde_json::from_str(&expected_str)?; // -------------------------------------------------------------------------------- // assert that they are the same model @@ -66,4 +70,55 @@ fn integration_test(path: &str, essence_base: &str) -> Result<(), Box Ok(()) } +/// Recursively sorts the keys of all JSON objects within the provided JSON value. +/// +/// serde_json will output JSON objects in an arbitrary key order. +/// this is normally fine, except in our use case we wouldn't want to update the expected output again and again. +/// so a consistent (sorted) ordering of the keys is desirable. +fn sort_json_object(value: &Value) -> Value { + match value { + Value::Object(obj) => { + let mut ordered: Vec<(String, Value)> = obj + .iter() + .map(|(k, v)| { + if k == "variables" { + (k.clone(), sort_json_variables(v)) + } else { + (k.clone(), sort_json_object(v)) + } + }) + // .map(|(k, v)| (k.clone(), sort_json_object(v))) + .collect(); + ordered.sort_by(|a, b| a.0.cmp(&b.0)); + + Value::Object(ordered.into_iter().collect()) + } + Value::Array(arr) => Value::Array(arr.iter().map(sort_json_object).collect()), + _ => value.clone(), + } +} + +/// Sort the "variables" field by name. +/// We have to do this separately becasue that field is not a JSON object, instead it's an array of tuples. +fn sort_json_variables(value: &Value) -> Value { + match value { + Value::Array(vars) => { + let mut vars_sorted = vars.clone(); + vars_sorted.sort_by(|a, b| { + let a_obj = &a.as_array().unwrap()[0]; + let a_name: conjure_oxide::ast::Name = + serde_json::from_value(a_obj.clone()).unwrap(); + + let b_obj = &b.as_array().unwrap()[0]; + let b_name: conjure_oxide::ast::Name = + serde_json::from_value(b_obj.clone()).unwrap(); + + a_name.cmp(&b_name) + }); + Value::Array(vars_sorted) + } + _ => value.clone(), + } +} + include!(concat!(env!("OUT_DIR"), "/gen_tests.rs")); diff --git a/conjure_oxide/tests/integration/basic/bool-01/bool-01.expected.serialised.json b/conjure_oxide/tests/integration/basic/bool-01/bool-01.expected.serialised.json index 06dbb91f2..102e718db 100644 --- a/conjure_oxide/tests/integration/basic/bool-01/bool-01.expected.serialised.json +++ b/conjure_oxide/tests/integration/basic/bool-01/bool-01.expected.serialised.json @@ -1,4 +1,5 @@ { + "constraints": [], "variables": [ [ { @@ -8,6 +9,5 @@ "domain": "BoolDomain" } ] - ], - "constraints": [] + ] } \ No newline at end of file diff --git a/conjure_oxide/tests/integration/basic/bool-02/bool-02.expected.serialised.json b/conjure_oxide/tests/integration/basic/bool-02/bool-02.expected.serialised.json index d5883b4ff..c652dff47 100644 --- a/conjure_oxide/tests/integration/basic/bool-02/bool-02.expected.serialised.json +++ b/conjure_oxide/tests/integration/basic/bool-02/bool-02.expected.serialised.json @@ -1,8 +1,9 @@ { + "constraints": [], "variables": [ [ { - "UserName": "y" + "UserName": "x" }, { "domain": "BoolDomain" @@ -10,12 +11,11 @@ ], [ { - "UserName": "x" + "UserName": "y" }, { "domain": "BoolDomain" } ] - ], - "constraints": [] + ] } \ No newline at end of file diff --git a/conjure_oxide/tests/integration/basic/bool-03/bool-03.expected.serialised.json b/conjure_oxide/tests/integration/basic/bool-03/bool-03.expected.serialised.json index 2ef697aed..46c572cb7 100644 --- a/conjure_oxide/tests/integration/basic/bool-03/bool-03.expected.serialised.json +++ b/conjure_oxide/tests/integration/basic/bool-03/bool-03.expected.serialised.json @@ -1,4 +1,20 @@ { + "constraints": [ + { + "Neq": [ + { + "Reference": { + "UserName": "x" + } + }, + { + "Reference": { + "UserName": "y" + } + } + ] + } + ], "variables": [ [ { @@ -16,6 +32,5 @@ "domain": "BoolDomain" } ] - ], - "constraints": [] + ] } \ No newline at end of file diff --git a/conjure_oxide/tests/integration/xyz/input.expected.serialised.json b/conjure_oxide/tests/integration/xyz/input.expected.serialised.json index f7a71e68e..249f50953 100644 --- a/conjure_oxide/tests/integration/xyz/input.expected.serialised.json +++ b/conjure_oxide/tests/integration/xyz/input.expected.serialised.json @@ -1,4 +1,50 @@ { + "constraints": [ + { + "Eq": [ + { + "Sum": [ + { + "Sum": [ + { + "Reference": { + "UserName": "a" + } + }, + { + "Reference": { + "UserName": "b" + } + } + ] + }, + { + "Reference": { + "UserName": "c" + } + } + ] + }, + { + "ConstantInt": 4 + } + ] + }, + { + "Geq": [ + { + "Reference": { + "UserName": "a" + } + }, + { + "Reference": { + "UserName": "b" + } + } + ] + } + ], "variables": [ [ { @@ -51,6 +97,5 @@ } } ] - ], - "constraints": [] + ] } \ No newline at end of file