Skip to content

Commit eafcdbc

Browse files
committed
feat: Constant Folding/Partial Evaluation
A special mode of evaluation is introduced in which - access to input is disallowed - only those builtin functions that are idempotent are allowed - extensions are disallowed Those rules which evaluate to value other than undefined will procduce the same value irrespective of the input. Values of such rules are cached so that in regular evaluation these rules don't have to be evaluated. TODO: - Expressions evaluating to values other than undefined can also be cached. But this might consume memory. - Even if one expression or statement in a query evaluated to undefined, evaluation can still proceed to evaluate other expressions so that they can be cached if possible. - Expensive to create objects such as Regexes can also be cached. This will require caching at a lower level than expressions. Impact on memory must also be considered. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1 parent 60ac4a7 commit eafcdbc

2 files changed

Lines changed: 143 additions & 13 deletions

File tree

src/engine.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,7 @@ impl Engine {
615615
.set_functions(gather_functions(&self.modules)?);
616616
self.interpreter.gather_rules()?;
617617
self.interpreter.process_imports()?;
618+
self.interpreter.constant_fold()?;
618619
self.prepared = true;
619620
}
620621

src/interpreter.rs

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ enum FunctionModifier {
3636
Value(Value),
3737
}
3838

39+
#[derive(Debug, Clone)]
40+
struct Optimized {
41+
data: Value,
42+
processed: BTreeSet<Ref<Rule>>,
43+
processed_paths: Value,
44+
}
45+
3946
#[derive(Debug, Clone)]
4047
pub struct Interpreter {
4148
modules: Vec<Ref<Module>>,
@@ -73,6 +80,9 @@ pub struct Interpreter {
7380
gather_prints: bool,
7481
prints: Vec<String>,
7582
rule_paths: Set<String>,
83+
is_constant_folding: bool,
84+
optimized: Option<Optimized>,
85+
has_side_effects: bool,
7686
}
7787

7888
impl Default for Interpreter {
@@ -197,6 +207,9 @@ impl Interpreter {
197207
gather_prints: false,
198208
prints: Vec::default(),
199209
rule_paths: Set::new(),
210+
is_constant_folding: false,
211+
has_side_effects: false,
212+
optimized: None,
200213
}
201214
}
202215

@@ -255,9 +268,16 @@ impl Interpreter {
255268
}
256269

257270
pub fn clean_internal_evaluation_state(&mut self) {
258-
self.data = self.init_data.clone();
259-
self.processed.clear();
260-
self.processed_paths = Value::new_object();
271+
if let Some(optimized) = &self.optimized {
272+
self.data = optimized.data.clone();
273+
// TODO: Check use of processed and processed_paths
274+
self.processed = optimized.processed.clone();
275+
self.processed_paths = optimized.processed_paths.clone();
276+
} else {
277+
self.data = self.init_data.clone();
278+
self.processed.clear();
279+
self.processed_paths = Value::new_object();
280+
}
261281
self.loop_var_values.clear();
262282
self.scopes = vec![Scope::new()];
263283
self.contexts = vec![];
@@ -1281,6 +1301,7 @@ impl Interpreter {
12811301
// Mark modified rules as processed.
12821302
if let Some(rules) = self.rules.get(&target) {
12831303
for r in rules {
1304+
// TODO: check if this is correct for constant folding
12841305
self.processed.insert(r.clone());
12851306
}
12861307
}
@@ -1507,6 +1528,15 @@ impl Interpreter {
15071528

15081529
self.scopes.pop();
15091530

1531+
// When constant folding, evaluate one iteration of the loop so as to
1532+
// fold constants within the loop body. But return false to prevent
1533+
// the loop itself from ptentially being treated as a constant.
1534+
//if self.is_constant_folding {
1535+
// return Ok(false);
1536+
//}
1537+
// Ignore previous comment.
1538+
// TODO: Implement expression value caching for constant values.
1539+
15101540
// Return true if at least on iteration returned true
15111541
Ok(result)
15121542
}
@@ -2055,6 +2085,8 @@ impl Interpreter {
20552085
}
20562086

20572087
fn eval_array_compr(&mut self, term: &ExprRef, query: &Ref<Query>) -> Result<Value> {
2088+
let hse = self.has_side_effects;
2089+
self.has_side_effects = false;
20582090
// Push new context
20592091
self.contexts.push(Context {
20602092
output_expr: Some(term.clone()),
@@ -2066,13 +2098,21 @@ impl Interpreter {
20662098
// Evaluate body first.
20672099
self.eval_query(query)?;
20682100

2101+
self.has_side_effects = hse || self.has_side_effects;
2102+
if self.is_constant_folding && self.has_side_effects {
2103+
return Ok(Value::Undefined);
2104+
}
2105+
20692106
match self.contexts.pop() {
20702107
Some(ctx) => Ok(ctx.value),
20712108
None => bail!("internal error: context already popped"),
20722109
}
20732110
}
20742111

20752112
fn eval_set_compr(&mut self, term: &ExprRef, query: &Ref<Query>) -> Result<Value> {
2113+
let hse = self.has_side_effects;
2114+
self.has_side_effects = false;
2115+
20762116
// Push new context
20772117
self.contexts.push(Context {
20782118
output_expr: Some(term.clone()),
@@ -2083,6 +2123,11 @@ impl Interpreter {
20832123

20842124
self.eval_query(query)?;
20852125

2126+
self.has_side_effects = hse || self.has_side_effects;
2127+
if self.is_constant_folding && self.has_side_effects {
2128+
return Ok(Value::Undefined);
2129+
}
2130+
20862131
match self.contexts.pop() {
20872132
Some(ctx) => Ok(ctx.value),
20882133
None => bail!("internal error: context already popped"),
@@ -2095,6 +2140,9 @@ impl Interpreter {
20952140
value: &ExprRef,
20962141
query: &Ref<Query>,
20972142
) -> Result<Value> {
2143+
let hse = self.has_side_effects;
2144+
self.has_side_effects = false;
2145+
20982146
// Push new context
20992147
self.contexts.push(Context {
21002148
key_expr: Some(key.clone()),
@@ -2106,6 +2154,11 @@ impl Interpreter {
21062154

21072155
self.eval_query(query)?;
21082156

2157+
self.has_side_effects = hse || self.has_side_effects;
2158+
if self.is_constant_folding && self.has_side_effects {
2159+
return Ok(Value::Undefined);
2160+
}
2161+
21092162
match self.contexts.pop() {
21102163
Some(ctx) => Ok(ctx.value),
21112164
None => bail!("internal error: context already popped"),
@@ -2137,6 +2190,12 @@ impl Interpreter {
21372190
return Ok(Value::Undefined);
21382191
}
21392192

2193+
// TODO: Allow builtins that can be constant folded
2194+
if self.is_constant_folding {
2195+
self.has_side_effects = true;
2196+
return Ok(Value::Undefined);
2197+
}
2198+
21402199
let cache = builtins::must_cache(name);
21412200
if let Some(name) = &cache {
21422201
if let Some(v) = self.builtins_cache.get(&(name, args.clone())) {
@@ -2316,6 +2375,10 @@ impl Interpreter {
23162375
extension = Some(ext);
23172376
(&empty, None)
23182377
} else if fcn_path == "print" {
2378+
if self.is_constant_folding {
2379+
// Ignore side-effects in constant folding.
2380+
return Ok(Value::Undefined);
2381+
}
23192382
return self.eval_print(span, params, param_values);
23202383
}
23212384
// Look up builtin function.
@@ -2344,6 +2407,11 @@ impl Interpreter {
23442407
}
23452408

23462409
if let Some((nargs, ext)) = extension {
2410+
if self.is_constant_folding {
2411+
// Extensions are not supported in constant folding.
2412+
return Ok(Value::Undefined);
2413+
}
2414+
23472415
if param_values.len() != *nargs as usize {
23482416
bail!(span.error("incorrect number of parameters supplied to extension"));
23492417
}
@@ -2621,14 +2689,20 @@ impl Interpreter {
26212689
}
26222690

26232691
// Evaluate the associated default rules after non-default rules
2624-
if let Some(rules) = self.default_rules.get(&path) {
2625-
matched = true;
2626-
for (r, _) in rules.clone() {
2627-
if !self.processed.contains(&r) {
2628-
let module = self.get_rule_module(&r)?;
2629-
let prev_module = self.set_current_module(Some(module))?;
2630-
self.eval_default_rule(&r)?;
2631-
self.set_current_module(prev_module)?;
2692+
if self.is_constant_folding {
2693+
// We don't want to evaluate default rules at this point.
2694+
// A non default rule with the same path could have failed due to it needing input.
2695+
// TODO: Cleanup rule evaluation bookmarking.
2696+
} else {
2697+
if let Some(rules) = self.default_rules.get(&path) {
2698+
matched = true;
2699+
for (r, _) in rules.clone() {
2700+
if !self.processed.contains(&r) {
2701+
let module = self.get_rule_module(&r)?;
2702+
let prev_module = self.set_current_module(Some(module))?;
2703+
self.eval_default_rule(&r)?;
2704+
self.set_current_module(prev_module)?;
2705+
}
26322706
}
26332707
}
26342708
}
@@ -2660,6 +2734,12 @@ impl Interpreter {
26602734

26612735
fn mark_processed(&mut self, path: &[&str]) -> Result<()> {
26622736
let obj = self.processed_paths.make_or_get_value_mut(path)?;
2737+
2738+
if self.is_constant_folding && obj == &Value::Undefined {
2739+
// If constant folding, then do not register undefined values.
2740+
return Ok(());
2741+
}
2742+
26632743
if obj == &Value::Undefined {
26642744
*obj = Value::new_object();
26652745
}
@@ -2677,6 +2757,11 @@ impl Interpreter {
26772757

26782758
// Handle input.
26792759
if name.text() == "input" {
2760+
if self.is_constant_folding {
2761+
// When constant folding, expressions cannot depend on input.
2762+
self.has_side_effects = true;
2763+
return Ok(Value::Undefined);
2764+
}
26802765
return Ok(Self::get_value_chained(self.input.clone(), fields));
26812766
}
26822767

@@ -3297,7 +3382,16 @@ impl Interpreter {
32973382
if let Ok(mut comps) = self.eval_rule_ref(refr) {
32983383
let mut full_path = package_components;
32993384
full_path.append(&mut comps);
3300-
self.update_rule_value(span, full_path, Value::new_set(), true)?;
3385+
if value == Value::Undefined && self.is_constant_folding {
3386+
// Do not create empty set if constant folding and rule failed to evaluate.
3387+
} else {
3388+
self.update_rule_value(
3389+
span,
3390+
full_path,
3391+
Value::new_set(),
3392+
true,
3393+
)?;
3394+
}
33013395
}
33023396
} else if is_object {
33033397
// Fetch the rule, ignoring the key.
@@ -3314,7 +3408,15 @@ impl Interpreter {
33143408
}
33153409
}
33163410
}
3317-
self.processed.insert(rule.clone());
3411+
3412+
if self.is_constant_folding {
3413+
if value != Value::Undefined {
3414+
// When constant folding, record only those rules that have successfully evaluated.
3415+
self.processed.insert(rule.clone());
3416+
}
3417+
} else {
3418+
self.processed.insert(rule.clone());
3419+
}
33183420
}
33193421
RuleHead::Func {
33203422
refr, args, assign, ..
@@ -3392,7 +3494,10 @@ impl Interpreter {
33923494
let scopes = core::mem::take(&mut self.scopes);
33933495
let prev_module = self.set_current_module(Some(module.clone()))?;
33943496

3497+
let hse = self.has_side_effects;
3498+
self.has_side_effects = false;
33953499
let res = self.eval_rule_impl(module, rule);
3500+
self.has_side_effects = hse;
33963501

33973502
self.set_current_module(prev_module)?;
33983503
self.scopes = scopes;
@@ -3485,6 +3590,30 @@ impl Interpreter {
34853590
}
34863591
}
34873592

3593+
pub fn constant_fold(&mut self) -> Result<()> {
3594+
self.is_constant_folding = true;
3595+
self.optimized = None;
3596+
self.clean_internal_evaluation_state();
3597+
// TODO: Maybe use scheduler information to determine which rules to evaluate and in which order.
3598+
for module in &self.modules.clone() {
3599+
for rule in &module.policy {
3600+
if let Rule::Spec { head, .. } = rule.as_ref() {
3601+
if matches!(&head, RuleHead::Compr { .. } | RuleHead::Set { .. }) {
3602+
// Evaluate rule and ignore any errors.
3603+
let _ = self.eval_rule(module, rule);
3604+
}
3605+
}
3606+
}
3607+
}
3608+
self.is_constant_folding = false;
3609+
self.optimized = Some(Optimized {
3610+
data: self.data.clone(),
3611+
processed: self.processed.clone(),
3612+
processed_paths: self.processed_paths.clone(),
3613+
});
3614+
Ok(())
3615+
}
3616+
34883617
fn get_rule_path_components(mut refr: &Ref<Expr>) -> Result<Vec<Rc<str>>> {
34893618
let mut components: Vec<Rc<str>> = vec![];
34903619
loop {

0 commit comments

Comments
 (0)