Files
regorus/src/compiler/destructuring_planner/assignment.rs
Anand Krishnamoorthi 86b4a279fa fix(ffi): eliminate aliasing UB + add Azure Policy JSON compilation FFI (#727)
* fix(ffi): eliminate aliasing UB via to_shared_ref migration

Add to_shared_ref() helper that creates &T (shared reference) from raw
pointers instead of &mut T. This eliminates undefined behavior caused by
violating Rust's aliasing invariant when C# SafeHandle permits concurrent
FFI calls on the same handle.

With &mut T, the compiler may assume exclusive (noalias) access and
reorder or elide reads/writes — a miscompilation risk when another thread
holds a reference to the same object. Switching to &T removes that
assumption; actual mutation is mediated by the interior RwLock inside
Handle<T>, which is the sole synchronization mechanism.

Migrated sites:
- rvm.rs: 20 non-drop call sites
- engine.rs: 30 non-drop call sites + with_unwind_guard for timer fns
- compiled_policy.rs: 2 call sites
- Fix null-data UB in regorus_program_deserialize_binary

Drop paths retain to_ref() where exclusive access is guaranteed by the
caller contract (preventing use-after-free).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(ffi): add Azure Policy JSON compilation FFI and C# bindings

- AliasRegistry builder pattern: RegorusAliasRegistryBuilder (mutable,
  single-threaded) + RegorusAliasRegistry (immutable, Arc-wrapped)
- Azure Policy JSON compilation: regorus_compile_azure_policy_rule and
  regorus_compile_azure_policy_definition with alias registry support
- regorus_rvm_set_context for host-supplied ambient data
- C# AliasRegistryBuilder and AliasRegistry classes with convenience
  factories (FromJson, FromManifest, Empty)
- C# AzurePolicyCompiler static class for policy rule/definition compilation
- Compile functions take *const RegorusAliasRegistry (read-only via
  to_shared_ref for concurrent compilation safety)
- Fix pre-existing clippy warnings across multiple crates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-22 12:50:16 -05:00

410 lines
14 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(
clippy::unreachable,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::unused_trait_names,
clippy::pattern_type_mismatch
)]
//! 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);
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)
}