Skip to content

Commit 5d8387f

Browse files
authored
feat(hoist): pre-compute loop hoisting metadata at compilation time (microsoft#483)
Introduce a compiler pass that analyzes and pre-computes loop hoisting information during policy compilation. This hoisted metadata is stored in lookup tables and made available to downstream consumers: - interpreter: use HoistedLoop entries during evaluation (replaces runtime scanning) - type inference: can leverage pre-computed loop structure for type propagation - RVM compiler: will consume hoisting metadata for optimized bytecode generation Changes: - populate loop hoisting tables during engine preparation and query snippet execution - refactor eval_stmts_in_loop and eval_output_expr_in_loop to consume HoistedLoop directly - add helper methods for accessing loop expressions, collections, and indices from HoistedLoop - extend Lookup with get_checked and into_slots for safe query context access and merging Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1 parent 9604fe8 commit 5d8387f

14 files changed

Lines changed: 1365 additions & 267 deletions

File tree

benches/schema_validation_benchmark.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ fn bench_mixed_type_array(c: &mut Criterion) {
269269
}
270270
});
271271
let schema = Schema::from_serde_json_value(schema_json).unwrap();
272-
let value = Value::from(json!(["hello", 42, true, "world", 3.14, false]));
272+
let value = Value::from(json!(["hello", 42, true, "world", 99.5, false]));
273273

