feat!: Introduce structured destructuring plans for bindings (#485)

- add a dedicated `compiler/destructuring_planner` feature that precomputes binding plans for assignments, parameters, and `some in` expressions
- enrich `ScopeContext` with same-scope tracking, local scheduling hints, and module globals so the planner enforces := shadowing rules without blocking parent scopes
- wire the planner through compiler, hoist, interpreter, and engine paths while updating binding plan variants and adding query traversal helpers for dependency analysis
- document the new planner architecture and ship interpreter regressions that exercise nested destructuring, shadowing, and error reporting

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2025-10-21 15:57:49 -05:00
committed by GitHub
parent 25a7ddad0a
commit 1e4ff952e6
27 changed files with 3334 additions and 1009 deletions

View File

@@ -3,7 +3,7 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_numeric};
use crate::builtins::utils::{ensure_args_count, ensure_numeric, validate_integer_arg};
use crate::lexer::Span;
use crate::value::Value;
@@ -19,77 +19,111 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn
m.insert("bits.xor", (xor, 2));
}
fn and(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn and(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "bits.and";
ensure_args_count(span, name, params, args, 2)?;
let v1 = ensure_numeric(name, &params[0], &args[0])?;
let v2 = ensure_numeric(name, &params[1], &args[1])?;
if !validate_integer_arg(name, &params[0], &args[0], &v1, strict, true)?
|| !validate_integer_arg(name, &params[1], &args[1], &v2, strict, true)?
{
return Ok(Value::Undefined);
}
Ok(match v1.and(&v2) {
Some(v) => Value::from(v),
_ => Value::Undefined,
})
}
fn lsh(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn lsh(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "bits.lsh";
ensure_args_count(span, name, params, args, 2)?;
let v1 = ensure_numeric(name, &params[0], &args[0])?;
let v2 = ensure_numeric(name, &params[1], &args[1])?;
if !validate_integer_arg(name, &params[0], &args[0], &v1, strict, true)?
|| !validate_integer_arg(name, &params[1], &args[1], &v2, strict, false)?
{
return Ok(Value::Undefined);
}
Ok(match v1.lsh(&v2) {
Some(v) => Value::from(v),
_ => Value::Undefined,
})
}
fn negate(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn negate(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "bits.negate";
ensure_args_count(span, name, params, args, 1)?;
let v = ensure_numeric(name, &params[0], &args[0])?;
if !validate_integer_arg(name, &params[0], &args[0], &v, strict, true)? {
return Ok(Value::Undefined);
}
Ok(match v.neg() {
Some(v) => Value::from(v),
_ => Value::Undefined,
})
}
fn or(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn or(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "bits.or";
ensure_args_count(span, name, params, args, 2)?;
let v1 = ensure_numeric(name, &params[0], &args[0])?;
let v2 = ensure_numeric(name, &params[1], &args[1])?;
if !validate_integer_arg(name, &params[0], &args[0], &v1, strict, true)?
|| !validate_integer_arg(name, &params[1], &args[1], &v2, strict, true)?
{
return Ok(Value::Undefined);
}
Ok(match v1.or(&v2) {
Some(v) => Value::from(v),
_ => Value::Undefined,
})
}
fn rsh(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn rsh(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "bits.rsh";
ensure_args_count(span, name, params, args, 2)?;
let v1 = ensure_numeric(name, &params[0], &args[0])?;
let v2 = ensure_numeric(name, &params[1], &args[1])?;
if !validate_integer_arg(name, &params[0], &args[0], &v1, strict, true)?
|| !validate_integer_arg(name, &params[1], &args[1], &v2, strict, false)?
{
return Ok(Value::Undefined);
}
Ok(match v1.rsh(&v2) {
Some(v) => Value::from(v),
_ => Value::Undefined,
})
}
fn xor(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn xor(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "bits.xor";
ensure_args_count(span, name, params, args, 2)?;
let v1 = ensure_numeric(name, &params[0], &args[0])?;
let v2 = ensure_numeric(name, &params[1], &args[1])?;
if !validate_integer_arg(name, &params[0], &args[0], &v1, strict, true)?
|| !validate_integer_arg(name, &params[1], &args[1], &v2, strict, true)?
{
return Ok(Value::Undefined);
}
Ok(match v1.xor(&v2) {
Some(v) => Value::from(v),
_ => Value::Undefined,

View File

@@ -45,6 +45,50 @@ pub fn ensure_numeric(fcn: &str, arg: &Expr, v: &Value) -> Result<Number> {
})
}
pub fn validate_integer_arg(
fcn: &str,
param: &Ref<Expr>,
original_value: &Value,
numeric_value: &Number,
strict: bool,
allow_negative: bool,
) -> Result<bool> {
if !numeric_value.is_integer() {
if strict {
bail!(param.span().error(
format!("`{fcn}` expects integer arguments. Got `{original_value}`").as_str()
));
}
return Ok(false);
}
if !allow_negative {
if let Some(int_value) = numeric_value.as_i128() {
if int_value < 0 {
if strict {
bail!(param.span().error(
format!("`{fcn}` expects non-negative integer arguments. Got `{original_value}`")
.as_str(),
));
}
return Ok(false);
}
} else if !numeric_value.is_positive() {
if strict {
bail!(param.span().error(
format!(
"`{fcn}` expects non-negative integer arguments. Got `{original_value}`"
)
.as_str(),
));
}
return Ok(false);
}
}
Ok(true)
}
pub fn ensure_string(fcn: &str, arg: &Expr, v: &Value) -> Result<Rc<str>> {
Ok(match &v {
Value::String(s) => s.clone(),

View File

@@ -7,4 +7,5 @@
//! the compilation phase to prepare policies for efficient execution.
pub mod context;
pub mod destructuring_planner;
pub mod hoist;

View File

@@ -32,19 +32,34 @@ pub enum ContextType {
#[derive(Debug, Clone)]
pub struct ScopeContext {
/// Type of context (Rule, Comprehension, Every, Query)
#[allow(dead_code)]
pub context_type: ContextType,
/// Variables that are bound in the current scope
pub bound_vars: BTreeSet<String>,
/// Variables that are introduced in this scope (used for conflict detection)
pub current_scope_bound_vars: BTreeSet<String>,
/// Variables that are explicitly marked as unbound (from `some` declarations)
pub unbound_vars: BTreeSet<String>,
/// Variables that are local to this scope and will become bound once assigned
pub local_vars: BTreeSet<String>,
/// Flag indicating whether scheduler scope information was available
pub has_scheduler_scope: bool,
/// Key expression from rule head or object comprehension (for output expression hoisting)
#[allow(dead_code)]
pub key_expr: Option<ExprRef>,
/// Value expression from rule assignment or comprehension term (for output expression hoisting)
#[allow(dead_code)]
pub value_expr: Option<ExprRef>,
/// Shared set of module-level globals available in this scope
pub module_globals: Option<crate::Rc<BTreeSet<String>>>,
}
impl ScopeContext {
@@ -53,9 +68,13 @@ impl ScopeContext {
Self {
context_type: ContextType::Query,
bound_vars: BTreeSet::new(),
current_scope_bound_vars: BTreeSet::new(),
unbound_vars: BTreeSet::new(),
local_vars: BTreeSet::new(),
has_scheduler_scope: false,
key_expr: None,
value_expr: None,
module_globals: None,
}
}
@@ -65,9 +84,13 @@ impl ScopeContext {
Self {
context_type,
bound_vars: BTreeSet::new(),
current_scope_bound_vars: BTreeSet::new(),
unbound_vars: BTreeSet::new(),
local_vars: BTreeSet::new(),
has_scheduler_scope: false,
key_expr: None,
value_expr: None,
module_globals: None,
}
}
@@ -81,9 +104,13 @@ impl ScopeContext {
Self {
context_type,
bound_vars: BTreeSet::new(),
current_scope_bound_vars: BTreeSet::new(),
unbound_vars: BTreeSet::new(),
local_vars: BTreeSet::new(),
has_scheduler_scope: false,
key_expr,
value_expr,
module_globals: None,
}
}
@@ -97,9 +124,13 @@ impl ScopeContext {
Self {
context_type,
bound_vars: self.bound_vars.clone(),
current_scope_bound_vars: BTreeSet::new(),
unbound_vars: self.unbound_vars.clone(),
local_vars: self.local_vars.clone(),
has_scheduler_scope: self.has_scheduler_scope,
key_expr,
value_expr,
module_globals: self.module_globals.clone(),
}
}
@@ -107,13 +138,18 @@ impl ScopeContext {
pub fn bind_variable(&mut self, var_name: &str) {
if var_name != "_" {
self.bound_vars.insert(var_name.to_string());
self.current_scope_bound_vars.insert(var_name.to_string());
self.unbound_vars.remove(var_name);
self.local_vars.remove(var_name);
}
}
/// Mark a variable as unbound
pub fn add_unbound_variable(&mut self, var_name: &str) {
if var_name != "_" && !self.bound_vars.contains(var_name) {
if var_name != "_" {
self.bound_vars.remove(var_name);
self.current_scope_bound_vars.remove(var_name);
self.local_vars.remove(var_name);
self.unbound_vars.insert(var_name.to_string());
}
}
@@ -126,23 +162,27 @@ impl ScopeContext {
/// Check if we can determine that a variable should be treated as a loop iterator
/// (either it's unbound or explicitly marked as such)
pub fn should_hoist_as_loop(&self, var_name: &str) -> bool {
if var_name == "_" || self.is_unbound(var_name) {
true
} else {
// Treat variables that haven't been bound in this scope as potential loop iterators
!self.bound_vars.contains(var_name)
if var_name == "_" {
return true;
}
}
/// Create a child context inheriting parent bindings, output expressions, and context type
pub fn child(&self) -> Self {
Self {
context_type: self.context_type.clone(),
bound_vars: self.bound_vars.clone(),
unbound_vars: self.unbound_vars.clone(),
key_expr: self.key_expr.clone(),
value_expr: self.value_expr.clone(),
if self
.module_globals
.as_ref()
.is_some_and(|globals| globals.contains(var_name))
{
return false;
}
if self.is_unbound(var_name) {
return true;
}
if self.has_scheduler_scope {
return self.local_vars.contains(var_name);
}
!self.bound_vars.contains(var_name)
}
}

View File

@@ -0,0 +1,401 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Assignment-specific planning utilities.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::ast::{AssignOp, Expr, ExprRef};
use crate::compiler::destructuring_planner::create_destructuring_plan;
use crate::compiler::destructuring_planner::destructuring::create_destructuring_plan_with_tracking;
use crate::compiler::destructuring_planner::utils::{
collect_plan_var_spans, ensure_literal_match, ensure_structural_compatibility,
extract_literal_key, format_literal_key_for_error, plan_only_if_binds,
};
use crate::compiler::destructuring_planner::{
AssignmentPlan, BindingPlan, BindingPlannerError, DestructuringPlan, Result, ScopingMode,
VariableBindingContext, WildcardSide,
};
use crate::lexer::Span;
use crate::query::traversal::collect_expr_dependencies;
use crate::value::Value;
/// Convenience function for assignment expressions with specific := and = rules.
pub fn create_assignment_binding_plan<T: VariableBindingContext>(
op: AssignOp,
lhs_expr: &ExprRef,
rhs_expr: &ExprRef,
context: &T,
) -> Result<BindingPlan> {
let assignment_plan = match op {
AssignOp::ColEq => {
// For :=, only LHS can be destructured
if let Some(lhs_plan) = plan_only_if_binds(create_destructuring_plan(
lhs_expr,
context,
ScopingMode::AllowShadowing,
)) {
let mut var_spans = Vec::new();
collect_plan_var_spans(&lhs_plan, &mut var_spans);
let mut lhs_scope_bindings = BTreeSet::new();
for span in var_spans {
let name = span.text().to_string();
let is_duplicate = !lhs_scope_bindings.insert(name.clone());
let has_same_scope_binding = context.has_same_scope_binding(&name);
if is_duplicate || has_same_scope_binding {
return Err(BindingPlannerError::VariableAlreadyDefined {
var: name,
span,
});
}
}
ensure_structural_compatibility(lhs_expr, rhs_expr)?;
ensure_literal_match(&lhs_plan, rhs_expr)?;
AssignmentPlan::ColonEquals {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
lhs_plan,
}
} else {
return Err(BindingPlannerError::ColonEqualsRequiresBindableLeft {
span: lhs_expr.span().clone(),
});
}
}
AssignOp::Eq => {
let lhs_struct_plan =
create_destructuring_plan(lhs_expr, context, ScopingMode::RespectParent);
let rhs_struct_plan =
create_destructuring_plan(rhs_expr, context, ScopingMode::RespectParent);
let lhs_plan = plan_only_if_binds(lhs_struct_plan.clone());
let rhs_plan = plan_only_if_binds(rhs_struct_plan.clone());
let lhs_is_wildcard =
matches!(lhs_expr.as_ref(), Expr::Var { span, .. } if span.text() == "_");
let rhs_is_wildcard =
matches!(rhs_expr.as_ref(), Expr::Var { span, .. } if span.text() == "_");
if lhs_is_wildcard || rhs_is_wildcard {
let wildcard_side = match (lhs_is_wildcard, rhs_is_wildcard) {
(true, true) => WildcardSide::Both,
(true, false) => WildcardSide::Lhs,
(false, true) => WildcardSide::Rhs,
(false, false) => unreachable!(),
};
AssignmentPlan::WildcardMatch {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
wildcard_side,
}
} else if lhs_plan.is_some() && rhs_plan.is_some() {
// Both sides have unbound vars - recursively flatten all nested structures
let mut element_pairs = Vec::new();
let mut newly_bound = BTreeSet::new();
flatten_assignment_pairs(
lhs_expr,
rhs_expr,
context,
&mut newly_bound,
&mut element_pairs,
)?;
order_element_pairs(&mut element_pairs, context);
AssignmentPlan::EqualsBothSides {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
element_pairs,
}
} else if lhs_plan.is_none() && rhs_plan.is_none() {
AssignmentPlan::EqualityCheck {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
}
} else if let Some(lhs) = lhs_plan {
ensure_structural_compatibility(lhs_expr, rhs_expr)?;
ensure_literal_match(&lhs, rhs_expr)?;
AssignmentPlan::EqualsBindLeft {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
lhs_plan: lhs,
}
} else if let Some(rhs) = rhs_plan {
ensure_structural_compatibility(rhs_expr, lhs_expr)?;
ensure_literal_match(&rhs, lhs_expr)?;
AssignmentPlan::EqualsBindRight {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
rhs_plan: rhs,
}
} else if let Some(lhs) = lhs_struct_plan {
ensure_structural_compatibility(lhs_expr, rhs_expr)?;
ensure_literal_match(&lhs, rhs_expr)?;
AssignmentPlan::EqualsBindLeft {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
lhs_plan: lhs,
}
} else if let Some(rhs) = rhs_struct_plan {
ensure_structural_compatibility(rhs_expr, lhs_expr)?;
ensure_literal_match(&rhs, lhs_expr)?;
AssignmentPlan::EqualsBindRight {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
rhs_plan: rhs,
}
} else {
AssignmentPlan::EqualityCheck {
lhs_expr: lhs_expr.clone(),
rhs_expr: rhs_expr.clone(),
}
}
}
};
Ok(BindingPlan::Assignment {
plan: assignment_plan,
})
}
/// Recursively flatten assignment destructuring into (value_expr, pattern_plan) pairs.
fn flatten_assignment_pairs<T: VariableBindingContext>(
lhs_expr: &ExprRef,
rhs_expr: &ExprRef,
context: &T,
newly_bound: &mut BTreeSet<String>,
pairs: &mut Vec<(ExprRef, DestructuringPlan)>,
) -> Result<()> {
let (lhs_plan, lhs_delta) =
preview_binding_plan(lhs_expr, context, ScopingMode::RespectParent, newly_bound);
let (rhs_plan, rhs_delta) =
preview_binding_plan(rhs_expr, context, ScopingMode::RespectParent, newly_bound);
if lhs_plan.is_none() && rhs_plan.is_none() {
pairs.push((
rhs_expr.clone(),
DestructuringPlan::EqualityExpr(lhs_expr.clone()),
));
return Ok(());
}
let lhs_is_array = matches!(lhs_expr.as_ref(), Expr::Array { .. });
let rhs_is_array = matches!(rhs_expr.as_ref(), Expr::Array { .. });
let lhs_is_object = matches!(lhs_expr.as_ref(), Expr::Object { .. });
let rhs_is_object = matches!(rhs_expr.as_ref(), Expr::Object { .. });
if (lhs_is_array && rhs_is_object) || (lhs_is_object && rhs_is_array) {
return Err(BindingPlannerError::IncompatibleDestructuringPatterns {
span: lhs_expr.span().clone(),
});
}
if lhs_is_array && rhs_is_array {
for (lhs_item, rhs_item) in collect_array_pairs(lhs_expr, rhs_expr)? {
flatten_assignment_pairs(&lhs_item, &rhs_item, context, newly_bound, pairs)?;
}
return Ok(());
}
if lhs_is_object && rhs_is_object {
if let Some(object_pairs) = collect_object_pairs(lhs_expr, rhs_expr)? {
for (lhs_value, rhs_value) in object_pairs {
flatten_assignment_pairs(&lhs_value, &rhs_value, context, newly_bound, pairs)?;
}
return Ok(());
}
}
match (lhs_plan, rhs_plan, lhs_expr.as_ref(), rhs_expr.as_ref()) {
// Case 1: LHS has pattern, RHS is value - add pair
(Some(lhs_pattern), None, _, _) => {
newly_bound.extend(lhs_delta);
pairs.push((rhs_expr.clone(), lhs_pattern));
}
// Case 2: RHS has pattern, LHS is value - add pair
(None, Some(rhs_pattern), _, _) => {
newly_bound.extend(rhs_delta);
pairs.push((lhs_expr.clone(), rhs_pattern));
}
// Case 5: Both have patterns but incompatible structures
(Some(_), Some(_), _, _) => {
return Err(BindingPlannerError::IncompatibleDestructuringPatterns {
span: lhs_expr.span().clone(),
});
}
// Remaining cases are handled by earlier match arms and guard
(None, None, _, _) => {
unreachable!("handled by equality guard above");
}
}
Ok(())
}
fn collect_array_pairs(lhs_expr: &ExprRef, rhs_expr: &ExprRef) -> Result<Vec<(ExprRef, ExprRef)>> {
let lhs_items = match lhs_expr.as_ref() {
Expr::Array { items, .. } => items,
_ => unreachable!(),
};
let rhs_items = match rhs_expr.as_ref() {
Expr::Array { items, .. } => items,
_ => unreachable!(),
};
if lhs_items.len() != rhs_items.len() {
return Err(BindingPlannerError::ArraySizeMismatch {
left_size: lhs_items.len(),
right_size: rhs_items.len(),
span: lhs_expr.span().clone(),
});
}
Ok(lhs_items
.iter()
.cloned()
.zip(rhs_items.iter().cloned())
.collect())
}
fn order_element_pairs<T: VariableBindingContext>(
element_pairs: &mut Vec<(ExprRef, DestructuringPlan)>,
context: &T,
) {
if element_pairs.len() <= 1 {
return;
}
let mut remaining: Vec<_> = element_pairs
.drain(..)
.map(|(value_expr, plan)| {
let binds = plan.bound_vars().into_iter().collect::<BTreeSet<_>>();
let deps = collect_expr_dependencies(&value_expr);
(value_expr, plan, deps, binds)
})
.collect();
if remaining.iter().any(|(_, _, deps, _)| deps.is_none()) {
*element_pairs = remaining
.into_iter()
.map(|(value_expr, plan, _, _)| (value_expr, plan))
.collect();
return;
}
let mut scheduled = BTreeSet::new();
let mut ordered = Vec::with_capacity(remaining.len());
while !remaining.is_empty() {
let mut progress = false;
for idx in 0..remaining.len() {
let (_, _, deps, _) = &remaining[idx];
let deps = deps.as_ref().expect("checked above");
let ready = deps.iter().all(|var| {
scheduled.contains(var) || !context.is_var_unbound(var, ScopingMode::RespectParent)
});
if ready {
let (value_expr, plan, _deps, binds) = remaining.remove(idx);
scheduled.extend(binds.into_iter());
ordered.push((value_expr, plan));
progress = true;
break;
}
}
if !progress {
ordered.extend(
remaining
.into_iter()
.map(|(value_expr, plan, _, _)| (value_expr, plan)),
);
break;
}
}
*element_pairs = ordered;
}
fn collect_object_pairs(
lhs_expr: &ExprRef,
rhs_expr: &ExprRef,
) -> Result<Option<Vec<(ExprRef, ExprRef)>>> {
let lhs_fields = match lhs_expr.as_ref() {
Expr::Object { fields, .. } => fields,
_ => unreachable!(),
};
let rhs_fields = match rhs_expr.as_ref() {
Expr::Object { fields, .. } => fields,
_ => unreachable!(),
};
let mut lhs_map: BTreeMap<Value, ExprRef> = BTreeMap::new();
for (_, key_expr, val_expr) in lhs_fields {
if let Some(key_value) = extract_literal_key(key_expr) {
lhs_map.insert(key_value, val_expr.clone());
} else {
return Ok(None);
}
}
let mut pairs = Vec::with_capacity(lhs_map.len());
let mut rhs_literal_count = 0;
let mut missing_literal_key: Option<(String, Span)> = None;
for (_, key_expr, val_expr) in rhs_fields {
if let Some(key_value) = extract_literal_key(key_expr) {
rhs_literal_count += 1;
if let Some(lhs_value_expr) = lhs_map.get(&key_value) {
pairs.push((lhs_value_expr.clone(), val_expr.clone()));
} else if missing_literal_key.is_none() {
missing_literal_key = Some((
format_literal_key_for_error(&key_value),
key_expr.span().clone(),
));
}
} else {
return Ok(None);
}
}
if rhs_literal_count != lhs_map.len() {
return Err(BindingPlannerError::ObjectFieldCountMismatch {
left_count: lhs_map.len(),
right_count: rhs_literal_count,
span: lhs_expr.span().clone(),
});
}
if let Some((key, span)) = missing_literal_key {
return Err(BindingPlannerError::ObjectKeyNotFound { key, span });
}
Ok(Some(pairs))
}
fn preview_binding_plan<T: VariableBindingContext>(
expr: &ExprRef,
context: &T,
scoping: ScopingMode,
already_bound: &BTreeSet<String>,
) -> (Option<DestructuringPlan>, BTreeSet<String>) {
let mut scratch = already_bound.clone();
let plan = create_destructuring_plan_with_tracking(expr, context, scoping, &mut scratch);
let mut delta: BTreeSet<String> = scratch.difference(already_bound).cloned().collect();
let plan = plan_only_if_binds(plan);
if plan.is_none() {
delta.clear();
}
(plan, delta)
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Context traits shared across planner submodules.
use alloc::collections::BTreeSet;
use alloc::string::String;
/// Scoping mode for variable binding decisions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopingMode {
/// Respect existing bindings from parent scopes (normal scoping).
RespectParent,
/// Allow shadowing of parent scope bindings (local scoping).
AllowShadowing,
}
/// Trait for determining variable binding status within a scope context.
pub trait VariableBindingContext {
/// Check if a variable is unbound in this context.
///
/// # Arguments
/// * `var_name` - The name of the variable to check
/// * `scoping` - Whether to respect parent scopes or allow shadowing
fn is_var_unbound(&self, var_name: &str, scoping: ScopingMode) -> bool;
/// Determine whether the current scope already bound this variable.
///
/// This is used to detect same-scope rebinding conflicts when the planner
/// encounters `:=` assignments, while still allowing shadowing in nested scopes.
fn has_same_scope_binding(&self, var_name: &str) -> bool;
}
/// Context overlay that tracks newly bound variables on top of an existing context.
pub(crate) struct OverlayBindingContext<'a, T: VariableBindingContext> {
pub(crate) base: &'a T,
pub(crate) newly_bound: &'a BTreeSet<String>,
}
impl<'a, T: VariableBindingContext> VariableBindingContext for OverlayBindingContext<'a, T> {
fn is_var_unbound(&self, var_name: &str, scoping: ScopingMode) -> bool {
if self.newly_bound.contains(var_name) {
return false;
}
self.base.is_var_unbound(var_name, scoping)
}
fn has_same_scope_binding(&self, var_name: &str) -> bool {
if self.newly_bound.contains(var_name) {
return true;
}
self.base.has_same_scope_binding(var_name)
}
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Functions responsible for building destructuring plans.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::ast::{Expr, ExprRef};
use crate::compiler::destructuring_planner::context::OverlayBindingContext;
use crate::compiler::destructuring_planner::utils::extract_literal_key;
use crate::compiler::destructuring_planner::{
DestructuringPlan, ScopingMode, VariableBindingContext,
};
/// Create a destructuring plan for an expression using a variable binding context.
pub fn create_destructuring_plan<T: VariableBindingContext>(
expr: &ExprRef,
context: &T,
scoping: ScopingMode,
) -> Option<DestructuringPlan> {
let mut newly_bound = BTreeSet::new();
create_destructuring_plan_with_tracking(expr, context, scoping, &mut newly_bound)
}
/// Create a destructuring plan while tracking newly bound variables.
pub(crate) fn create_destructuring_plan_with_tracking<T: VariableBindingContext>(
expr: &ExprRef,
context: &T,
scoping: ScopingMode,
newly_bound: &mut BTreeSet<String>,
) -> Option<DestructuringPlan> {
let overlay = OverlayBindingContext {
base: context,
newly_bound,
};
match expr.as_ref() {
// Variable binding
Expr::Var { span: name, .. } => {
if name.text() == "_" {
return Some(DestructuringPlan::Ignore);
}
if overlay.is_var_unbound(name.text(), scoping) {
newly_bound.insert(name.text().to_string());
Some(DestructuringPlan::Var(name.clone()))
} else {
// Already bound - treat as equality check
Some(DestructuringPlan::EqualityExpr(expr.clone()))
}
}
Expr::String { value, .. }
| Expr::RawString { value, .. }
| Expr::Number { value, .. }
| Expr::Bool { value, .. }
| Expr::Null { value, .. } => Some(DestructuringPlan::EqualityValue(value.clone())),
// Array destructuring
Expr::Array { items, .. } => {
let mut element_plans = Vec::new();
for item in items {
if let Some(plan) =
create_destructuring_plan_with_tracking(item, context, scoping, newly_bound)
{
element_plans.push(plan);
} else {
// If any element can't be destructured, fail the whole array
return None;
}
}
Some(DestructuringPlan::Array { element_plans })
}
// Object destructuring
Expr::Object { fields, .. } => {
let mut field_plans = BTreeMap::new();
let mut dynamic_fields = Vec::new();
for (_, key_expr, value_expr) in fields {
if let Some(value_plan) = create_destructuring_plan_with_tracking(
value_expr,
context,
scoping,
newly_bound,
) {
if let Some(key_value) = extract_literal_key(key_expr) {
field_plans.insert(key_value, value_plan);
} else {
dynamic_fields.push((key_expr.clone(), value_plan));
}
} else {
return None;
}
}
Some(DestructuringPlan::Object {
field_plans,
dynamic_fields,
})
}
// For all others, treat as equality checks
_ => Some(DestructuringPlan::EqualityExpr(expr.clone())),
}
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Error definitions for the destructuring planner.
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use anyhow::Error;
use core::error::Error as CoreError;
use core::fmt;
use crate::lexer::Span;
/// Errors produced while building binding plans.
#[derive(Debug)]
pub enum BindingPlannerError {
/// Assignment operator := requires left-hand side to have bindable variables.
ColonEqualsRequiresBindableLeft { span: Span },
/// Array size mismatch in assignment destructuring.
ArraySizeMismatch {
left_size: usize,
right_size: usize,
span: Span,
},
/// Array length mismatch detected while planning evaluation (no assignments).
ArrayLengthMismatch {
expected: usize,
actual: usize,
span: Span,
},
/// Object literal keys mismatch detected while planning evaluation (no assignments).
ObjectLiteralKeysMismatch {
expected: Vec<String>,
actual: Vec<String>,
span: Span,
},
/// Object field count mismatch in assignment destructuring.
ObjectFieldCountMismatch {
left_count: usize,
right_count: usize,
span: Span,
},
/// Object key not found in destructuring.
ObjectKeyNotFound { key: String, span: Span },
/// Variable reuse detected when a new binding is required.
VariableAlreadyDefined { var: String, span: Span },
/// Incompatible destructuring patterns.
IncompatibleDestructuringPatterns { span: Span },
/// Failed to create destructuring plan.
FailedToCreateDestructuringPlan { plan_type: String, span: Span },
}
/// Result alias used throughout the binding planner.
pub type Result<T> = core::result::Result<T, BindingPlannerError>;
/// Convert planner errors into diagnostic-rich anyhow errors for compiler callers.
pub fn map_binding_error(err: BindingPlannerError) -> Error {
match err {
BindingPlannerError::ColonEqualsRequiresBindableLeft { span } => span
.error("assignment operator := requires left-hand side to have bindable variables"),
BindingPlannerError::ArraySizeMismatch { span, .. }
| BindingPlannerError::ArrayLengthMismatch { span, .. } => {
span.error("mismatch in number of array elements")
}
BindingPlannerError::ObjectLiteralKeysMismatch {
expected,
actual,
span,
} => span.error(&format!(
"object literal keys mismatch. Expected keys {:?} got {:?}.",
expected, actual
)),
BindingPlannerError::ObjectFieldCountMismatch {
left_count,
right_count,
span,
} => span.error(&format!(
"object field count mismatch in assignment: left has {left_count} fields, right has {right_count} fields"
)),
BindingPlannerError::ObjectKeyNotFound { key, span } => span
.error(&format!("key \"{key}\" not found in left-hand side object during destructuring")),
BindingPlannerError::VariableAlreadyDefined { var, span } => {
span.error(&format!("var `{var}` used before definition below"))
}
BindingPlannerError::IncompatibleDestructuringPatterns { span } => span.error(
"incompatible destructuring patterns: both sides must be arrays or objects with matching structure",
),
BindingPlannerError::FailedToCreateDestructuringPlan { plan_type, span } => {
span.error(&format!("failed to create {plan_type} destructuring plan"))
}
}
}
impl BindingPlannerError {
pub(crate) fn to_span_message(&self) -> String {
match self {
BindingPlannerError::ColonEqualsRequiresBindableLeft { span } => span
.message(
"error",
"assignment operator := requires left-hand side to have bindable variables",
),
BindingPlannerError::ArraySizeMismatch {
left_size,
right_size,
span,
} => {
let detail = format!(
"mismatch in number of array elements (left has {left_size}, right has {right_size})"
);
span.message("error", detail.as_str())
}
BindingPlannerError::ArrayLengthMismatch {
expected,
actual,
span,
} => {
let detail = format!(
"array length mismatch. Expected {expected} got {actual}."
);
span.message("error", detail.as_str())
}
BindingPlannerError::ObjectLiteralKeysMismatch {
expected,
actual,
span,
} => {
let detail = format!(
"object literal keys mismatch. Expected keys {:?} got {:?}.",
expected, actual
);
span.message("error", detail.as_str())
}
BindingPlannerError::ObjectFieldCountMismatch {
left_count,
right_count,
span,
} => {
let detail = format!(
"object field count mismatch in assignment: left has {left_count} fields, right has {right_count} fields"
);
span.message("error", detail.as_str())
}
BindingPlannerError::ObjectKeyNotFound { key, span } => {
let detail = format!(
"key \"{key}\" not found in left-hand side object during destructuring"
);
span.message("error", detail.as_str())
}
BindingPlannerError::VariableAlreadyDefined { var, span } => {
let detail = format!("var `{var}` used before definition below");
span.message("error", detail.as_str())
}
BindingPlannerError::IncompatibleDestructuringPatterns { span } => span
.message(
"error",
"incompatible destructuring patterns: both sides must be arrays or objects with matching structure",
),
BindingPlannerError::FailedToCreateDestructuringPlan { plan_type, span } => {
let detail = format!("failed to create {plan_type} destructuring plan");
span.message("error", detail.as_str())
}
}
}
}
impl fmt::Display for BindingPlannerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_span_message())
}
}
impl CoreError for BindingPlannerError {}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Destructuring and binding planner utilities.
//!
//! This module hierarchy reorganizes the binding planner into smaller components
//! so the compiler can evolve without ambiguity with FFI bindings. Submodules
//! will be filled in as code is migrated from the legacy `bindings` module.
pub mod assignment;
pub mod context;
pub mod destructuring;
pub mod error;
pub mod parameters;
pub mod plans;
pub mod some_in;
pub mod utils;
pub use assignment::create_assignment_binding_plan;
pub use context::{ScopingMode, VariableBindingContext};
pub use destructuring::create_destructuring_plan;
pub use error::{map_binding_error, BindingPlannerError, Result};
pub use parameters::{create_loop_index_binding_plan, create_parameter_binding_plan};
pub use plans::{AssignmentPlan, BindingPlan, DestructuringPlan, WildcardSide};
pub use some_in::create_some_in_binding_plan;

View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Planner helpers for function parameters and loop indices.
use alloc::collections::BTreeSet;
use alloc::string::ToString;
use crate::ast::ExprRef;
use crate::compiler::destructuring_planner::create_destructuring_plan;
use crate::compiler::destructuring_planner::destructuring::create_destructuring_plan_with_tracking;
use crate::compiler::destructuring_planner::utils::validate_pattern_bindings;
use crate::compiler::destructuring_planner::{
BindingPlan, BindingPlannerError, Result, ScopingMode, VariableBindingContext,
};
/// Convenience function for loop index expressions (respects parent scope).
pub fn create_loop_index_binding_plan<T: VariableBindingContext>(
index_expr: &ExprRef,
context: &T,
) -> Result<BindingPlan> {
let destructuring_plan =
create_destructuring_plan(index_expr, context, ScopingMode::RespectParent).ok_or_else(
|| BindingPlannerError::FailedToCreateDestructuringPlan {
plan_type: "loop index".to_string(),
span: index_expr.span().clone(),
},
)?;
Ok(BindingPlan::LoopIndex {
index_expr: index_expr.clone(),
destructuring_plan,
})
}
/// Convenience function for function parameters (always allow shadowing).
pub fn create_parameter_binding_plan<T: VariableBindingContext>(
param_expr: &ExprRef,
context: &T,
) -> Result<BindingPlan> {
let mut newly_bound = BTreeSet::new();
let destructuring_plan = create_destructuring_plan_with_tracking(
param_expr,
context,
ScopingMode::AllowShadowing,
&mut newly_bound,
)
.ok_or_else(|| BindingPlannerError::FailedToCreateDestructuringPlan {
plan_type: "parameter".to_string(),
span: param_expr.span().clone(),
})?;
validate_pattern_bindings(param_expr, &newly_bound, context)?;
Ok(BindingPlan::Parameter {
param_expr: param_expr.clone(),
destructuring_plan,
})
}

View File

@@ -0,0 +1,241 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Core data structures used by the destructuring planner.
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::ast::ExprRef;
use crate::lexer::Span;
use crate::value::Value;
/// Strategy for how to destructure an expression.
#[derive(Debug, Clone)]
pub enum DestructuringPlan {
/// Bind the entire value to a variable.
Var(Span),
/// Ignore the value completely.
Ignore,
/// Check equality with the value of the dynamic expression.
EqualityExpr(ExprRef),
/// Check equality against a literal value captured at planning time.
EqualityValue(Value),
/// Destructure an array.
Array {
element_plans: Vec<DestructuringPlan>,
},
/// Destructure an object.
Object {
field_plans: BTreeMap<Value, DestructuringPlan>,
dynamic_fields: Vec<(ExprRef, DestructuringPlan)>,
},
}
impl DestructuringPlan {
fn collect_bound_vars(&self, vars: &mut Vec<String>) {
match self {
DestructuringPlan::Var(name) => vars.push(name.text().to_string()),
DestructuringPlan::Array { element_plans } => {
for element in element_plans {
element.collect_bound_vars(vars);
}
}
DestructuringPlan::Object {
field_plans,
dynamic_fields,
} => {
for plan in field_plans.values() {
plan.collect_bound_vars(vars);
}
for (_, plan) in dynamic_fields {
plan.collect_bound_vars(vars);
}
}
DestructuringPlan::Ignore
| DestructuringPlan::EqualityExpr(_)
| DestructuringPlan::EqualityValue(_) => {}
}
}
pub(crate) fn contains_wildcards(&self) -> bool {
match self {
DestructuringPlan::Ignore => true,
DestructuringPlan::Array { element_plans } => element_plans
.iter()
.any(DestructuringPlan::contains_wildcards),
DestructuringPlan::Object {
field_plans,
dynamic_fields,
} => {
field_plans
.values()
.any(DestructuringPlan::contains_wildcards)
|| dynamic_fields
.iter()
.any(|(_, plan)| plan.contains_wildcards())
}
DestructuringPlan::Var(_)
| DestructuringPlan::EqualityExpr(_)
| DestructuringPlan::EqualityValue(_) => false,
}
}
pub(crate) fn bound_vars(&self) -> Vec<String> {
let mut vars = Vec::new();
self.collect_bound_vars(&mut vars);
vars
}
pub(crate) fn introduces_binding(&self) -> bool {
match self {
DestructuringPlan::Var(_) | DestructuringPlan::Ignore => true,
DestructuringPlan::Array { element_plans } => element_plans
.iter()
.any(DestructuringPlan::introduces_binding),
DestructuringPlan::Object {
field_plans,
dynamic_fields,
} => {
field_plans
.values()
.any(DestructuringPlan::introduces_binding)
|| dynamic_fields
.iter()
.any(|(_, plan)| plan.introduces_binding())
}
DestructuringPlan::EqualityExpr(_) | DestructuringPlan::EqualityValue(_) => false,
}
}
}
/// Strategy-based assignment plan with specific rules for := and = operators.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum AssignmentPlan {
/// For := (ColEq) - only LHS can have patterns.
ColonEquals {
lhs_expr: ExprRef,
rhs_expr: ExprRef,
lhs_plan: DestructuringPlan,
},
/// For = (Eq) - only one side has unbound variables.
EqualsBindLeft {
lhs_expr: ExprRef,
rhs_expr: ExprRef,
lhs_plan: DestructuringPlan,
},
/// For = (Eq) - only one side has unbound variables.
EqualsBindRight {
lhs_expr: ExprRef,
rhs_expr: ExprRef,
rhs_plan: DestructuringPlan,
},
/// For = (Eq) - both sides have unbound vars, flattened to pairs.
/// Each pair is (value_expr, destructuring_plan_for_pattern).
EqualsBothSides {
lhs_expr: ExprRef,
rhs_expr: ExprRef,
element_pairs: Vec<(ExprRef, DestructuringPlan)>,
},
/// No variables to bind - simple equality check.
EqualityCheck {
lhs_expr: ExprRef,
rhs_expr: ExprRef,
},
/// No variables to bind and at least one side is a wildcard `_`.
WildcardMatch {
lhs_expr: ExprRef,
rhs_expr: ExprRef,
wildcard_side: WildcardSide,
},
}
impl AssignmentPlan {
pub(crate) fn bound_vars(&self) -> Vec<String> {
match self {
AssignmentPlan::ColonEquals { lhs_plan, .. }
| AssignmentPlan::EqualsBindLeft { lhs_plan, .. } => lhs_plan.bound_vars(),
AssignmentPlan::EqualsBindRight { rhs_plan, .. } => rhs_plan.bound_vars(),
AssignmentPlan::EqualsBothSides { element_pairs, .. } => {
let mut vars = Vec::new();
for (_, plan) in element_pairs {
vars.extend(plan.bound_vars());
}
vars
}
AssignmentPlan::EqualityCheck { .. } | AssignmentPlan::WildcardMatch { .. } => {
Vec::new()
}
}
}
}
/// Indicates which side of an equality expression contains a wildcard `_`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum WildcardSide {
Lhs,
Rhs,
Both,
}
/// High-level plan describing how bindings are produced in various contexts.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum BindingPlan {
Assignment {
plan: AssignmentPlan,
},
LoopIndex {
index_expr: ExprRef,
destructuring_plan: DestructuringPlan,
},
Parameter {
param_expr: ExprRef,
destructuring_plan: DestructuringPlan,
},
SomeIn {
collection_expr: ExprRef,
key_plan: Option<DestructuringPlan>,
value_plan: DestructuringPlan,
},
}
impl BindingPlan {
/// Return the set of variables newly bound by this plan.
pub fn bound_vars(&self) -> Vec<String> {
match self {
BindingPlan::Assignment { plan } => plan.bound_vars(),
BindingPlan::LoopIndex {
destructuring_plan, ..
} => destructuring_plan.bound_vars(),
BindingPlan::Parameter {
destructuring_plan, ..
} => destructuring_plan.bound_vars(),
BindingPlan::SomeIn {
key_plan,
value_plan,
..
} => {
let mut vars = Vec::new();
if let Some(key_destructuring) = key_plan {
vars.extend(key_destructuring.bound_vars());
}
vars.extend(value_plan.bound_vars());
vars
}
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Planner support for `some .. in` expressions.
use alloc::collections::BTreeSet;
use alloc::string::String;
use crate::ast::{Expr, ExprRef};
use crate::compiler::destructuring_planner::destructuring::create_destructuring_plan_with_tracking;
use crate::compiler::destructuring_planner::utils::{
check_literal_structure, validate_pattern_bindings, LiteralStructureCheck,
};
use crate::compiler::destructuring_planner::{
BindingPlan, BindingPlannerError, Result, ScopingMode, VariableBindingContext,
};
/// Convenience function for some..in expressions (always allow shadowing for new bindings).
pub fn create_some_in_binding_plan<T: VariableBindingContext>(
key_expr: &Option<ExprRef>,
value_expr: &ExprRef,
collection_expr: &ExprRef,
context: &T,
) -> Result<BindingPlan> {
let key_plan = if let Some(key) = key_expr {
let mut newly_bound = BTreeSet::new();
let plan = create_destructuring_plan_with_tracking(
key,
context,
ScopingMode::RespectParent,
&mut newly_bound,
);
if let Some(plan) = plan {
validate_pattern_bindings(key, &newly_bound, context)?;
Some(plan)
} else {
None
}
} else {
None
};
let mut newly_bound_value = BTreeSet::new();
let value_plan = create_destructuring_plan_with_tracking(
value_expr,
context,
ScopingMode::RespectParent,
&mut newly_bound_value,
)
.ok_or_else(|| BindingPlannerError::FailedToCreateDestructuringPlan {
plan_type: String::from("some-in value"),
span: value_expr.span().clone(),
})?;
validate_pattern_bindings(value_expr, &newly_bound_value, context)?;
if let Expr::Array { items, .. } = collection_expr.as_ref() {
let mut found_match = false;
let mut mismatch_error: Option<BindingPlannerError> = None;
let mut saw_unknown = false;
for collection_item in items {
match check_literal_structure(&value_plan, collection_item) {
LiteralStructureCheck::Match => {
found_match = true;
}
LiteralStructureCheck::Unknown => {
saw_unknown = true;
}
mismatch => {
if let Some(err) = mismatch.into_error() {
if mismatch_error.is_none() {
mismatch_error = Some(err);
}
}
}
}
}
if !found_match && !saw_unknown {
if let Some(err) = mismatch_error {
return Err(err);
}
}
}
Ok(BindingPlan::SomeIn {
collection_expr: collection_expr.clone(),
key_plan,
value_plan,
})
}

View File

@@ -0,0 +1,271 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Shared helper routines for the destructuring planner modules.
use alloc::collections::BTreeSet;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::ast::{Expr, ExprRef};
use crate::compiler::destructuring_planner::{
BindingPlannerError, DestructuringPlan, Result, ScopingMode, VariableBindingContext,
};
use crate::lexer::Span;
use crate::value::Value;
/// Result of statically comparing a destructuring plan with a literal expression.
pub(crate) enum LiteralStructureCheck {
Match,
Unknown,
ArrayMismatch {
expected: usize,
actual: usize,
span: Span,
},
ObjectMismatch {
expected: Vec<String>,
actual: Vec<String>,
span: Span,
},
}
impl LiteralStructureCheck {
pub(crate) fn into_error(self) -> Option<BindingPlannerError> {
match self {
LiteralStructureCheck::Match | LiteralStructureCheck::Unknown => None,
LiteralStructureCheck::ArrayMismatch {
expected,
actual,
span,
} => Some(BindingPlannerError::ArrayLengthMismatch {
expected,
actual,
span,
}),
LiteralStructureCheck::ObjectMismatch {
expected,
actual,
span,
} => Some(BindingPlannerError::ObjectLiteralKeysMismatch {
expected,
actual,
span,
}),
}
}
}
/// Compare a destructuring plan against a literal expression.
pub(crate) fn check_literal_structure(
plan: &DestructuringPlan,
expr: &ExprRef,
) -> LiteralStructureCheck {
match (plan, expr.as_ref()) {
(DestructuringPlan::Array { element_plans }, Expr::Array { items, .. }) => {
if items.len() != element_plans.len() {
return LiteralStructureCheck::ArrayMismatch {
expected: element_plans.len(),
actual: items.len(),
span: expr.span().clone(),
};
}
for (nested_plan, nested_expr) in element_plans.iter().zip(items.iter()) {
match check_literal_structure(nested_plan, nested_expr) {
LiteralStructureCheck::Match => {}
LiteralStructureCheck::Unknown => return LiteralStructureCheck::Unknown,
mismatch => return mismatch,
}
}
LiteralStructureCheck::Match
}
(
DestructuringPlan::Object {
field_plans,
dynamic_fields,
},
Expr::Object { fields, .. },
) => {
if field_plans.is_empty() && dynamic_fields.is_empty() {
return LiteralStructureCheck::Match;
}
if !field_plans.is_empty() {
let expected_keys: Vec<String> = field_plans
.keys()
.map(format_literal_key_for_error)
.collect();
let mut literal_fields: Vec<(Value, &ExprRef)> = Vec::new();
let mut actual_keys: Vec<String> = Vec::new();
for (_, key_expr, value_expr) in fields {
if let Some(key_value) = extract_literal_key(key_expr) {
actual_keys.push(format_literal_key_for_error(&key_value));
literal_fields.push((key_value, value_expr));
}
}
let mut expected_sorted = expected_keys.clone();
expected_sorted.sort();
let mut actual_sorted = actual_keys.clone();
actual_sorted.sort();
if expected_sorted != actual_sorted {
return LiteralStructureCheck::ObjectMismatch {
expected: expected_keys,
actual: actual_keys,
span: expr.span().clone(),
};
}
for (key_value, value_expr) in literal_fields {
if let Some(field_plan) = field_plans.get(&key_value) {
match check_literal_structure(field_plan, value_expr) {
LiteralStructureCheck::Match => {}
LiteralStructureCheck::Unknown => {
return LiteralStructureCheck::Unknown
}
mismatch => return mismatch,
}
}
}
if dynamic_fields.is_empty() {
return LiteralStructureCheck::Match;
}
}
LiteralStructureCheck::Unknown
}
_ => LiteralStructureCheck::Unknown,
}
}
pub(crate) fn ensure_literal_match(plan: &DestructuringPlan, expr: &ExprRef) -> Result<()> {
match check_literal_structure(plan, expr).into_error() {
Some(err) => Err(err),
None => Ok(()),
}
}
pub(crate) fn collect_pattern_var_spans(expr: &ExprRef, spans: &mut Vec<Span>) {
match expr.as_ref() {
Expr::Var { span, .. } => {
let name = span.text();
if name != "_" && name != "input" && name != "data" {
spans.push(span.clone());
}
}
Expr::Array { items, .. } => {
for item in items {
collect_pattern_var_spans(item, spans);
}
}
Expr::Set { items, .. } => {
for item in items {
collect_pattern_var_spans(item, spans);
}
}
Expr::Object { fields, .. } => {
for (_, _, value_expr) in fields {
collect_pattern_var_spans(value_expr, spans);
}
}
_ => {}
}
}
pub(crate) fn validate_pattern_bindings<T: VariableBindingContext>(
expr: &ExprRef,
newly_bound: &BTreeSet<String>,
context: &T,
) -> Result<()> {
let mut candidate_vars = Vec::new();
collect_pattern_var_spans(expr, &mut candidate_vars);
for span in candidate_vars {
let name = span.text().to_string();
if newly_bound.contains(&name) {
continue;
}
if !context.is_var_unbound(&name, ScopingMode::RespectParent) {
return Err(BindingPlannerError::VariableAlreadyDefined { var: name, span });
}
}
Ok(())
}
pub(crate) fn collect_plan_var_spans(plan: &DestructuringPlan, spans: &mut Vec<Span>) {
match plan {
DestructuringPlan::Var(span) => spans.push(span.clone()),
DestructuringPlan::Array { element_plans } => {
for nested in element_plans {
collect_plan_var_spans(nested, spans);
}
}
DestructuringPlan::Object {
field_plans,
dynamic_fields,
} => {
for nested in field_plans.values() {
collect_plan_var_spans(nested, spans);
}
for (_, nested) in dynamic_fields {
collect_plan_var_spans(nested, spans);
}
}
DestructuringPlan::Ignore
| DestructuringPlan::EqualityExpr(_)
| DestructuringPlan::EqualityValue(_) => {}
}
}
pub(crate) fn ensure_structural_compatibility(
lhs_expr: &ExprRef,
rhs_expr: &ExprRef,
) -> Result<()> {
let lhs_is_array = matches!(lhs_expr.as_ref(), Expr::Array { .. });
let rhs_is_array = matches!(rhs_expr.as_ref(), Expr::Array { .. });
let lhs_is_object = matches!(lhs_expr.as_ref(), Expr::Object { .. });
let rhs_is_object = matches!(rhs_expr.as_ref(), Expr::Object { .. });
if (lhs_is_array && rhs_is_object) || (lhs_is_object && rhs_is_array) {
return Err(BindingPlannerError::IncompatibleDestructuringPatterns {
span: lhs_expr.span().clone(),
});
}
Ok(())
}
/// Helper that discards destructuring plans which do not bind any variables.
pub(crate) fn plan_only_if_binds(plan: Option<DestructuringPlan>) -> Option<DestructuringPlan> {
plan.and_then(|plan| {
if plan.introduces_binding() || plan.contains_wildcards() {
Some(plan)
} else {
None
}
})
}
pub(crate) fn extract_literal_key(expr: &ExprRef) -> Option<Value> {
match expr.as_ref() {
Expr::String { value, .. } => Some(value.clone()),
Expr::RawString { value, .. } => Some(value.clone()),
Expr::Number { value, .. } => Some(value.clone()),
Expr::Bool { value, .. } => Some(value.clone()),
Expr::Null { .. } => Some(Value::Null),
_ => None,
}
}
pub(crate) fn format_literal_key_for_error(value: &Value) -> String {
match value {
Value::String(s) => s.as_ref().to_string(),
_ => value.to_string(),
}
}

View File

@@ -7,14 +7,66 @@
//! pre-computing loop hoisting information that can be stored in the
//! compiled policy and reused by the interpreter.
use super::destructuring_planner::{
map_binding_error, BindingPlan, ScopingMode, VariableBindingContext,
};
use crate::ast::{Expr, ExprRef, Literal, LiteralStmt, Module, Query, Ref, Rule, RuleHead};
use crate::compiler::context::{ContextType, ScopeContext};
use crate::lookup::Lookup;
use crate::scheduler::compute_module_globals;
use crate::*;
use anyhow::Result;
use anyhow::{anyhow, Result};
use alloc::collections::BTreeSet;
use alloc::vec::Vec;
/// Implementation of VariableBindingContext for ScopeContext
impl VariableBindingContext for ScopeContext {
fn is_var_unbound(&self, var_name: &str, scoping: ScopingMode) -> bool {
if var_name == "_" {
return false;
}
if self.unbound_vars.contains(var_name) {
return true;
}
if self.has_scheduler_scope && self.local_vars.contains(var_name) {
return true;
}
match scoping {
ScopingMode::AllowShadowing => {
// Allow shadowing - always consider variables as potentially unbound
true
}
ScopingMode::RespectParent => {
// Respect parent scope bindings
if self
.module_globals
.as_ref()
.is_some_and(|globals| globals.contains(var_name))
{
return false;
}
if self.bound_vars.contains(var_name) {
return false;
}
true
}
}
}
fn has_same_scope_binding(&self, var_name: &str) -> bool {
if var_name == "_" {
return false;
}
self.current_scope_bound_vars.contains(var_name)
}
}
/// Type of loop that was hoisted
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoopType {
@@ -54,6 +106,10 @@ pub struct HoistedLoopsLookup {
/// For output expressions in comprehensions and rule values
expr_loops: Lookup<Vec<HoistedLoop>>,
/// Maps (module_index, expr_index) -> BindingPlan
/// Stores pre-computed binding plans for assignment-style expressions
expr_binding_plans: Lookup<BindingPlan>,
/// Maps (module_index, query_index) -> ScopeContext
/// Stores compilation contexts for queries (rules, comprehensions, every)
query_contexts: Lookup<ScopeContext>,
@@ -69,6 +125,7 @@ impl HoistedLoopsLookup {
pub fn ensure_statement_capacity(&mut self, module_idx: u32, stmt_idx: u32) {
self.statement_loops.ensure_capacity(module_idx, stmt_idx);
self.expr_loops.ensure_capacity(module_idx, 0);
self.expr_binding_plans.ensure_capacity(module_idx, 0);
self.query_contexts.ensure_capacity(module_idx, 0);
}
@@ -76,6 +133,8 @@ impl HoistedLoopsLookup {
pub fn ensure_expr_capacity(&mut self, module_idx: u32, expr_idx: u32) {
self.expr_loops.ensure_capacity(module_idx, expr_idx);
self.statement_loops.ensure_capacity(module_idx, 0);
self.expr_binding_plans
.ensure_capacity(module_idx, expr_idx);
self.query_contexts.ensure_capacity(module_idx, 0);
}
@@ -84,6 +143,7 @@ impl HoistedLoopsLookup {
self.query_contexts.ensure_capacity(module_idx, query_idx);
self.statement_loops.ensure_capacity(module_idx, 0);
self.expr_loops.ensure_capacity(module_idx, 0);
self.expr_binding_plans.ensure_capacity(module_idx, 0);
}
/// Store hoisted loops for a statement
@@ -111,11 +171,22 @@ impl HoistedLoopsLookup {
self.query_contexts.set(module_idx, query_idx, context);
}
/// Store a binding plan for an expression
pub fn set_expr_binding_plan(&mut self, module_idx: u32, expr_idx: u32, plan: BindingPlan) {
self.expr_binding_plans.set(module_idx, expr_idx, plan);
}
/// Get the compilation context for a query
#[allow(dead_code)]
pub fn get_query_context(&self, module_idx: u32, query_idx: u32) -> Option<&ScopeContext> {
self.query_contexts.get_checked(module_idx, query_idx)
}
/// Get the binding plan for an expression
pub fn get_expr_binding_plan(&self, module_idx: u32, expr_idx: u32) -> Option<&BindingPlan> {
self.expr_binding_plans.get_checked(module_idx, expr_idx)
}
/// Merge another loop hoisting table into this one
/// This is used to add query module loops to the existing table
pub fn merge_query_loops(&mut self, mut other: HoistedLoopsLookup, module_idx: usize) {
@@ -127,6 +198,10 @@ impl HoistedLoopsLookup {
self.expr_loops.push_module(Vec::new());
}
while self.expr_binding_plans.module_len() < module_idx {
self.expr_binding_plans.push_module(Vec::new());
}
while self.query_contexts.module_len() < module_idx {
self.query_contexts.push_module(Vec::new());
}
@@ -141,6 +216,10 @@ impl HoistedLoopsLookup {
self.expr_loops.push_module(module);
}
if let Some(module) = other.expr_binding_plans.remove_module(query_module_idx) {
self.expr_binding_plans.push_module(module);
}
if let Some(module) = other.query_contexts.remove_module(query_module_idx) {
self.query_contexts.push_module(module);
}
@@ -149,6 +228,7 @@ impl HoistedLoopsLookup {
pub fn truncate_modules(&mut self, module_count: usize) {
self.statement_loops.truncate_modules(module_count);
self.expr_loops.truncate_modules(module_count);
self.expr_binding_plans.truncate_modules(module_count);
self.query_contexts.truncate_modules(module_count);
}
@@ -163,6 +243,7 @@ impl HoistedLoopsLookup {
pub struct LoopHoister {
lookup: HoistedLoopsLookup,
schedule: Option<crate::Rc<crate::scheduler::Schedule>>,
module_globals: Lookup<crate::Rc<BTreeSet<String>>>,
}
impl LoopHoister {
@@ -171,6 +252,7 @@ impl LoopHoister {
Self {
lookup: HoistedLoopsLookup::new(),
schedule: None,
module_globals: Lookup::new(),
}
}
@@ -179,18 +261,30 @@ impl LoopHoister {
Self {
lookup: HoistedLoopsLookup::new(),
schedule: Some(schedule),
module_globals: Lookup::new(),
}
}
/// Populate loop hoisting information for all modules
/// Returns the populated lookup table
pub fn populate(mut self, modules: &[Ref<Module>]) -> Result<HoistedLoopsLookup> {
self.module_globals = compute_module_globals(modules).map_err(|err| anyhow!(err))?;
for (module_idx, module) in modules.iter().enumerate() {
self.populate_module(module_idx as u32, module)?;
}
Ok(self.lookup)
}
fn create_scope_context(&self, module_idx: u32) -> ScopeContext {
let mut context = ScopeContext::new();
if let Some(globals) = self.module_globals.get_checked(module_idx, 0) {
context.module_globals = Some(globals.clone());
}
context
}
/// Populate loop hoisting information for all modules, with extra capacity
/// for additional modules that will be added later (e.g., query modules)
///
@@ -202,6 +296,7 @@ impl LoopHoister {
modules: &[Ref<Module>],
extra_capacity: u32,
) -> Result<HoistedLoopsLookup> {
self.module_globals = compute_module_globals(modules).map_err(|err| anyhow!(err))?;
for (module_idx, module) in modules.iter().enumerate() {
self.populate_module(module_idx as u32, module)?;
}
@@ -212,6 +307,9 @@ impl LoopHoister {
self.lookup
.ensure_statement_capacity(last_module_idx + i, 0);
self.lookup.ensure_expr_capacity(last_module_idx + i, 0);
self.module_globals.ensure_capacity(last_module_idx + i, 0);
self.module_globals
.set(last_module_idx + i, 0, crate::Rc::new(BTreeSet::new()));
}
Ok(self.lookup)
}
@@ -257,10 +355,18 @@ impl LoopHoister {
.ensure_expr_capacity(module_idx, num_expressions - 1);
}
self.module_globals.ensure_capacity(module_idx, 0);
let mut reserved_globals = BTreeSet::new();
reserved_globals.insert("data".to_string());
reserved_globals.insert("input".to_string());
self.module_globals
.set(module_idx, 0, crate::Rc::new(reserved_globals));
// Populate the query with default context
let context = ScopeContext::new();
let context = self.create_scope_context(module_idx);
self.lookup.ensure_query_capacity(module_idx, query.qidx);
self.populate_query(module_idx, query, &context).map(|_| ())
self.populate_query(module_idx, query, &context)?;
Ok(())
}
/// Populate loop information for a single rule
@@ -268,11 +374,31 @@ impl LoopHoister {
match rule {
Rule::Spec { head, bodies, .. } => {
// Create a context for this rule
let mut context = ScopeContext::new();
let mut context = self.create_scope_context(module_idx);
// Bind function parameters if this is a function rule
if let RuleHead::Func { args, .. } = head {
for param in args {
// Create binding plan for function parameter
match super::destructuring_planner::create_parameter_binding_plan(
param, &context,
) {
Ok(binding_plan) => {
let expr_idx = param.as_ref().eidx();
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
// Immediately bind variables from the plan to context
Self::bind_vars_from_plan_to_context(&binding_plan, &mut context);
self.lookup.set_expr_binding_plan(
module_idx,
expr_idx,
binding_plan,
);
}
Err(err) => return Err(map_binding_error(err)),
}
// Extract variable name from parameter expression
if let Expr::Var { span, .. } = param.as_ref() {
context.bind_variable(span.text());
@@ -309,11 +435,13 @@ impl LoopHoister {
// Process each rule body (definitions)
for body in bodies {
// Create a context with the output expressions (using Rule context type)
let body_context = context.child_with_output_exprs(
let mut body_context = context.child_with_output_exprs(
ContextType::Rule,
key_expr.clone(),
value_expr.clone(),
);
body_context.current_scope_bound_vars =
context.current_scope_bound_vars.clone();
// Store the context for this query
let populated_body_context =
@@ -348,11 +476,13 @@ impl LoopHoister {
// Handle rules with head assignments but no bodies (e.g., `y := "string"`)
if bodies.is_empty() {
let body_context = context.child_with_output_exprs(
let mut body_context = context.child_with_output_exprs(
ContextType::Rule,
key_expr.clone(),
value_expr.clone(),
);
body_context.current_scope_bound_vars =
context.current_scope_bound_vars.clone();
if let Some(ref key) = key_expr {
self.populate_output_expr(module_idx, key, &body_context)?;
@@ -365,7 +495,7 @@ impl LoopHoister {
}
Rule::Default { value, .. } => {
// For default rules, just process the value expression
let context = ScopeContext::new();
let context = self.create_scope_context(module_idx);
self.populate_output_expr(module_idx, value, &context)?;
}
}
@@ -380,7 +510,8 @@ impl LoopHoister {
query: &Query,
parent_context: &ScopeContext,
) -> Result<ScopeContext> {
let mut context = parent_context.child();
let mut context = parent_context.clone();
context.current_scope_bound_vars = parent_context.current_scope_bound_vars.clone();
// Get the scheduled order if available
let stmt_order: Vec<usize> = if let Some(ref schedule) = self.schedule {
@@ -423,93 +554,26 @@ impl LoopHoister {
}
}
// Traverse literal expressions to populate nested contexts (comprehensions, every, etc.)
self.process_literal_for_contexts(module_idx, &stmt.literal, context)?;
let mut loops = Vec::new();
self.analyze_literal(module_idx, &stmt.literal, context, &mut loops)?;
for with_mod in &stmt.with_mods {
self.process_expr_for_contexts(module_idx, &with_mod.refr, context)?;
self.process_expr_for_contexts(module_idx, &with_mod.r#as, context)?;
self.analyze_expr(module_idx, &with_mod.refr, context, &mut loops)?;
self.analyze_expr(module_idx, &with_mod.r#as, context, &mut loops)?;
}
// Hoist loops from this statement using populated contexts
let loops =
self.hoist_loops_from_literal_with_context(module_idx, &stmt.literal, context)?;
// Always store in lookup table, even if no loops (store empty vec)
// This ensures the interpreter can always find an entry
self.lookup.ensure_statement_capacity(module_idx, stmt_idx);
self.lookup.set_statement_loops(module_idx, stmt_idx, loops);
// Update context based on variable bindings in this statement
self.update_context_from_literal(&stmt.literal, context);
Ok(())
}
/// Hoist loops from a literal with variable binding context
fn hoist_loops_from_literal_with_context(
&self,
module_idx: u32,
literal: &Literal,
context: &ScopeContext,
) -> Result<Vec<HoistedLoop>> {
let mut loops = Vec::new();
use Literal::*;
match literal {
SomeIn {
key,
value,
collection,
..
} => {
// Recursively hoist from sub-expressions first
if let Some(key) = key {
self.hoist_loops_from_expr_with_context(module_idx, key, &mut loops, context)?;
}
self.hoist_loops_from_expr_with_context(module_idx, value, &mut loops, context)?;
self.hoist_loops_from_expr_with_context(
module_idx, collection, &mut loops, context,
)?;
}
Expr { expr, .. } => {
// Hoist loops from expressions (like array[_] patterns)
self.hoist_loops_from_expr_with_context(module_idx, expr, &mut loops, context)?;
}
Every { domain, query, .. } => {
// Hoist from domain expression
self.hoist_loops_from_expr_with_context(module_idx, domain, &mut loops, context)?;
// Process the Every query in a child context
let child_context = self
.lookup
.get_query_context(module_idx, query.qidx)
.cloned()
.unwrap_or_else(|| context.child());
for stmt in &query.stmts {
self.hoist_loops_from_literal_with_context(
module_idx,
&stmt.literal,
&child_context,
)?;
}
}
NotExpr { expr, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, expr, &mut loops, context)?;
}
_ => {
// Other literal types don't have loops to hoist
}
}
Ok(loops)
}
/// Traverse literals to populate nested contexts (comprehensions, every, etc.)
fn process_literal_for_contexts(
fn analyze_literal(
&mut self,
module_idx: u32,
literal: &Literal,
context: &ScopeContext,
context: &mut ScopeContext,
loops: &mut Vec<HoistedLoop>,
) -> Result<()> {
use Literal::*;
@@ -520,31 +584,38 @@ impl LoopHoister {
collection,
..
} => {
let binding_plan = super::destructuring_planner::create_some_in_binding_plan(
key, value, collection, context,
)
.map_err(map_binding_error)?;
let expr_idx = collection.as_ref().eidx();
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
Self::bind_vars_from_plan_to_context(&binding_plan, context);
self.lookup
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
if let Some(key_expr) = key {
self.process_expr_for_contexts(module_idx, key_expr, context)?;
self.analyze_expr(module_idx, key_expr, context, loops)?;
}
self.process_expr_for_contexts(module_idx, value, context)?;
self.process_expr_for_contexts(module_idx, collection, context)?;
self.analyze_expr(module_idx, value, context, loops)?;
self.analyze_expr(module_idx, collection, context, loops)?;
}
Expr { expr, .. } | NotExpr { expr, .. } => {
self.process_expr_for_contexts(module_idx, expr, context)?;
Expr { expr, .. } => {
self.analyze_expr(module_idx, expr, context, loops)?;
}
Every { domain, query, .. } => {
// Process the domain expression for nested contexts
self.process_expr_for_contexts(module_idx, domain, context)?;
self.analyze_expr(module_idx, domain, context, loops)?;
// Create a child context for the Every quantifier
let every_context = context.child_with_output_exprs(ContextType::Every, None, None);
let populated_every_context =
let populated_context =
self.populate_query(module_idx, query.as_ref(), &every_context)?;
self.lookup.ensure_query_capacity(module_idx, query.qidx);
self.lookup.set_query_context(
module_idx,
query.qidx,
populated_every_context.clone(),
);
// Nested query already processed for hoisting via populated context
self.lookup
.set_query_context(module_idx, query.qidx, populated_context);
}
NotExpr { expr, .. } => {
self.analyze_expr(module_idx, expr, context, loops)?;
}
_ => {}
}
@@ -552,25 +623,31 @@ impl LoopHoister {
Ok(())
}
/// Traverse expressions to populate nested contexts (comprehensions, function params, etc.)
fn process_expr_for_contexts(
fn analyze_expr(
&mut self,
module_idx: u32,
expr: &ExprRef,
context: &ScopeContext,
context: &mut ScopeContext,
loops: &mut Vec<HoistedLoop>,
) -> Result<()> {
use crate::ast::Expr as E;
match expr.as_ref() {
E::String { .. }
| E::RawString { .. }
| E::Number { .. }
| E::Bool { .. }
| E::Null { .. }
| E::Var { .. } => {}
E::Array { items, .. } | E::Set { items, .. } => {
for item in items {
self.process_expr_for_contexts(module_idx, item, context)?;
self.analyze_expr(module_idx, item, context, loops)?;
}
}
E::Object { fields, .. } => {
for (_, key_expr, value_expr) in fields {
self.process_expr_for_contexts(module_idx, key_expr, context)?;
self.process_expr_for_contexts(module_idx, value_expr, context)?;
self.analyze_expr(module_idx, key_expr, context, loops)?;
self.analyze_expr(module_idx, value_expr, context, loops)?;
}
}
E::ArrayCompr { term, query, .. } | E::SetCompr { term, query, .. } => {
@@ -579,17 +656,12 @@ impl LoopHoister {
None,
Some(term.clone()),
);
let populated_compr_context =
let populated_context =
self.populate_query(module_idx, query.as_ref(), &compr_context)?;
self.lookup.ensure_query_capacity(module_idx, query.qidx);
self.lookup.set_query_context(
module_idx,
query.qidx,
populated_compr_context.clone(),
);
self.populate_output_expr(module_idx, term, &populated_compr_context)?;
self.lookup
.set_query_context(module_idx, query.qidx, populated_context.clone());
self.populate_output_expr_with_context(module_idx, term, &populated_context)?;
}
E::ObjectCompr {
key, value, query, ..
@@ -599,154 +671,21 @@ impl LoopHoister {
Some(key.clone()),
Some(value.clone()),
);
let populated_compr_context =
let populated_context =
self.populate_query(module_idx, query.as_ref(), &compr_context)?;
self.lookup.ensure_query_capacity(module_idx, query.qidx);
self.lookup.set_query_context(
module_idx,
query.qidx,
populated_compr_context.clone(),
);
self.populate_output_expr(module_idx, key, &populated_compr_context)?;
self.populate_output_expr(module_idx, value, &populated_compr_context)?;
self.lookup
.set_query_context(module_idx, query.qidx, populated_context.clone());
self.populate_output_expr_with_context(module_idx, key, &populated_context)?;
self.populate_output_expr_with_context(module_idx, value, &populated_context)?;
}
E::Call { fcn, params, .. } => {
self.process_expr_for_contexts(module_idx, fcn, context)?;
self.analyze_expr(module_idx, fcn, context, loops)?;
for param in params {
self.process_expr_for_contexts(module_idx, param, context)?;
}
}
E::UnaryExpr { expr, .. } => {
self.process_expr_for_contexts(module_idx, expr, context)?;
}
E::RefDot { refr, .. } => {
self.process_expr_for_contexts(module_idx, refr, context)?;
}
E::RefBrack { refr, index, .. } => {
self.process_expr_for_contexts(module_idx, refr, context)?;
self.process_expr_for_contexts(module_idx, index, context)?;
}
E::BinExpr { lhs, rhs, .. }
| E::BoolExpr { lhs, rhs, .. }
| E::ArithExpr { lhs, rhs, .. } => {
self.process_expr_for_contexts(module_idx, lhs, context)?;
self.process_expr_for_contexts(module_idx, rhs, context)?;
}
E::AssignExpr { lhs, rhs, .. } => {
self.process_expr_for_contexts(module_idx, lhs, context)?;
self.process_expr_for_contexts(module_idx, rhs, context)?;
}
E::Membership {
key,
value,
collection,
..
} => {
if let Some(key_expr) = key {
self.process_expr_for_contexts(module_idx, key_expr, context)?;
}
self.process_expr_for_contexts(module_idx, value, context)?;
self.process_expr_for_contexts(module_idx, collection, context)?;
}
#[cfg(feature = "rego-extensions")]
E::OrExpr { lhs, rhs, .. } => {
self.process_expr_for_contexts(module_idx, lhs, context)?;
self.process_expr_for_contexts(module_idx, rhs, context)?;
}
_ => {}
}
Ok(())
}
/// Hoist loops from expressions with variable binding context
fn hoist_loops_from_expr_with_context(
&self,
module_idx: u32,
expr: &ExprRef,
loops: &mut Vec<HoistedLoop>,
context: &ScopeContext,
) -> Result<()> {
use Expr::*;
match expr.as_ref() {
// Primitive types - no loops to hoist
String { .. }
| RawString { .. }
| Number { .. }
| Bool { .. }
| Null { .. }
| Var { .. } => {
// No sub-expressions to process
}
// Collection types - hoist from items
Array { items, .. } => {
for item in items {
self.hoist_loops_from_expr_with_context(module_idx, item, loops, context)?;
}
}
Set { items, .. } => {
for item in items {
self.hoist_loops_from_expr_with_context(module_idx, item, loops, context)?;
}
}
Object { fields, .. } => {
for (_, key_expr, value_expr) in fields {
self.hoist_loops_from_expr_with_context(module_idx, key_expr, loops, context)?;
self.hoist_loops_from_expr_with_context(
module_idx, value_expr, loops, context,
)?;
}
}
// Comprehensions - process their queries
// Note: Comprehension contexts and output expressions will be handled
// by populate_comprehension called from the parent expression processing
ArrayCompr { term, query, .. } | SetCompr { term, query, .. } => {
let child_context = self
.lookup
.get_query_context(module_idx, query.qidx)
.cloned()
.unwrap_or_else(|| context.child());
for stmt in &query.stmts {
self.hoist_loops_from_literal_with_context(
module_idx,
&stmt.literal,
&child_context,
)?;
}
self.hoist_loops_from_expr_with_context(module_idx, term, loops, &child_context)?;
}
ObjectCompr {
key, value, query, ..
} => {
let child_context = self
.lookup
.get_query_context(module_idx, query.qidx)
.cloned()
.unwrap_or_else(|| context.child());
for stmt in &query.stmts {
self.hoist_loops_from_literal_with_context(
module_idx,
&stmt.literal,
&child_context,
)?;
}
self.hoist_loops_from_expr_with_context(module_idx, key, loops, &child_context)?;
self.hoist_loops_from_expr_with_context(module_idx, value, loops, &child_context)?;
}
// Function calls - check for walk() builtin which generates loops
Call { fcn, params, .. } => {
// First hoist loops in parameters.
for param in params {
self.hoist_loops_from_expr_with_context(module_idx, param, loops, context)?;
self.analyze_expr(module_idx, param, context, loops)?;
}
// Check if this is a walk() call
let is_walk = if let Var {
let is_walk = if let E::Var {
value: Value::String(name),
..
} = fcn.as_ref()
@@ -759,35 +698,55 @@ impl LoopHoister {
if is_walk {
loops.push(HoistedLoop {
loop_expr: Some(expr.clone()),
key: None, // walk doesn't have an index
key: None,
value: expr.clone(),
collection: expr.clone(), // The walk call itself
collection: expr.clone(),
loop_type: LoopType::Walk,
});
return Ok(());
}
// If the last parameter expression contains unbound vars, create a binding plan
if let Some(last_param) = params.last() {
match super::destructuring_planner::create_parameter_binding_plan(
last_param, context,
) {
Ok(binding_plan) => {
let expr_idx = last_param.as_ref().eidx();
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
// For other function calls, hoist loops in parameters
// Immediately bind variables from the plan to context
Self::bind_vars_from_plan_to_context(&binding_plan, context);
self.lookup
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
}
Err(err) => return Err(map_binding_error(err)),
}
}
}
// Unary expressions - hoist from operand
UnaryExpr { expr, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, expr, loops, context)?;
E::UnaryExpr { expr, .. } => {
self.analyze_expr(module_idx, expr, context, loops)?;
}
// Reference expressions - check for array[_] patterns
RefDot { refr, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, refr, loops, context)?;
E::RefDot { refr, .. } => {
self.analyze_expr(module_idx, refr, context, loops)?;
}
RefBrack { refr, index, .. } => {
// Recursively hoist from sub-expressions
self.hoist_loops_from_expr_with_context(module_idx, refr, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, index, loops, context)?;
E::RefBrack { refr, index, .. } => {
self.analyze_expr(module_idx, refr, context, loops)?;
self.analyze_expr(module_idx, index, context, loops)?;
// Check if the index expression contains unbound variables
// This handles both simple cases like array[x] and complex cases like array[[x, y]]
if Self::expr_contains_unbound_vars(index, context) {
// This index contains unbound variables - create a loop to iterate
match super::destructuring_planner::create_loop_index_binding_plan(
index, context,
) {
Ok(binding_plan) => {
let expr_idx = index.as_ref().eidx();
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
Self::bind_vars_from_plan_to_context(&binding_plan, context);
self.lookup
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
}
Err(err) => return Err(map_binding_error(err)),
}
loops.push(HoistedLoop {
loop_expr: Some(expr.clone()),
key: Some(index.clone()),
@@ -795,97 +754,59 @@ impl LoopHoister {
collection: refr.clone(),
loop_type: LoopType::IndexIteration,
});
return Ok(());
}
}
E::BinExpr { lhs, rhs, .. }
| E::BoolExpr { lhs, rhs, .. }
| E::ArithExpr { lhs, rhs, .. } => {
self.analyze_expr(module_idx, lhs, context, loops)?;
self.analyze_expr(module_idx, rhs, context, loops)?;
}
E::AssignExpr { op, lhs, rhs, .. } => {
let binding_plan = super::destructuring_planner::create_assignment_binding_plan(
op.clone(),
lhs,
rhs,
context,
)
.map_err(map_binding_error)?;
// Binary expressions - hoist from both operands
BinExpr { lhs, rhs, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
}
BoolExpr { lhs, rhs, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
}
ArithExpr { lhs, rhs, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
}
AssignExpr { lhs, rhs, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
}
let expr_idx = expr.as_ref().eidx();
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
Self::bind_vars_from_plan_to_context(&binding_plan, context);
self.lookup
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
// Membership expressions - hoist from key, value, and collection
Membership {
self.analyze_expr(module_idx, lhs, context, loops)?;
self.analyze_expr(module_idx, rhs, context, loops)?;
}
E::Membership {
key,
value,
collection,
..
} => {
if let Some(key_expr) = key {
self.hoist_loops_from_expr_with_context(module_idx, key_expr, loops, context)?;
self.analyze_expr(module_idx, key_expr, context, loops)?;
}
self.hoist_loops_from_expr_with_context(module_idx, value, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, collection, loops, context)?;
self.analyze_expr(module_idx, value, context, loops)?;
self.analyze_expr(module_idx, collection, context, loops)?;
}
// Handle conditionally compiled expression types
#[cfg(feature = "rego-extensions")]
OrExpr { lhs, rhs, .. } => {
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
E::OrExpr { lhs, rhs, .. } => {
self.analyze_expr(module_idx, lhs, context, loops)?;
self.analyze_expr(module_idx, rhs, context, loops)?;
}
}
Ok(())
}
/// Update context based on variable bindings in a literal
fn update_context_from_literal(&self, literal: &Literal, context: &mut ScopeContext) {
use crate::ast::Expr as E;
use Literal::*;
match literal {
SomeIn { key, value, .. } => {
// Bind the loop variables
if let Some(key_expr) = key {
if let E::Var { span, .. } = key_expr.as_ref() {
context.bind_variable(span.text());
}
}
if let E::Var { span, .. } = value.as_ref() {
context.bind_variable(span.text());
}
}
Expr { expr, .. } => {
// Look for assignment expressions that bind variables
if let E::AssignExpr { lhs, .. } = expr.as_ref() {
Self::bind_variables_from_expr(lhs, context);
}
}
_ => {}
}
}
/// Recursively bind variables from an expression (for assignments)
fn bind_variables_from_expr(expr: &ExprRef, context: &mut ScopeContext) {
use crate::ast::Expr as E;
match expr.as_ref() {
E::Var { span, .. } => {
context.bind_variable(span.text());
}
E::Array { items, .. } => {
for item in items {
Self::bind_variables_from_expr(item, context);
}
}
E::Object { fields, .. } => {
for (_, key_expr, value_expr) in fields {
Self::bind_variables_from_expr(key_expr, context);
Self::bind_variables_from_expr(value_expr, context);
}
}
_ => {}
/// Bind variables from a binding plan into the context
fn bind_vars_from_plan_to_context(binding_plan: &BindingPlan, context: &mut ScopeContext) {
let bound_vars = binding_plan.bound_vars();
for var in bound_vars {
context.bind_variable(&var);
}
}
@@ -916,18 +837,23 @@ impl LoopHoister {
expr: &ExprRef,
context: &ScopeContext,
) -> Result<()> {
let mut loops = Vec::new();
self.hoist_loops_from_expr_with_context(module_idx, expr, &mut loops, context)?;
self.populate_output_expr_with_context(module_idx, expr, context)
}
fn populate_output_expr_with_context(
&mut self,
module_idx: u32,
expr: &ExprRef,
context: &ScopeContext,
) -> Result<()> {
let mut loops = Vec::new();
let mut expr_context = context.clone();
self.analyze_expr(module_idx, expr, &mut expr_context, &mut loops)?;
// Always store expression loops, even if empty
// This ensures the interpreter can always find an entry
let expr_idx = expr.as_ref().eidx();
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
self.lookup.set_expr_loops(module_idx, expr_idx, loops);
// Traverse child expressions to populate any nested contexts (e.g., comprehensions)
self.process_expr_for_contexts(module_idx, expr, context)?;
Ok(())
}
}

View File

@@ -852,27 +852,24 @@ impl Engine {
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);
// Run loop hoisting for query snippet
let mut hoister = LoopHoister::new_with_schedule(query_schedule_rc.clone());
hoister.populate_query_snippet(
module_idx,
&query_node,
query_module.num_statements,
query_module.num_expressions,
)?;
let query_loops = hoister.finalize();
let query_lookup = 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
query_lookup
.get_statement_loops(module_idx, stmt.sidx)
.is_some(),
"missing hoisted loop entry for query statement index {}",
@@ -891,7 +888,7 @@ impl Engine {
"loop hoisting table should not retain extra modules before merge"
);
}
existing_table.merge_query_loops(query_loops, self.modules.len());
existing_table.merge_query_loops(query_lookup, self.modules.len());
#[cfg(debug_assertions)]
{
for stmt in &query_node.stmts {
@@ -952,8 +949,11 @@ impl Engine {
// Populate loop hoisting table for efficient evaluation
// Reserve capacity for 1 extra module (for query modules)
use crate::compiler::hoist::LoopHoister;
// Run loop hoisting pass first
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

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ mod lookup;
mod number;
mod parser;
mod policy_info;
mod query;
#[cfg(feature = "azure_policy")]
pub mod registry;
mod scheduler;

View File

@@ -489,4 +489,15 @@ mod test {
let n = Number::from(123456f64);
assert_eq!(format!("{}", n.format_decimal()), "123456");
}
#[test]
fn division_matches_high_precision_decimal() {
let one = Number::from(1u64);
let three = Number::from(3u64);
let div = one.divide(&three).unwrap();
let from_str: Number = "0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
.parse()
.unwrap();
assert_eq!(div, from_str);
}
}

1
src/query/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod traversal;

224
src/query/traversal.rs Normal file
View File

@@ -0,0 +1,224 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use anyhow::Result;
use crate::ast::Expr::{self, *};
use crate::ast::{AssignOp, ExprRef};
use crate::lexer::{SourceStr, Span};
use crate::value::Value;
#[derive(Clone, Default, Debug)]
pub struct Scope {
pub locals: BTreeMap<SourceStr, Span>,
pub unscoped: BTreeSet<SourceStr>,
pub inputs: BTreeSet<SourceStr>,
pub uses_input: bool,
}
pub fn traverse(expr: &ExprRef, f: &mut dyn FnMut(&ExprRef) -> Result<bool>) -> Result<()> {
if !f(expr)? {
return Ok(());
}
match expr.as_ref() {
Expr::String { .. }
| RawString { .. }
| Number { .. }
| Bool { .. }
| Null { .. }
| Var { .. } => (),
Array { items, .. } | Set { items, .. } => {
for item in items {
traverse(item, f)?;
}
}
Object { fields, .. } => {
for (_, key, value) in fields {
traverse(key, f)?;
traverse(value, f)?;
}
}
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => (),
Call { params, .. } => {
for param in params {
traverse(param, f)?;
}
}
UnaryExpr { expr, .. } => traverse(expr, f)?,
RefDot { refr, .. } => traverse(refr, f)?,
RefBrack { refr, index, .. } => {
traverse(refr, f)?;
traverse(index, f)?;
}
BinExpr { lhs, rhs, .. }
| BoolExpr { lhs, rhs, .. }
| ArithExpr { lhs, rhs, .. }
| AssignExpr { lhs, rhs, .. } => {
traverse(lhs, f)?;
traverse(rhs, f)?;
}
#[cfg(feature = "rego-extensions")]
OrExpr { lhs, rhs, .. } => {
traverse(lhs, f)?;
traverse(rhs, f)?;
}
Membership {
key,
value,
collection,
..
} => {
if let Some(key) = key.as_ref() {
traverse(key, f)?;
}
traverse(value, f)?;
traverse(collection, f)?;
}
}
Ok(())
}
pub fn var_exists(var: &Span, parent_scopes: &[Scope]) -> bool {
let name = var.source_str();
for scope in parent_scopes.iter().rev() {
if scope.unscoped.contains(&name) {
return true;
}
if let Some(span) = scope.locals.get(&name) {
if span.line <= var.line {
return true;
}
}
}
false
}
pub fn gather_assigned_vars(
expr: &ExprRef,
can_shadow: bool,
parent_scopes: &[Scope],
scope: &mut Scope,
) -> Result<()> {
traverse(expr, &mut |node| match node.as_ref() {
Var { span, .. } if matches!(span.text(), "_" | "input" | "data") => {
if span.text() == "input" {
scope.uses_input = true;
}
Ok(false)
}
Var { span, .. } if can_shadow => {
scope.locals.insert(span.source_str(), span.clone());
Ok(false)
}
Var { span, .. } if var_exists(span, parent_scopes) => {
scope.inputs.insert(span.source_str());
Ok(false)
}
Var { span, .. } => {
scope.unscoped.insert(span.source_str());
Ok(false)
}
Array { .. } | Object { .. } => Ok(true),
_ => Ok(false),
})
}
pub fn gather_input_vars(expr: &ExprRef, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
traverse(expr, &mut |node| match node.as_ref() {
Var { span, .. } => {
let name = span.source_str();
if name.text() == "input" {
scope.uses_input = true;
} else if !scope.unscoped.contains(&name) && var_exists(span, parent_scopes) {
scope.inputs.insert(name);
}
Ok(false)
}
_ => Ok(true),
})
}
pub fn gather_loop_vars(expr: &ExprRef, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
traverse(expr, &mut |node| match node.as_ref() {
Var { span, .. } if span.text() == "input" => {
scope.uses_input = true;
Ok(false)
}
RefBrack { index, .. } => {
gather_assigned_vars(index, false, parent_scopes, scope)?;
Ok(true)
}
_ => Ok(true),
})
}
pub fn gather_vars(
expr: &ExprRef,
can_shadow: bool,
parent_scopes: &[Scope],
scope: &mut Scope,
) -> Result<()> {
if let AssignExpr { op, lhs, rhs, .. } = expr.as_ref() {
gather_assigned_vars(lhs, *op == AssignOp::ColEq, parent_scopes, scope)?;
gather_assigned_vars(rhs, false, parent_scopes, scope)?;
} else {
gather_assigned_vars(expr, can_shadow, parent_scopes, scope)?;
}
gather_input_vars(expr, parent_scopes, scope)?;
gather_loop_vars(expr, parent_scopes, scope)
}
pub fn collect_expr_dependencies(expr: &ExprRef) -> Option<BTreeSet<String>> {
let mut deps = BTreeSet::new();
let mut valid = true;
if traverse(expr, &mut |node| match node.as_ref() {
Var { value, .. } => {
if let Value::String(name) = value {
let var = name.as_ref();
if var != "_" {
deps.insert(var.to_string());
}
}
Ok(false)
}
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => {
valid = false;
Ok(false)
}
#[cfg(feature = "rego-extensions")]
OrExpr { .. } => {
valid = false;
Ok(false)
}
_ => Ok(true),
})
.is_err()
{
return None;
}
if valid {
Some(deps)
} else {
None
}
}

View File

@@ -1,10 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::Expr::{Set, *};
use crate::ast::Expr::*;
use crate::ast::*;
use crate::lexer::*;
use crate::lookup::*;
pub use crate::query::traversal::Scope;
use crate::query::traversal::{
gather_assigned_vars, gather_input_vars, gather_loop_vars, gather_vars, traverse,
};
use crate::utils::*;
use crate::*;
@@ -208,205 +212,12 @@ pub fn schedule<Str: Clone + cmp::Ord + fmt::Debug>(
Ok(SortResult::Order(order))
}
#[derive(Clone, Default, Debug)]
pub struct Scope {
pub locals: BTreeMap<SourceStr, Span>,
pub unscoped: BTreeSet<SourceStr>,
pub inputs: BTreeSet<SourceStr>,
pub uses_input: bool,
}
#[derive(Clone, Default, Debug)]
pub struct QuerySchedule {
pub scope: Scope,
pub order: Vec<u16>,
}
pub fn traverse(expr: &Ref<Expr>, f: &mut dyn FnMut(&Ref<Expr>) -> Result<bool>) -> Result<()> {
if !f(expr)? {
return Ok(());
}
match expr.as_ref() {
Expr::String { .. }
| RawString { .. }
| Number { .. }
| Bool { .. }
| Null { .. }
| Var { .. } => (),
Array { items, .. } | Set { items, .. } => {
for i in items {
traverse(i, f)?;
}
}
Object { fields, .. } => {
for (_, k, v) in fields {
traverse(k, f)?;
traverse(v, f)?;
}
}
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => (),
Call { params, .. } => {
for p in params {
traverse(p, f)?;
}
}
UnaryExpr { expr, .. } => traverse(expr, f)?,
RefDot { refr, .. } => traverse(refr, f)?,
RefBrack { refr, index, .. } => {
traverse(refr, f)?;
traverse(index, f)?;
}
BinExpr { lhs, rhs, .. }
| BoolExpr { lhs, rhs, .. }
| ArithExpr { lhs, rhs, .. }
| AssignExpr { lhs, rhs, .. } => {
traverse(lhs, f)?;
traverse(rhs, f)?;
}
#[cfg(feature = "rego-extensions")]
OrExpr { lhs, rhs, .. } => {
traverse(lhs, f)?;
traverse(rhs, f)?;
}
Membership {
key,
value,
collection,
..
} => {
if let Some(key) = key.as_ref() {
traverse(key, f)?;
}
traverse(value, f)?;
traverse(collection, f)?;
}
}
Ok(())
}
fn var_exists(var: &Span, parent_scopes: &[Scope]) -> bool {
let name = var.source_str();
for pscope in parent_scopes.iter().rev() {
if pscope.unscoped.contains(&name) {
return true;
}
// Check parent scope vars defined using :=.
if let Some(s) = pscope.locals.get(&name) {
// Note: Since a rule cannot span multiple files, it is safe to check only
// the line numbers.
if s.line <= var.line {
// The variable was defined in parent scope prior to current comprehension.
return true;
}
}
}
false
}
fn gather_assigned_vars(
expr: &Ref<Expr>,
can_shadow: bool,
parent_scopes: &[Scope],
scope: &mut Scope,
) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
// Ignore _, input, data.
Var { span: v, .. } if matches!(v.text(), "_" | "input" | "data") => {
if v.text() == "input" {
scope.uses_input = true;
}
Ok(false)
}
// Record local var that can shadow input var.
Var { span: v, .. } if can_shadow => {
scope.locals.insert(v.source_str(), v.clone());
Ok(false)
}
// Record input vars.
Var { span: v, .. } if var_exists(v, parent_scopes) => {
scope.inputs.insert(v.source_str());
Ok(false)
}
// Record local var.
Var { span: v, .. } => {
scope.unscoped.insert(v.source_str());
Ok(false)
}
// TODO: key vs value for object binding
Array { .. } | Object { .. } => Ok(true),
_ => Ok(false),
})
}
fn gather_input_vars(expr: &Ref<Expr>, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
Var { span: v, .. } => {
let name = v.source_str();
if name.text() == "input" {
scope.uses_input = true;
} else if !scope.unscoped.contains(&name) && var_exists(v, parent_scopes) {
scope.inputs.insert(name);
}
Ok(false)
}
_ => Ok(true),
})
}
fn gather_loop_vars(expr: &Ref<Expr>, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
Var { span: v, .. } if v.text() == "input" => {
scope.uses_input = true;
Ok(false)
}
RefBrack { index, .. } => {
gather_assigned_vars(index, false, parent_scopes, scope)?;
Ok(true)
}
_ => Ok(true),
})
}
// TODO: start opa discussion
// k = "k"
// t = {"k": 5}
// {k:y} = t
// Try inlining value of t
fn gather_vars(
expr: &Ref<Expr>,
can_shadow: bool,
parent_scopes: &[Scope],
scope: &mut Scope,
) -> Result<()> {
// Process assignment expressions to gather vars that are defined/assigned
// in current scope.
if let AssignExpr { op, lhs, rhs, .. } = expr.as_ref() {
gather_assigned_vars(lhs, *op == AssignOp::ColEq, parent_scopes, scope)?;
gather_assigned_vars(rhs, false, parent_scopes, scope)?;
} else {
gather_assigned_vars(expr, can_shadow, parent_scopes, scope)?;
}
// Process all expressions to gather loop index vars and inputs.
// TODO: := assignment and use in same statement.
gather_input_vars(expr, parent_scopes, scope)?;
gather_loop_vars(expr, parent_scopes, scope)
}
pub struct Analyzer {
packages: BTreeMap<String, Scope>,
scopes: Vec<Scope>,
@@ -1011,7 +822,7 @@ impl Analyzer {
expr: &Ref<Expr>,
scope: &Scope,
_first_use: &BTreeMap<SourceStr, Span>,
vars: &mut Vec<SourceStr>,
vars: &mut Vec<(SourceStr, Span)>,
non_vars: &mut Vec<Ref<Expr>>,
) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
@@ -1021,7 +832,7 @@ impl Analyzer {
Ok(false)
}
Var { span: v, .. } if scope.locals.contains_key(&v.source_str()) => {
vars.push(v.source_str());
vars.push((v.source_str(), v.clone()));
Ok(false)
}
// TODO: Object key/value
@@ -1114,13 +925,17 @@ impl Analyzer {
)?;
// Add dependency between some-vars and vars used in collection.
for var in &some_vars {
for (var, _) in &some_vars {
definitions.push(Definition {
var: var.clone(),
used_vars: col_used_vars.clone(),
})
}
for (var, span) in &some_vars {
first_use.entry(var.clone()).or_insert(span.clone());
}
let mut used_vars = vec![];
for e in non_vars {
let mut definitions = vec![];
@@ -1296,3 +1111,71 @@ impl Analyzer {
Ok(())
}
}
/// Compute module globals for each module.
///
/// For each module, the globals are:
/// 1) The set of rule names defined in the package that the module defines
/// 2) Additionally, the set of aliases imported by the module
pub fn compute_module_globals(
modules: &[Ref<Module>],
) -> Result<Lookup<crate::Rc<BTreeSet<String>>>> {
let mut result = Lookup::new();
let mut packages: BTreeMap<String, crate::Rc<BTreeSet<String>>> = BTreeMap::new();
// First pass: collect all rule names by package
for m in modules {
let path = get_path_string(&m.package.refr, Some("data"))?;
let package_globals: &mut crate::Rc<BTreeSet<String>> = packages.entry(path).or_default();
for r in &m.policy {
let var = match r.as_ref() {
Rule::Default { refr, .. }
| Rule::Spec {
head:
RuleHead::Compr { refr, .. }
| RuleHead::Set { refr, .. }
| RuleHead::Func { refr, .. },
..
} => get_root_var(refr)?,
};
crate::Rc::make_mut(package_globals).insert(var.text().to_string());
}
}
// Second pass: for each module, combine package globals with module-specific imports
for (module_idx, m) in modules.iter().enumerate() {
let path = get_path_string(&m.package.refr, Some("data"))?;
let mut module_globals = packages.get(&path).cloned().unwrap_or_default();
// Add import aliases specific to this module
for import in &m.imports {
if let Some(var) = &import.r#as {
crate::Rc::make_mut(&mut module_globals).insert(var.text().to_string());
}
}
// Ensure reserved root documents are always treated as globals.
for &reserved in ["input", "data"].iter() {
crate::Rc::make_mut(&mut module_globals).insert(reserved.to_string());
}
// Reserved documents are always available in every module.
let reserved_docs = ["input", "data"];
for doc in reserved_docs {
crate::Rc::make_mut(&mut module_globals).insert(doc.to_string());
}
// Seed with reserved document roots that are always globally accessible.
{
let globals = crate::Rc::make_mut(&mut module_globals);
globals.insert("input".to_string());
globals.insert("data".to_string());
}
result.ensure_capacity(module_idx as u32, 0);
result.set(module_idx as u32, 0, module_globals);
}
Ok(result)
}

View File

@@ -253,9 +253,10 @@ pub fn eval_file(
query: &str,
enable_tracing: bool,
strict: bool,
v0: bool,
) -> Result<(Vec<Value>, Vec<String>)> {
let mut engine: Engine = Engine::new();
engine.set_rego_v0(true);
engine.set_rego_v0(v0);
engine.set_strict_builtin_errors(strict);
engine.set_gather_prints(true);
@@ -333,9 +334,10 @@ pub fn eval_file_with_rule_evaluation(
query: &str,
_enable_tracing: bool,
strict: bool,
v0: bool,
) -> Result<(Vec<Value>, Vec<String>)> {
let mut engine: Engine = Engine::new();
engine.set_rego_v0(true);
engine.set_rego_v0(v0);
engine.set_strict_builtin_errors(strict);
engine.set_gather_prints(true);
@@ -484,9 +486,19 @@ fn yaml_test_impl(file: &str) -> Result<()> {
}
}
}
#[cfg(not(feature = "graph"))]
{
// Skip tests that depend on graph builtin that need graph feature.
if file.contains("walk.yaml") {
std::println!("skipped {file} without graph feature.");
return Ok(());
}
}
std::println!("running {file}");
let v0 = !file.contains("bindings.yaml");
for case in test.cases {
std::print!("case {} ", case.note);
if case.skip == Some(true) {
@@ -516,6 +528,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
case.query.as_str(),
enable_tracing,
case.strict,
v0,
)
}
#[cfg(not(feature = "azure_policy"))]
@@ -530,6 +543,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
case.query.as_str(),
enable_tracing,
case.strict,
v0,
)
};