Skip to content

Commit 16e29da

Browse files
authored
feat: Parser and AST support for dominanceRelation (#691)
* feat: add parser support for dominance relations * test: add dominance parsing tests * make clippy happy * fix clippy
1 parent b49d647 commit 16e29da

21 files changed

+339
-60
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

conjure_oxide/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,17 @@ glob = "0.3.2"
3535
rand = "0.9.0"
3636
tracing-appender = "0.2"
3737
tracing-log = "0.2.0"
38-
tracing-subscriber = { version = "0.3.18", features = ["ansi", "env-filter", "json"] }
38+
tracing-subscriber = { version = "0.3.18", features = [
39+
"ansi",
40+
"env-filter",
41+
"json",
42+
] }
3943
tracing = "0.1.41"
4044
tree-sitter = "0.24.7"
4145
tree-sitter-essence = { version = "0.1.0", path = "../crates/tree-sitter-essence" }
4246
tree-sitter-haskell = "0.23.0"
4347
git-version = "0.3.9"
48+
project-root = "0.2.2"
4449

4550
[features]
4651

conjure_oxide/examples/solver-hello-minion.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use conjure_core::{
66
ast::{Literal, Name},
77
solver::{adaptors::Minion, states::ExecutionSuccess},
88
};
9-
use conjure_oxide::defaults::get_default_rule_sets;
9+
use conjure_oxide::defaults::DEFAULT_RULE_SETS;
1010
use itertools::Itertools;
1111
use std::collections::HashMap;
1212

@@ -25,7 +25,7 @@ pub fn main() {
2525
println!("Input model: \n {model} \n",);
2626

2727
// TODO: We will have a nicer way to do this in the future
28-
let rule_sets = resolve_rule_sets(SolverFamily::Minion, &get_default_rule_sets()).unwrap();
28+
let rule_sets = resolve_rule_sets(SolverFamily::Minion, DEFAULT_RULE_SETS).unwrap();
2929

3030
let model = rewrite_model(&model, &rule_sets).unwrap();
3131
println!("Rewritten model: \n {model} \n",);

conjure_oxide/src/defaults.rs

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1 @@
1-
/// Returns the default rule sets, excluding solver specific ones.
2-
pub fn get_default_rule_sets() -> Vec<String> {
3-
vec![
4-
"Base".to_string(),
5-
"Constant".to_string(),
6-
"Bubble".to_string(),
7-
]
8-
}
1+
pub static DEFAULT_RULE_SETS: &[&str] = &["Base", "Constant", "Bubble"];

conjure_oxide/src/main.rs

+15-13
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ use std::sync::Arc;
77
use anyhow::Result as AnyhowResult;
88
use anyhow::{anyhow, bail};
99
use clap::{arg, command, Parser};
10-
use conjure_core::rule_engine::rewrite_naive;
11-
use conjure_core::Model;
12-
use conjure_oxide::defaults::get_default_rule_sets;
10+
use git_version::git_version;
1311
use schemars::schema_for;
1412
use serde_json::to_string_pretty;
13+
use tracing_subscriber::filter::LevelFilter;
14+
use tracing_subscriber::layer::SubscriberExt as _;
15+
use tracing_subscriber::util::SubscriberInitExt as _;
16+
use tracing_subscriber::{EnvFilter, Layer};
1517

1618
use conjure_core::context::Context;
19+
use conjure_core::rule_engine::rewrite_naive;
20+
use conjure_core::Model;
21+
22+
use conjure_oxide::defaults::DEFAULT_RULE_SETS;
1723
use conjure_oxide::find_conjure::conjure_executable;
1824
use conjure_oxide::rule_engine::{resolve_rule_sets, rewrite_model};
19-
use conjure_oxide::{get_rules, model_from_json};
20-
2125
use conjure_oxide::utils::conjure::{get_minion_solutions, minion_solutions_to_json};
2226
use conjure_oxide::SolverFamily;
23-
use git_version::git_version;
24-
use tracing_subscriber::filter::LevelFilter;
25-
use tracing_subscriber::layer::SubscriberExt as _;
26-
use tracing_subscriber::util::SubscriberInitExt as _;
27-
use tracing_subscriber::{EnvFilter, Layer};
27+
use conjure_oxide::{get_rules, model_from_json};
2828

2929
static AFTER_HELP_TEXT: &str = include_str!("help_text.txt");
3030

@@ -129,8 +129,10 @@ pub fn main() -> AnyhowResult<()> {
129129
}
130130

131131
let target_family = cli.solver.unwrap_or(SolverFamily::Minion);
132-
let mut extra_rule_sets: Vec<String> = get_default_rule_sets();
133-
extra_rule_sets.extend(cli.extra_rule_sets.clone());
132+
let mut extra_rule_sets: Vec<&str> = DEFAULT_RULE_SETS.to_vec();
133+
for rs in &cli.extra_rule_sets {
134+
extra_rule_sets.push(rs.as_str());
135+
}
134136

135137
// Logging:
136138
//
@@ -264,7 +266,7 @@ pub fn main() -> AnyhowResult<()> {
264266

265267
let context = Context::new_ptr(
266268
target_family,
267-
extra_rule_sets.clone(),
269+
extra_rule_sets.iter().map(|rs| rs.to_string()).collect(),
268270
rules,
269271
rule_sets.clone(),
270272
);

conjure_oxide/src/utils/essence_parser.rs

+40-14
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub fn parse_essence_file_native(
4141
let mut constraint_vec: Vec<Expression> = Vec::new();
4242
for constraint in named_children(&statement) {
4343
if constraint.kind() != "single_line_comment" {
44-
constraint_vec.push(parse_constraint(constraint, &source_code));
44+
constraint_vec.push(parse_constraint(constraint, &source_code, &statement));
4545
}
4646
}
4747
model.as_submodel_mut().add_constraints(constraint_vec);
@@ -51,6 +51,19 @@ pub fn parse_essence_file_native(
5151
let letting_vars = parse_letting_statement(statement, &source_code);
5252
model.as_submodel_mut().symbols_mut().extend(letting_vars);
5353
}
54+
"dominance_relation" => {
55+
let inner = statement
56+
.child(1)
57+
.expect("Expected a sub-expression inside `dominanceRelation`");
58+
let expr = parse_constraint(inner, &source_code, &statement);
59+
let dominance = Expression::DominanceRelation(Metadata::new(), Box::new(expr));
60+
if model.dominance.is_some() {
61+
return Err(EssenceParseError::ParseError(Error::Parse(
62+
"Duplicate dominance relation".to_owned(),
63+
)));
64+
}
65+
model.dominance = Some(dominance);
66+
}
5467
_ => {
5568
let kind = statement.kind();
5669
return Err(EssenceParseError::ParseError(Error::Parse(
@@ -63,8 +76,9 @@ pub fn parse_essence_file_native(
6376
}
6477

6578
fn get_tree(path: &str, filename: &str, extension: &str) -> (Tree, String) {
66-
let source_code = fs::read_to_string(format!("{path}/{filename}.{extension}"))
67-
.expect("Failed to read the source code file");
79+
let pth = format!("{path}/{filename}.{extension}");
80+
let source_code = fs::read_to_string(&pth)
81+
.unwrap_or_else(|_| panic!("Failed to read the source code file {}", pth));
6882
let mut parser = Parser::new();
6983
parser.set_language(&LANGUAGE.into()).unwrap();
7084
(
@@ -201,7 +215,7 @@ fn parse_letting_statement(letting_statement_list: Node, source_code: &str) -> S
201215
for name in temp_symbols {
202216
symbol_table.insert(Rc::new(Declaration::new_value_letting(
203217
Name::UserName(String::from(name)),
204-
parse_constraint(expr_or_domain, source_code),
218+
parse_constraint(expr_or_domain, source_code, &letting_statement_list),
205219
)));
206220
}
207221
}
@@ -219,24 +233,24 @@ fn parse_letting_statement(letting_statement_list: Node, source_code: &str) -> S
219233
symbol_table
220234
}
221235

222-
fn parse_constraint(constraint: Node, source_code: &str) -> Expression {
236+
fn parse_constraint(constraint: Node, source_code: &str, root: &Node) -> Expression {
223237
match constraint.kind() {
224-
"constraint" | "expression" => child_expr(constraint, source_code),
238+
"constraint" | "expression" => child_expr(constraint, source_code, root),
225239
"not_expr" => Expression::Not(
226240
Metadata::new(),
227-
Box::new(child_expr(constraint, source_code)),
241+
Box::new(child_expr(constraint, source_code, root)),
228242
),
229243
"abs_value" => Expression::Abs(
230244
Metadata::new(),
231-
Box::new(child_expr(constraint, source_code)),
245+
Box::new(child_expr(constraint, source_code, root)),
232246
),
233247
"negative_expr" => Expression::Neg(
234248
Metadata::new(),
235-
Box::new(child_expr(constraint, source_code)),
249+
Box::new(child_expr(constraint, source_code, root)),
236250
),
237251
"exponent" | "product_expr" | "sum_expr" | "comparison" | "and_expr" | "or_expr"
238252
| "implication" => {
239-
let expr1 = child_expr(constraint, source_code);
253+
let expr1 = child_expr(constraint, source_code, root);
240254
let op = constraint.child(1).unwrap_or_else(|| {
241255
panic!(
242256
"Error: missing node in expression of kind {}",
@@ -250,7 +264,7 @@ fn parse_constraint(constraint: Node, source_code: &str) -> Expression {
250264
constraint.kind()
251265
)
252266
});
253-
let expr2 = parse_constraint(expr2_node, source_code);
267+
let expr2 = parse_constraint(expr2_node, source_code, root);
254268

255269
match op_type {
256270
"**" => Expression::UnsafePow(Metadata::new(), Box::new(expr1), Box::new(expr2)),
@@ -280,7 +294,7 @@ fn parse_constraint(constraint: Node, source_code: &str) -> Expression {
280294
"quantifier_expr" => {
281295
let mut expr_list = Vec::new();
282296
for expr in named_children(&constraint) {
283-
expr_list.push(parse_constraint(expr, source_code));
297+
expr_list.push(parse_constraint(expr, source_code, root));
284298
}
285299

286300
let quantifier = constraint.child(0).unwrap_or_else(|| {
@@ -331,6 +345,18 @@ fn parse_constraint(constraint: Node, source_code: &str) -> Expression {
331345
Atom::Reference(Name::UserName(variable_name)),
332346
)
333347
}
348+
"from_solution" => match root.kind() {
349+
"dominance_relation" => {
350+
let inner = child_expr(constraint, source_code, root);
351+
match inner {
352+
Expression::Atomic(_, _) => {
353+
Expression::FromSolution(Metadata::new(), Box::new(inner))
354+
}
355+
_ => panic!("Expression inside a `fromSolution()` must be a variable name"),
356+
}
357+
}
358+
_ => panic!("`fromSolution()` is only allowed inside dominance relation definitions"),
359+
},
334360
_ => panic!("{} is not a recognized node kind", constraint.kind()),
335361
}
336362
}
@@ -339,9 +365,9 @@ fn named_children<'a>(node: &'a Node<'a>) -> impl Iterator<Item = Node<'a>> + 'a
339365
(0..node.named_child_count()).filter_map(|i| node.named_child(i))
340366
}
341367

342-
fn child_expr(node: Node, source_code: &str) -> Expression {
368+
fn child_expr(node: Node, source_code: &str, root: &Node) -> Expression {
343369
let child = node
344370
.named_child(0)
345371
.unwrap_or_else(|| panic!("Error: missing node in expression of kind {}", node.kind()));
346-
parse_constraint(child, source_code)
372+
parse_constraint(child, source_code, root)
347373
}

conjure_oxide/tests/generated_tests.rs

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
use conjure_core::rule_engine::rewrite_model;
44
use conjure_core::rule_engine::rewrite_naive;
5+
use conjure_oxide::defaults::DEFAULT_RULE_SETS;
56
use conjure_oxide::utils::essence_parser::parse_essence_file_native;
6-
use conjure_oxide::utils::testing::{read_human_rule_trace, read_rule_trace};
7+
use conjure_oxide::utils::testing::read_human_rule_trace;
78
use glob::glob;
89
use itertools::Itertools;
910
use std::collections::BTreeMap;
1011
use std::env;
1112
use std::error::Error;
1213
use std::fs;
1314
use std::fs::File;
14-
use std::str::FromStr;
1515
use tracing::{span, Level, Metadata as OtherMetadata};
1616
use tracing_subscriber::{
1717
filter::EnvFilter, filter::FilterFn, fmt, layer::SubscriberExt, Layer, Registry,
@@ -27,7 +27,6 @@ use std::sync::RwLock;
2727
use conjure_core::ast::Atom;
2828
use conjure_core::ast::{Expression, Literal, Name};
2929
use conjure_core::context::Context;
30-
use conjure_oxide::defaults::get_default_rule_sets;
3130
use conjure_oxide::rule_engine::resolve_rule_sets;
3231
use conjure_oxide::utils::conjure::minion_solutions_to_json;
3332
use conjure_oxide::utils::conjure::{
@@ -258,7 +257,7 @@ fn integration_test_inner(
258257

259258
// Stage 2a: Rewrite the model using the rule engine (run unless explicitly disabled)
260259
let rewritten_model = if config.apply_rewrite_rules {
261-
let rule_sets = resolve_rule_sets(SolverFamily::Minion, &get_default_rule_sets())?;
260+
let rule_sets = resolve_rule_sets(SolverFamily::Minion, DEFAULT_RULE_SETS)?;
262261

263262
let rewritten = if config.enable_native_impl {
264263
rewrite_model(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
find cost: int(1..10)
2+
find carbon: int(1..10)
3+
4+
such that (cost + carbon) <= 15
5+
6+
dominanceRelation (
7+
(cost <= fromSolution(cost)) /\
8+
(carbon <= fromSolution(carbon)) /\
9+
(
10+
(cost < fromSolution(cost)) \/
11+
(carbon < fromSolution(carbon))
12+
)
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
find cost: int(1..10)
2+
find carbon: int(1..10)
3+
4+
such that (cost + carbon) <= 15
5+
6+
dominanceRelation (
7+
(cost <= fromSolution(cost)) /\
8+
(carbon <= fromSolution(carbon)) /\
9+
(
10+
(cost < fromSolution(cost)) \/
11+
(carbon < fromSolution(carbon))
12+
)
13+
)
14+
15+
dominanceRelation (
16+
(cost <= fromSolution(cost))
17+
)

0 commit comments

Comments
 (0)