274274
c.bench_function("validate_mixed_type_array", |b| {
275275
b.iter(|| {

src/compiled_policy.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
use crate::ast::*;
5+
use crate::compiler::hoist::HoistedLoopsLookup;
56
use crate::engine::Engine;
67
use crate::scheduler::*;
78
use crate::utils::*;
@@ -190,7 +191,7 @@ pub(crate) struct TargetInfo {
190191
#[derive(Debug, Clone, Default)]
191192
pub(crate) struct CompiledPolicyData {
192193
pub(crate) modules: Rc<Vec<Ref<Module>>>,
193-
pub(crate) schedule: Option<Schedule>,
194+
pub(crate) schedule: Option<Rc<Schedule>>,
194195
pub(crate) rules: Map<String, Vec<Ref<Rule>>>,
195196
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
196197
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
@@ -212,4 +213,7 @@ pub(crate) struct CompiledPolicyData {
212213

213214
// The semantics of extensions ought to be changes to be more Clone friendly.
214215
pub(crate) extensions: Map<String, (u8, Rc<Box<dyn Extension>>)>,
216+
217+
// Pre-computed loop hoisting information
218+
pub(crate) loop_hoisting_table: HoistedLoopsLookup,
215219
}

src/compiler.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Compiler-related functionality for Regorus.
5+
//!
6+
//! This module contains utilities and data structures used during
7+
//! the compilation phase to prepare policies for efficient execution.
8+
9+
pub mod context;
10+
pub mod hoist;

src/compiler/context.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Compilation context types shared across compiler components.
5+
//!
6+
//! This module defines context structures used for tracking scope-level information
7+
//! during compilation and analysis phases. These types are designed to be compatible
8+
//! with both the interpreter's loop hoisting and the RVM compiler.
9+
10+
use crate::ast::ExprRef;
11+
use alloc::collections::BTreeSet;
12+
use alloc::string::{String, ToString};
13+
14+
/// Type of compilation context for tracking different scenarios
15+
#[derive(Debug, Clone, PartialEq, Eq)]
16+
pub enum ContextType {
17+
/// Rule context (Complete, PartialSet, PartialObject, or Function)
18+
Rule,
19+
/// Comprehension context (Array, Set, or Object)
20+
Comprehension,
21+
/// Every quantifier context
22+
Every,
23+
/// Query/statement context (no output expressions)
24+
Query,
25+
}
26+
27+
/// Context for tracking variable bindings and output expressions within a scope.
28+
/// Used during loop hoisting and compilation to determine what needs to be hoisted
29+
/// and what's already bound.
30+
///
31+
/// This design is compatible with RVM's CompilationContext for potential future unification.
32+
#[derive(Debug, Clone)]
33+
pub struct ScopeContext {
34+
/// Type of context (Rule, Comprehension, Every, Query)
35+
pub context_type: ContextType,
36+
37+
/// Variables that are bound in the current scope
38+
pub bound_vars: BTreeSet<String>,
39+
40+
/// Variables that are explicitly marked as unbound (from `some` declarations)
41+
pub unbound_vars: BTreeSet<String>,
42+
43+
/// Key expression from rule head or object comprehension (for output expression hoisting)
44+
pub key_expr: Option<ExprRef>,
45+
46+
/// Value expression from rule assignment or comprehension term (for output expression hoisting)
47+
pub value_expr: Option<ExprRef>,
48+
}
49+
50+
impl ScopeContext {
51+
/// Create a new context with Query type (default, no output expressions)
52+
pub fn new() -> Self {
53+
Self {
54+
context_type: ContextType::Query,
55+
bound_vars: BTreeSet::new(),
56+
unbound_vars: BTreeSet::new(),
57+
key_expr: None,
58+
value_expr: None,
59+
}
60+
}
61+
62+
/// Create a new context with a specific context type
63+
#[allow(dead_code)]
64+
pub fn with_context_type(context_type: ContextType) -> Self {
65+
Self {
66+
context_type,
67+
bound_vars: BTreeSet::new(),
68+
unbound_vars: BTreeSet::new(),
69+
key_expr: None,
70+
value_expr: None,
71+
}
72+
}
73+
74+
/// Create a new context with output expressions (for rules and comprehensions)
75+
#[allow(dead_code)]
76+
pub fn with_output_exprs(
77+
context_type: ContextType,
78+
key_expr: Option<ExprRef>,
79+
value_expr: Option<ExprRef>,
80+
) -> Self {
81+
Self {
82+
context_type,
83+
bound_vars: BTreeSet::new(),
84+
unbound_vars: BTreeSet::new(),
85+
key_expr,
86+
value_expr,
87+
}
88+
}
89+
90+
/// Create a child context that inherits bindings but overrides context type and output expressions
91+
pub fn child_with_output_exprs(
92+
&self,
93+
context_type: ContextType,
94+
key_expr: Option<ExprRef>,
95+
value_expr: Option<ExprRef>,
96+
) -> Self {
97+
Self {
98+
context_type,
99+
bound_vars: self.bound_vars.clone(),
100+
unbound_vars: self.unbound_vars.clone(),
101+
key_expr,
102+
value_expr,
103+
}
104+
}
105+
106+
/// Add a variable to the bound set
107+
pub fn bind_variable(&mut self, var_name: &str) {
108+
if var_name != "_" {
109+
self.bound_vars.insert(var_name.to_string());
110+
self.unbound_vars.remove(var_name);
111+
}
112+
}
113+
114+
/// Mark a variable as unbound
115+
pub fn add_unbound_variable(&mut self, var_name: &str) {
116+
if var_name != "_" && !self.bound_vars.contains(var_name) {
117+
self.unbound_vars.insert(var_name.to_string());
118+
}
119+
}
120+
121+
/// Check if a variable is known to be unbound
122+
pub fn is_unbound(&self, var_name: &str) -> bool {
123+
self.unbound_vars.contains(var_name)
124+
}
125+
126+
/// Check if we can determine that a variable should be treated as a loop iterator
127+
/// (either it's unbound or explicitly marked as such)
128+
pub fn should_hoist_as_loop(&self, var_name: &str) -> bool {
129+
if var_name == "_" || self.is_unbound(var_name) {
130+
true
131+
} else {
132+
// Treat variables that haven't been bound in this scope as potential loop iterators
133+
!self.bound_vars.contains(var_name)
134+
}
135+
}
136+
137+
/// Create a child context inheriting parent bindings, output expressions, and context type
138+
pub fn child(&self) -> Self {
139+
Self {
140+
context_type: self.context_type.clone(),
141+
bound_vars: self.bound_vars.clone(),
142+
unbound_vars: self.unbound_vars.clone(),
143+
key_expr: self.key_expr.clone(),
144+
value_expr: self.value_expr.clone(),
145+
}
146+
}
147+
}
148+
149+
impl Default for ScopeContext {
150+
fn default() -> Self {
151+
Self::new()
152+
}
153+
}

0 commit comments

Comments
 (0)