Skip to content

Commit ad7290d

Browse files
feat: add Expr::Scope (#683)
Add `Scope` expression variant, allowing submodels to be defined inside a model. Change the rewriter so that it rewrites expressions inside their submodels, using that submodels symbol table. This allows rules to apply to variables defined in a local scope. This commit is part of on-going work to add lexical scope to Conjure Oxide, following on from 542c89e (feat!: add `Submodel`, 2025-02-16).
1 parent 542c89e commit ad7290d

12 files changed

+237
-33
lines changed

crates/conjure_core/src/ast/expressions.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ use crate::ast::Name;
1616
use crate::ast::ReturnType;
1717
use crate::metadata::Metadata;
1818

19-
use super::{Domain, Range, SubModel};
19+
use super::{Domain, Range, SubModel, Typeable};
2020

2121
/// Represents different types of expressions used to define rules and constraints in the model.
2222
///
2323
/// The `Expression` enum includes operations, constants, and variable references
2424
/// used to build rules and conditions for the model.
2525
#[document_compatibility]
2626
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Uniplate)]
27-
#[uniplate(walk_into=[Atom,Submodel])]
27+
#[uniplate(walk_into=[Atom,SubModel])]
2828
#[biplate(to=Literal)]
2929
#[biplate(to=Metadata)]
3030
#[biplate(to=Atom)]
@@ -41,6 +41,8 @@ pub enum Expression {
4141

4242
Atomic(Metadata, Atom),
4343

44+
Scope(Metadata, Box<SubModel>),
45+
4446
/// `|x|` - absolute value of `x`
4547
#[compatible(JsonInput)]
4648
Abs(Metadata, Box<Expression>),
@@ -334,6 +336,7 @@ impl Expression {
334336
Some(Domain::IntDomain(vec![Range::Single(*n)]))
335337
}
336338
Expression::Atomic(_, Atom::Literal(Literal::Bool(_))) => Some(Domain::BoolDomain),
339+
Expression::Scope(_, _) => Some(Domain::BoolDomain),
337340
Expression::Sum(_, exprs) => expr_vec_to_domain_i32(exprs, |x, y| Some(x + y), syms),
338341
Expression::Product(_, exprs) => {
339342
expr_vec_to_domain_i32(exprs, |x, y| Some(x * y), syms)
@@ -512,6 +515,7 @@ impl Expression {
512515
Expression::Atomic(_, Atom::Literal(Literal::Int(_))) => Some(ReturnType::Int),
513516
Expression::Atomic(_, Atom::Literal(Literal::Bool(_))) => Some(ReturnType::Bool),
514517
Expression::Atomic(_, Atom::Reference(_)) => None,
518+
Expression::Scope(_, scope) => scope.return_type(),
515519
Expression::Abs(_, _) => Some(ReturnType::Int),
516520
Expression::Sum(_, _) => Some(ReturnType::Int),
517521
Expression::Product(_, _) => Some(ReturnType::Int),
@@ -619,6 +623,7 @@ impl Display for Expression {
619623
write!(f, "{}", pretty_expressions_as_top_level(exprs))
620624
}
621625
Expression::Atomic(_, atom) => atom.fmt(f),
626+
Expression::Scope(_, submodel) => write!(f, "{{\n{submodel}\n}}"),
622627
Expression::Abs(_, a) => write!(f, "|{}|", a),
623628
Expression::Sum(_, expressions) => {
624629
write!(f, "Sum({})", pretty_vec(expressions))

crates/conjure_core/src/ast/model.rs

+57-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use std::cell::RefCell;
2+
use std::collections::HashMap;
13
use std::fmt::{Debug, Display};
4+
use std::rc::Rc;
25
use std::sync::{Arc, RwLock};
36

47
use derivative::Derivative;
@@ -8,9 +11,10 @@ use uniplate::{Biplate, Tree, Uniplate};
811
use crate::ast::Expression;
912
use crate::context::Context;
1013

14+
use super::serde::{HasId, ObjId};
1115
use super::types::Typeable;
12-
use super::ReturnType;
1316
use super::SubModel;
17+
use super::{ReturnType, SymbolTable};
1418

1519
/// An Essence model.
1620
///
@@ -88,6 +92,25 @@ impl Biplate<Expression> for Model {
8892
}
8993
}
9094

95+
impl Biplate<SubModel> for Model {
96+
fn biplate(&self) -> (Tree<SubModel>, Box<dyn Fn(Tree<SubModel>) -> Self>) {
97+
let submodel = self.as_submodel().clone();
98+
99+
let self2 = self.clone();
100+
let ctx = Box::new(move |x| {
101+
let Tree::One(submodel) = x else {
102+
panic!();
103+
};
104+
105+
let mut self3 = self2.clone();
106+
self3.replace_submodel(submodel);
107+
self3
108+
});
109+
110+
(Tree::One(submodel), ctx)
111+
}
112+
}
113+
91114
impl Display for Model {
92115
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93116
std::fmt::Display::fmt(self.as_submodel(), f)
@@ -108,10 +131,40 @@ pub struct SerdeModel {
108131
impl SerdeModel {
109132
/// Initialises the model for rewriting.
110133
pub fn initialise(self, context: Arc<RwLock<Context<'static>>>) -> Option<Model> {
111-
// TODO: Once we have submodels and multiple symbol tables, de-duplicate deserialized
112-
// Rc<RefCell<>> symbol tables and declarations using their stored ids.
134+
// The definitive versions of each symbol table are stored in the submodels. Parent
135+
// pointers store dummy values with the correct ids, but nothing else. We need to replace
136+
// these dummy values with pointers to the actual parent symbol tables, using the ids to
137+
// know which tables should be equal.
113138
//
114-
// See ast::serde::RcRefCellAsId.
139+
// See super::serde::RcRefCellToInner, super::serde::RcRefCellToId.
140+
141+
// Store the definitive versions of all symbol tables by id.
142+
let mut tables: HashMap<ObjId, Rc<RefCell<SymbolTable>>> = HashMap::new();
143+
144+
// Find the definitive versions by traversing the sub-models.
145+
for submodel in self.submodel.universe() {
146+
let id = submodel.symbols().id();
147+
148+
// ids should be unique!
149+
assert_eq!(
150+
tables.insert(id, submodel.symbols_ptr_unchecked().clone()),
151+
None
152+
);
153+
}
154+
155+
// Restore parent pointers using `tables`.
156+
for table in tables.clone().into_values() {
157+
let mut table_mut = table.borrow_mut();
158+
let parent_mut = table_mut.parent_mut_unchecked();
159+
160+
#[allow(clippy::unwrap_used)]
161+
if let Some(parent) = parent_mut {
162+
let parent_id = parent.borrow().id();
163+
164+
*parent = tables.get(&parent_id).unwrap().clone();
165+
}
166+
}
167+
115168
Some(Model {
116169
submodel: self.submodel,
117170
context,

crates/conjure_core/src/ast/submodel.rs

+14
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,17 @@ impl Biplate<Expression> for SubModel {
271271
(tree, ctx)
272272
}
273273
}
274+
275+
impl Biplate<SubModel> for SubModel {
276+
fn biplate(&self) -> (Tree<SubModel>, Box<dyn Fn(Tree<SubModel>) -> Self>) {
277+
(
278+
Tree::One(self.clone()),
279+
Box::new(move |x| {
280+
let Tree::One(x) = x else {
281+
panic!();
282+
};
283+
x
284+
}),
285+
)
286+
}
287+
}

crates/conjure_core/src/ast/symbol_table.rs

+7
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ impl SymbolTable {
202202
*(self.next_machine_name.borrow_mut()) += 1;
203203
Name::MachineName(num) // incremented when inserted
204204
}
205+
206+
/// Gets the parent of this symbol table as a mutable reference.
207+
///
208+
/// This function provides no sanity checks.
209+
pub fn parent_mut_unchecked(&mut self) -> &mut Option<Rc<RefCell<SymbolTable>>> {
210+
&mut self.parent
211+
}
205212
}
206213

207214
impl IntoIterator for SymbolTable {

crates/conjure_core/src/rule_engine/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub use rewrite_naive::rewrite_naive;
6969
pub use rewriter_common::RewriteError;
7070
pub use rule::{ApplicationError, ApplicationResult, Reduction, Rule};
7171
pub use rule_set::RuleSet;
72+
mod submodel_zipper;
7273

7374
use crate::solver::SolverFamily;
7475

crates/conjure_core/src/rule_engine/rewrite.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub fn rewrite_model<'a>(
129129
) {
130130
debug_assert!(is_vec_bool(&step.new_top)); // All new_top expressions should be boolean
131131
new_model.as_submodel_mut().constraints_mut()[i] = step.new_expression.clone();
132-
step.apply(&mut new_model); // Apply side-effects (e.g., symbol table updates)
132+
step.apply(new_model.as_submodel_mut()); // Apply side-effects (e.g., symbol table updates)
133133
}
134134

135135
// If new constraints are added, continue processing them in the next iterations.
@@ -220,7 +220,7 @@ fn rewrite_iteration(
220220
let rule_results = apply_all_rules(&expression, model, rules, stats);
221221
if let Some(result) = choose_rewrite(&rule_results, &expression) {
222222
// If a rule is applied, mark the expression as dirty
223-
log_rule_application(&result, &expression, model);
223+
log_rule_application(&result, &expression, model.as_submodel());
224224
return Some(result.reduction);
225225
}
226226

crates/conjure_core/src/rule_engine/rewrite_naive.rs

+33-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use super::{resolve_rules::RuleData, RewriteError, RuleSet};
22
use crate::{
3-
ast::Expression as Expr,
3+
ast::{Expression as Expr, SubModel},
44
bug,
55
rule_engine::{
66
get_rules_grouped,
77
rewriter_common::{log_rule_application, RuleResult},
8+
submodel_zipper::submodel_ctx,
89
},
910
Model,
1011
};
@@ -26,11 +27,31 @@ pub fn rewrite_naive<'a>(
2627
.collect_vec();
2728

2829
let mut model = model.clone();
30+
let mut done_something = true;
2931

3032
// Rewrite until there are no more rules left to apply.
31-
while let Some(()) =
32-
try_rewrite_model(&mut model, &rules_grouped, prop_multiple_equally_applicable)
33-
{}
33+
while done_something {
34+
let mut new_model = None;
35+
done_something = false;
36+
37+
// Rewrite each sub-model in the tree, largest first.
38+
for (mut submodel, ctx) in <_ as Biplate<SubModel>>::contexts_bi(&model) {
39+
if try_rewrite_model(
40+
&mut submodel,
41+
&rules_grouped,
42+
prop_multiple_equally_applicable,
43+
)
44+
.is_some()
45+
{
46+
new_model = Some(ctx(submodel));
47+
done_something = true;
48+
break;
49+
}
50+
}
51+
if let Some(new_model) = new_model {
52+
model = new_model;
53+
}
54+
}
3455

3556
Ok(model)
3657
}
@@ -39,23 +60,23 @@ pub fn rewrite_naive<'a>(
3960
//
4061
// Returns None if no change was made.
4162
fn try_rewrite_model(
42-
model: &mut Model,
63+
submodel: &mut SubModel,
4364
rules_grouped: &Vec<(u16, Vec<RuleData<'_>>)>,
4465
prop_multiple_equally_applicable: bool,
4566
) -> Option<()> {
46-
type CtxFn = Arc<dyn Fn(Expr) -> Model>;
67+
type CtxFn = Arc<dyn Fn(Expr) -> SubModel>;
4768
let mut results: Vec<(RuleResult<'_>, u16, Expr, CtxFn)> = vec![];
4869

4970
// Iterate over rules by priority in descending order.
5071
'top: for (priority, rules) in rules_grouped.iter() {
5172
// Using Biplate, rewrite both the expression tree, and any value lettings in the symbol
5273
// table.
53-
for (expr, ctx) in <_ as Biplate<Expr>>::contexts_bi(model) {
74+
for (expr, ctx) in submodel_ctx(submodel.clone()) {
5475
// Clone expr and ctx so they can be reused
5576
let expr = expr.clone();
5677
let ctx = ctx.clone();
5778
for rd in rules {
58-
match (rd.rule.application)(&expr, &model.as_submodel().symbols()) {
79+
match (rd.rule.application)(&expr, &submodel.symbols()) {
5980
Ok(red) => {
6081
// Collect applicable rules
6182
results.push((
@@ -99,15 +120,15 @@ fn try_rewrite_model(
99120
}
100121

101122
// Extract the single applicable rule and apply it
102-
log_rule_application(result, expr, model);
123+
log_rule_application(result, expr, submodel);
103124

104125
// Replace expr with new_expression
105-
*model = ctx(result.reduction.new_expression.clone());
126+
*submodel = ctx(result.reduction.new_expression.clone());
106127

107128
// Apply new symbols and top level
108-
result.reduction.clone().apply(model);
129+
result.reduction.clone().apply(submodel);
109130

110-
println!("{}", &model);
131+
println!("{}", &submodel);
111132
}
112133
}
113134

crates/conjure_core/src/rule_engine/rewriter_common.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ use super::{
33
resolve_rules::{ResolveRulesError, RuleData},
44
Reduction,
55
};
6-
use crate::{
7-
ast::{
8-
pretty::{pretty_variable_declaration, pretty_vec},
9-
Expression,
10-
},
11-
Model,
6+
use crate::ast::{
7+
pretty::{pretty_variable_declaration, pretty_vec},
8+
Expression, SubModel,
129
};
1310

1411
use itertools::Itertools;
@@ -28,7 +25,7 @@ pub struct RuleResult<'a> {
2825
pub fn log_rule_application(
2926
result: &RuleResult,
3027
initial_expression: &Expression,
31-
initial_model: &Model,
28+
initial_model: &SubModel,
3229
) {
3330
let red = &result.reduction;
3431
let rule = result.rule_data.rule;
@@ -63,7 +60,7 @@ pub fn log_rule_application(
6360
let new_variables_str = {
6461
let mut vars: Vec<String> = vec![];
6562

66-
for var_name in red.added_symbols(&initial_model.as_submodel().symbols()) {
63+
for var_name in red.added_symbols(&initial_model.symbols()) {
6764
#[allow(clippy::unwrap_used)]
6865
vars.push(format!(
6966
" {}",

crates/conjure_core/src/rule_engine/rule.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::rc::Rc;
66
use thiserror::Error;
77

88
use crate::ast::Declaration;
9-
use crate::ast::{Expression, Model, Name, SymbolTable};
9+
use crate::ast::{Expression, Name, SubModel, SymbolTable};
1010

1111
#[derive(Debug, Error)]
1212
pub enum ApplicationError {
@@ -100,10 +100,9 @@ impl Reduction {
100100
}
101101

102102
/// Applies side-effects (e.g. symbol table updates)
103-
pub fn apply(self, model: &mut Model) {
104-
let submodel = model.as_submodel_mut();
105-
submodel.symbols_mut().extend(self.symbols); // Add new assignments to the symbol table
106-
submodel.add_constraints(self.new_top.clone());
103+
pub fn apply(self, model: &mut SubModel) {
104+
model.symbols_mut().extend(self.symbols); // Add new assignments to the symbol table
105+
model.add_constraints(self.new_top.clone());
107106
}
108107

109108
/// Gets symbols added by this reduction

0 commit comments

Comments
 (0)