mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
* 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>
410 lines
14 KiB
Rust
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)
|
|
}
|