feat(hoist): pre-compute loop hoisting metadata at compilation time (#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>
This commit is contained in:
Anand Krishnamoorthi
2025-10-08 11:11:30 -05:00
committed by GitHub
parent 9604fe86f1
commit 5d8387f4d9
14 changed files with 1365 additions and 267 deletions

View File

@@ -846,6 +846,67 @@ impl Engine {
query_module.num_statements = parser.num_statements();
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
// Populate loop hoisting for the query snippet
// Query snippets are treated as if they're in a module appended at the end (same as analyzer)
// The loop hoisting table already has capacity for this (ensured in prepare_for_eval)
let module_idx = self.modules.len() as u32;
use crate::compiler::hoist::LoopHoister;
let query_schedule_rc = Rc::new(query_schedule.clone());
let mut hoister = LoopHoister::new_with_schedule(query_schedule_rc);
hoister.populate_query_snippet(
module_idx,
&query_node,
query_module.num_statements,
query_module.num_expressions,
)?;
let query_loops = hoister.finalize();
#[cfg(debug_assertions)]
{
debug_assert_eq!(
query_loops.module_len(),
module_idx as usize + 1,
"query hoisting table missing expected module slot {}",
module_idx
);
for stmt in &query_node.stmts {
debug_assert!(
query_loops
.get_statement_loops(module_idx, stmt.sidx)
.is_some(),
"missing hoisted loop entry for query statement index {}",
stmt.sidx
);
}
}
// Get the existing table, merge in the query loops, and set it back
let mut existing_table = self.interpreter.take_loop_hoisting_table();
existing_table.truncate_modules(self.modules.len());
#[cfg(debug_assertions)]
{
debug_assert!(
existing_table.module_len() <= self.modules.len(),
"loop hoisting table should not retain extra modules before merge"
);
}
existing_table.merge_query_loops(query_loops, self.modules.len());
#[cfg(debug_assertions)]
{
for stmt in &query_node.stmts {
debug_assert!(
existing_table
.get_statement_loops(module_idx, stmt.sidx)
.is_some(),
"missing hoisted loop entry after merge for module {} stmt {}",
module_idx,
stmt.sidx
);
}
}
self.interpreter.set_loop_hoisting_table(existing_table);
Ok((Ref::new(query_module), query_node, query_schedule))
}
@@ -873,9 +934,8 @@ impl Engine {
if !self.prepared {
// Analyze the modules and determine how statements must be scheduled.
let analyzer = Analyzer::new();
let schedule = analyzer.analyze(&self.modules)?;
let schedule = Rc::new(analyzer.analyze(&self.modules)?);
self.interpreter.set_schedule(Some(schedule));
self.interpreter.set_modules(self.modules.clone());
self.interpreter.clear_builtins_cache();
@@ -889,6 +949,16 @@ impl Engine {
self.interpreter.gather_rules()?;
self.interpreter.process_imports()?;
// Populate loop hoisting table for efficient evaluation
// Reserve capacity for 1 extra module (for query modules)
use crate::compiler::hoist::LoopHoister;
let hoister = LoopHoister::new_with_schedule(schedule.clone());
let loop_lookup = hoister.populate_with_extra_capacity(&self.modules, 0)?;
self.interpreter.set_loop_hoisting_table(loop_lookup);
// Set schedule after hoisting completes
self.interpreter.set_schedule(Some(schedule));
#[cfg(feature = "azure_policy")]
if for_target {
// Resolve and validate target specifications across all modules