mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Implement the full count loop compiler, replacing the stubs in count.rs, count_any.rs, and count_bindings.rs with a single consolidated module. Handles both field-based and value-based count nodes. Field counts walk the resource via resolve_alias_path then iterate the wildcard array; value counts operate on an arbitrary collection expression. For nested wildcard paths like A[*].B[*].C, the compiler emits recursive ForEach loops, drilling one wildcard level at a time. When an outer count binding already covers a prefix, the inner loop starts from the bound element register instead of re-walking from the resource root. Existence patterns (count > 0, count == 0) are recognized and lowered to LoopMode::Any, which exits on the first match rather than counting every element. Count-binding resolution threads the current-element register through inner field references and current() calls so that nested conditions can address fields relative to the loop variable. Also fixes the bound_len arithmetic in conditions_wildcard.rs with a cleaner strip_prefix call, and removes the nested-wildcard bail in split_count_wildcard_path since the compiler now handles them.
1642 lines
61 KiB
Rust
1642 lines
61 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
#![allow(clippy::pattern_type_mismatch)]
|
|
|
|
//! `count` / `count.where` compilation and count-binding resolution.
|
|
|
|
use alloc::format;
|
|
use alloc::string::{String, ToString as _};
|
|
use alloc::vec::Vec;
|
|
|
|
use anyhow::{anyhow, bail, Result};
|
|
|
|
use crate::languages::azure_policy::ast::{
|
|
Condition, Constraint, CountNode, FieldKind, JsonValue, OperatorKind, ValueOrExpr,
|
|
};
|
|
use crate::rvm::instructions::{GuardMode, LoopMode, LoopStartParams, PolicyOp};
|
|
use crate::rvm::Instruction;
|
|
use crate::Value;
|
|
|
|
use super::core::{Compiler, CountBinding};
|
|
use super::utils::{split_count_wildcard_path, split_path_without_wildcards};
|
|
|
|
impl Compiler {
|
|
pub(super) fn compile_count(&mut self, count_node: &CountNode) -> Result<u8> {
|
|
self.observed_uses_count = true;
|
|
match count_node {
|
|
CountNode::Value {
|
|
span,
|
|
value,
|
|
name,
|
|
where_,
|
|
} => {
|
|
let collection_reg = self.compile_value_or_expr(value, span)?;
|
|
self.compile_count_loop(
|
|
collection_reg,
|
|
name.as_ref().map(|n| n.name.clone()),
|
|
None,
|
|
where_.as_deref(),
|
|
span,
|
|
)
|
|
}
|
|
CountNode::Field {
|
|
span,
|
|
field,
|
|
where_,
|
|
} => {
|
|
let field_path = self.extract_field_count_path(field, span)?;
|
|
let (_prefix, suffix) = split_count_wildcard_path(&field_path)
|
|
.map_err(|e| span.error(&e.to_string()))?;
|
|
|
|
// Multi-level wildcard (e.g. `A[*].B[*]`) → emit nested loops.
|
|
// If an outer count binding covers part of the path, start
|
|
// from the bound element instead of the resource root.
|
|
if suffix.as_ref().is_some_and(|s| s.contains("[*]")) {
|
|
if let Some(binding) = self.resolve_count_binding(&field_path)? {
|
|
if let Some(outer_prefix) = &binding.field_wildcard_prefix {
|
|
let lc_prefix = outer_prefix.to_ascii_lowercase();
|
|
let wildcard_dot = format!("{}[*].", lc_prefix);
|
|
if let Some(inner_path) =
|
|
field_path.to_ascii_lowercase().strip_prefix(&wildcard_dot)
|
|
{
|
|
let inner_path = inner_path.to_string();
|
|
return self.compile_count_nested(
|
|
Some(binding.current_reg),
|
|
&inner_path,
|
|
where_.as_deref(),
|
|
outer_prefix,
|
|
span,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return self.compile_count_nested(
|
|
None,
|
|
&field_path,
|
|
where_.as_deref(),
|
|
"",
|
|
span,
|
|
);
|
|
}
|
|
|
|
// Single wildcard → existing path via resolve + single count loop.
|
|
let (collection_reg, prefix) = self.resolve_count_field_collection(field, span)?;
|
|
self.compile_count_loop(collection_reg, None, Some(prefix), where_.as_deref(), span)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve the collection register and wildcard prefix for a field-based
|
|
/// count node, handling nested count bindings.
|
|
fn resolve_count_field_collection(
|
|
&mut self,
|
|
field: &crate::languages::azure_policy::ast::FieldNode,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<(u8, String)> {
|
|
let field_path = self.extract_field_count_path(field, span)?;
|
|
let (collection_prefix, suffix) =
|
|
split_count_wildcard_path(&field_path).map_err(|e| span.error(&e.to_string()))?;
|
|
|
|
// Check if this field path is relative to an outer count binding.
|
|
if let Some(binding) = self.resolve_count_binding(&field_path)? {
|
|
if let Some(outer_prefix) = &binding.field_wildcard_prefix {
|
|
let lc_prefix = outer_prefix.to_ascii_lowercase();
|
|
let wildcard_dot = format!("{}[*].", lc_prefix);
|
|
if let Some(inner_path) =
|
|
field_path.to_ascii_lowercase().strip_prefix(&wildcard_dot)
|
|
{
|
|
let inner_path = inner_path.to_string();
|
|
if inner_path.contains("[*]") {
|
|
let (inner_collection, _) = split_count_wildcard_path(&inner_path)
|
|
.map_err(|e| span.error(&e.to_string()))?;
|
|
let inner_collection = inner_collection.to_ascii_lowercase();
|
|
let parts = split_path_without_wildcards(&inner_collection)?;
|
|
let refs = parts.iter().map(String::as_str).collect::<Vec<_>>();
|
|
let collection_reg =
|
|
self.emit_chained_index_literal_path(binding.current_reg, &refs, span)?;
|
|
let inner_prefix = format!("{}[*].{}", lc_prefix, inner_collection);
|
|
return Ok((collection_reg, inner_prefix));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Multi-level wildcard: now handled by compile_count_nested in compile_count.
|
|
// (Single-wildcard paths fall through to here.)
|
|
if suffix.as_ref().is_some_and(|s| s.contains("[*]")) {
|
|
bail!(span.error(&format!(
|
|
"multi-wildcard path should have been handled before resolve_count_field_collection: {}",
|
|
field_path
|
|
)));
|
|
}
|
|
|
|
let collection_reg = self.compile_resource_path_value(&collection_prefix, span)?;
|
|
Ok((collection_reg, collection_prefix))
|
|
}
|
|
|
|
/// Compile a multi-wildcard count path as nested loops.
|
|
///
|
|
/// Each intermediate `[*]` level emits a `ForEach` loop that accumulates
|
|
/// the inner count. The innermost `[*]` emits the real count loop with
|
|
/// the where clause and binding.
|
|
///
|
|
/// * `base_reg` — `None` for resource root, `Some` when inside an outer loop.
|
|
/// * `remaining_path` — the portion of the field path still to process;
|
|
/// must contain at least one `[*]`.
|
|
/// * `where_clause` — the optional where constraint (applied only at the
|
|
/// innermost level).
|
|
/// * `accumulated_prefix` — the path prefix accumulated from outer levels,
|
|
/// used to build binding prefixes.
|
|
fn compile_count_nested(
|
|
&mut self,
|
|
base_reg: Option<u8>,
|
|
remaining_path: &str,
|
|
where_clause: Option<&Constraint>,
|
|
accumulated_prefix: &str,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<u8> {
|
|
let (collection_part, suffix) =
|
|
split_count_wildcard_path(remaining_path).map_err(|e| span.error(&e.to_string()))?;
|
|
let has_more_wildcards = suffix.as_ref().is_some_and(|s| s.contains("[*]"));
|
|
|
|
// Build the binding prefix for this level.
|
|
let binding_prefix = if accumulated_prefix.is_empty() {
|
|
collection_part.clone()
|
|
} else {
|
|
format!("{}[*].{}", accumulated_prefix, collection_part)
|
|
};
|
|
|
|
// Lowercase the collection path to match normalized resource keys.
|
|
let collection_lower = collection_part.to_ascii_lowercase();
|
|
|
|
// Navigate to the collection. `split_count_wildcard_path` guarantees
|
|
// the collection segment before `[*]` is non-empty.
|
|
let collection_reg = match base_reg {
|
|
Some(base) => {
|
|
let parts = split_path_without_wildcards(&collection_lower)?;
|
|
let refs = parts.iter().map(String::as_str).collect::<Vec<_>>();
|
|
self.emit_chained_index_literal_path(base, &refs, span)?
|
|
}
|
|
None => self.compile_resource_path_value(&collection_lower, span)?,
|
|
};
|
|
|
|
if !has_more_wildcards {
|
|
// Innermost wildcard → delegate to the regular count loop.
|
|
// Optimization: if no where clause, just emit Count instruction.
|
|
// Note: Count returns Undefined for non-iterable collections,
|
|
// which differs from LoopMode::Any (treats them as empty). The
|
|
// existence-pattern optimizer (`try_compile_count_as_any`) skips
|
|
// nested-wildcard no-where counts so this path is always taken
|
|
// for that case, preserving Undefined-propagation semantics.
|
|
if where_clause.is_none() {
|
|
let dest = self.alloc_register()?;
|
|
self.emit(
|
|
Instruction::Count {
|
|
dest,
|
|
collection: collection_reg,
|
|
},
|
|
span,
|
|
);
|
|
return Ok(dest);
|
|
}
|
|
return self.compile_count_loop(
|
|
collection_reg,
|
|
None,
|
|
Some(binding_prefix),
|
|
where_clause,
|
|
span,
|
|
);
|
|
}
|
|
|
|
// Intermediate wildcard → ForEach loop that accumulates inner counts.
|
|
let count_reg = self.load_literal(Value::from(0_i64), span)?;
|
|
let key_reg = self.alloc_register()?;
|
|
let current_reg = self.alloc_register()?;
|
|
let loop_result_reg = self.alloc_register()?;
|
|
|
|
let params_index = self.program.add_loop_params(LoopStartParams {
|
|
mode: LoopMode::ForEach,
|
|
collection: collection_reg,
|
|
key_reg,
|
|
value_reg: current_reg,
|
|
result_reg: loop_result_reg,
|
|
body_start: 0,
|
|
loop_end: 0,
|
|
});
|
|
|
|
self.emit(Instruction::LoopStart { params_index }, span);
|
|
|
|
let body_start = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
// Push binding for this level so inner where-clause field references
|
|
// can resolve through this wildcard level.
|
|
self.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some(binding_prefix.clone()),
|
|
current_reg,
|
|
});
|
|
|
|
// Recurse for the inner level(s).
|
|
let suffix_ref = suffix
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow::anyhow!("suffix should be Some for nested count"))?;
|
|
let inner_count = self.compile_count_nested(
|
|
Some(current_reg),
|
|
suffix_ref,
|
|
where_clause,
|
|
&binding_prefix,
|
|
span,
|
|
)?;
|
|
|
|
// Accumulate inner count into outer count.
|
|
self.emit(
|
|
Instruction::Add {
|
|
dest: count_reg,
|
|
left: count_reg,
|
|
right: inner_count,
|
|
},
|
|
span,
|
|
);
|
|
|
|
self.count_bindings.pop();
|
|
|
|
self.emit(
|
|
Instruction::LoopNext {
|
|
body_start,
|
|
loop_end: 0,
|
|
},
|
|
span,
|
|
);
|
|
|
|
let loop_end = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
self.program.update_loop_params(params_index, |params| {
|
|
params.body_start = body_start;
|
|
params.loop_end = loop_end;
|
|
});
|
|
|
|
if let Some(Instruction::LoopNext { loop_end: le, .. }) =
|
|
self.program.instructions.last_mut()
|
|
{
|
|
*le = loop_end;
|
|
}
|
|
|
|
Ok(count_reg)
|
|
}
|
|
|
|
/// Compile a multi-wildcard count path as nested `Any` loops for the
|
|
/// `count > 0` / `count == 0` existence-pattern optimization.
|
|
///
|
|
/// Each intermediate `[*]` level emits an `Any` loop whose body is the
|
|
/// next level. The innermost `[*]` emits `compile_count_any_loop` with
|
|
/// the where clause. If `exists` is false the result is negated.
|
|
///
|
|
/// **Important:** This must only be called when `where_clause` is `Some`.
|
|
/// Without a where clause the non-optimized path (`compile_count_nested`)
|
|
/// uses `Instruction::Count` at the innermost level. That instruction
|
|
/// returns `Undefined` for missing/non-iterable collections, whereas the
|
|
/// `Any` loop treats them as empty (false). The difference changes the
|
|
/// semantics of `count == 0` from false (via Undefined propagation) to
|
|
/// true (via `Not(false)`). The caller (`try_compile_count_as_any`)
|
|
/// returns `None` for no-where nested counts so the generic count+compare
|
|
/// path is used instead.
|
|
fn compile_count_nested_any(
|
|
&mut self,
|
|
base_reg: Option<u8>,
|
|
remaining_path: &str,
|
|
where_clause: &Constraint,
|
|
accumulated_prefix: &str,
|
|
exists: bool,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<Option<u8>> {
|
|
let (collection_part, suffix) =
|
|
split_count_wildcard_path(remaining_path).map_err(|e| span.error(&e.to_string()))?;
|
|
let has_more_wildcards = suffix.as_ref().is_some_and(|s| s.contains("[*]"));
|
|
|
|
let binding_prefix = if accumulated_prefix.is_empty() {
|
|
collection_part.clone()
|
|
} else {
|
|
format!("{}[*].{}", accumulated_prefix, collection_part)
|
|
};
|
|
|
|
// Lowercase the collection path to match normalized resource keys.
|
|
let collection_lower = collection_part.to_ascii_lowercase();
|
|
|
|
// Navigate to the collection. `split_count_wildcard_path` guarantees
|
|
// the collection segment before `[*]` is non-empty.
|
|
let collection_reg = match base_reg {
|
|
Some(base) => {
|
|
let parts = split_path_without_wildcards(&collection_lower)?;
|
|
let refs = parts.iter().map(String::as_str).collect::<Vec<_>>();
|
|
self.emit_chained_index_literal_path(base, &refs, span)?
|
|
}
|
|
None => self.compile_resource_path_value(&collection_lower, span)?,
|
|
};
|
|
|
|
if !has_more_wildcards {
|
|
// Innermost wildcard → regular Any loop.
|
|
let any_result = self.compile_count_any_loop(
|
|
collection_reg,
|
|
None,
|
|
Some(binding_prefix),
|
|
Some(where_clause),
|
|
span,
|
|
)?;
|
|
return if exists {
|
|
Ok(Some(any_result))
|
|
} else {
|
|
let dest = self.alloc_register()?;
|
|
self.emit(
|
|
Instruction::PolicyCondition {
|
|
dest,
|
|
left: any_result,
|
|
right: 0,
|
|
op: PolicyOp::Not,
|
|
},
|
|
span,
|
|
);
|
|
Ok(Some(dest))
|
|
};
|
|
}
|
|
|
|
// Intermediate wildcard → Any loop wrapping inner nested Any.
|
|
let key_reg = self.alloc_register()?;
|
|
let current_reg = self.alloc_register()?;
|
|
let result_reg = self.alloc_register()?;
|
|
|
|
let params_index = self.program.add_loop_params(LoopStartParams {
|
|
mode: LoopMode::Any,
|
|
collection: collection_reg,
|
|
key_reg,
|
|
value_reg: current_reg,
|
|
result_reg,
|
|
body_start: 0,
|
|
loop_end: 0,
|
|
});
|
|
|
|
self.emit(Instruction::LoopStart { params_index }, span);
|
|
|
|
let body_start = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
// Push binding for this level.
|
|
self.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some(binding_prefix.clone()),
|
|
current_reg,
|
|
});
|
|
|
|
// Recurse — the inner call returns Some(result_reg) with the final
|
|
// negation already applied at the innermost level. For the outer
|
|
// Any loop, we need "any inner satisfies" so we pass `exists = true`
|
|
// here and handle the overall negation at the end.
|
|
let suffix_ref = suffix
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow::anyhow!("suffix should be Some for nested any"))?;
|
|
let inner = self
|
|
.compile_count_nested_any(
|
|
Some(current_reg),
|
|
suffix_ref,
|
|
where_clause,
|
|
&binding_prefix,
|
|
/* exists */ true,
|
|
span,
|
|
)?
|
|
.ok_or_else(|| anyhow::anyhow!("nested any should always return Some"))?;
|
|
|
|
// The outer Any body succeeds when the inner Any returned true.
|
|
self.emit(
|
|
Instruction::Guard {
|
|
register: inner,
|
|
mode: GuardMode::Condition,
|
|
},
|
|
span,
|
|
);
|
|
|
|
self.count_bindings.pop();
|
|
|
|
self.emit(
|
|
Instruction::LoopNext {
|
|
body_start,
|
|
loop_end: 0,
|
|
},
|
|
span,
|
|
);
|
|
|
|
let loop_end = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
self.program.update_loop_params(params_index, |params| {
|
|
params.body_start = body_start;
|
|
params.loop_end = loop_end;
|
|
});
|
|
|
|
if let Some(Instruction::LoopNext { loop_end: le, .. }) =
|
|
self.program.instructions.last_mut()
|
|
{
|
|
*le = loop_end;
|
|
}
|
|
|
|
// If !exists (count == 0), negate the Any result.
|
|
if exists {
|
|
Ok(Some(result_reg))
|
|
} else {
|
|
let dest = self.alloc_register()?;
|
|
self.emit(
|
|
Instruction::PolicyCondition {
|
|
dest,
|
|
left: result_reg,
|
|
right: 0,
|
|
op: PolicyOp::Not,
|
|
},
|
|
span,
|
|
);
|
|
Ok(Some(dest))
|
|
}
|
|
}
|
|
|
|
/// Map a [`FieldNode`] to the dotted property path used for count
|
|
/// resolution. Built-in field kinds (`type`, `id`, …) are returned
|
|
/// as-is; aliases go through [`resolve_alias_path`] which normalises
|
|
/// and lowercases when the alias catalog is loaded.
|
|
fn extract_field_count_path(
|
|
&self,
|
|
field: &crate::languages::azure_policy::ast::FieldNode,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<String> {
|
|
match &field.kind {
|
|
FieldKind::Type => Ok("type".to_string()),
|
|
FieldKind::Id => Ok("id".to_string()),
|
|
FieldKind::Kind => Ok("kind".to_string()),
|
|
FieldKind::Name => Ok("name".to_string()),
|
|
FieldKind::Location => Ok("location".to_string()),
|
|
FieldKind::FullName => Ok("fullName".to_string()),
|
|
FieldKind::IdentityType => Ok("identity.type".to_string()),
|
|
FieldKind::IdentityField(subpath) => {
|
|
Ok(format!("identity.{}", subpath.to_ascii_lowercase()))
|
|
}
|
|
FieldKind::ApiVersion => Ok("apiVersion".to_string()),
|
|
FieldKind::Tags => Ok("tags".to_string()),
|
|
FieldKind::Tag(tag) => Ok(format!("tags.{}", tag)),
|
|
FieldKind::Alias(path) => self.resolve_alias_path(path, span),
|
|
FieldKind::Expr(_) => {
|
|
bail!(span.error("count over expression field is not supported in core subset",))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Emit a single-level `ForEach` count loop.
|
|
///
|
|
/// Iterates `collection_reg`, pushes a [`CountBinding`] for the duration
|
|
/// of the loop body (so nested `current()` / field references resolve),
|
|
/// optionally guards with the where clause, and increments a counter
|
|
/// register on each passing iteration.
|
|
///
|
|
/// Returns the register holding the final count.
|
|
fn compile_count_loop(
|
|
&mut self,
|
|
collection_reg: u8,
|
|
binding_name: Option<String>,
|
|
field_wildcard_prefix: Option<String>,
|
|
where_constraint: Option<&Constraint>,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<u8> {
|
|
let count_reg = self.load_literal(Value::from(0_i64), span)?;
|
|
// Hoist the increment constant above the loop.
|
|
let one_reg = self.load_literal(Value::from(1_i64), span)?;
|
|
let key_reg = self.alloc_register()?;
|
|
let current_reg = self.alloc_register()?;
|
|
let loop_result_reg = self.alloc_register()?;
|
|
|
|
let params_index = self.program.add_loop_params(LoopStartParams {
|
|
mode: LoopMode::ForEach,
|
|
collection: collection_reg,
|
|
key_reg,
|
|
value_reg: current_reg,
|
|
result_reg: loop_result_reg,
|
|
body_start: 0,
|
|
loop_end: 0,
|
|
});
|
|
|
|
self.emit(Instruction::LoopStart { params_index }, span);
|
|
|
|
let body_start_u16 = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
self.count_bindings.push(CountBinding {
|
|
name: binding_name,
|
|
field_wildcard_prefix,
|
|
current_reg,
|
|
});
|
|
|
|
// Compile where clause body (if present) as a conditional increment.
|
|
if let Some(where_clause) = where_constraint {
|
|
let where_reg = self.compile_constraint(where_clause)?;
|
|
self.emit(
|
|
Instruction::Guard {
|
|
register: where_reg,
|
|
mode: GuardMode::Condition,
|
|
},
|
|
span,
|
|
);
|
|
}
|
|
|
|
self.emit(
|
|
Instruction::Add {
|
|
dest: count_reg,
|
|
left: count_reg,
|
|
right: one_reg,
|
|
},
|
|
span,
|
|
);
|
|
|
|
self.count_bindings.pop();
|
|
|
|
self.emit(
|
|
Instruction::LoopNext {
|
|
body_start: body_start_u16,
|
|
loop_end: 0,
|
|
},
|
|
span,
|
|
);
|
|
|
|
let loop_end_u16 = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
self.program.update_loop_params(params_index, |params| {
|
|
params.body_start = body_start_u16;
|
|
params.loop_end = loop_end_u16;
|
|
});
|
|
|
|
if let Some(Instruction::LoopNext { loop_end, .. }) = self.program.instructions.last_mut() {
|
|
*loop_end = loop_end_u16;
|
|
}
|
|
|
|
Ok(count_reg)
|
|
}
|
|
|
|
// -- count existence optimization (Any mode) ---------------------------
|
|
|
|
/// Try to compile a count condition as a `LoopMode::Any` loop when the
|
|
/// operator + RHS form an existence check (e.g., `count > 0`).
|
|
///
|
|
/// Returns `Some(result_reg)` if optimized, `None` to fall back to the
|
|
/// generic count + compare path.
|
|
pub(super) fn try_compile_count_as_any(
|
|
&mut self,
|
|
count_node: &CountNode,
|
|
condition: &Condition,
|
|
) -> Result<Option<u8>> {
|
|
// Determine whether the operator+RHS is an existence pattern.
|
|
let exists = match Self::classify_existence_pattern(condition) {
|
|
Some(e) => e,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
// Keep the where clause optional so plain `count(field: 'a[*]') > 0`
|
|
// can also use the early-exit Any lowering.
|
|
let where_constraint = match count_node {
|
|
CountNode::Field { where_, .. } | CountNode::Value { where_, .. } => where_.as_deref(),
|
|
};
|
|
|
|
self.observed_uses_count = true;
|
|
|
|
// Resolve collection and compile as Any loop.
|
|
let any_result = match count_node {
|
|
CountNode::Value {
|
|
span, value, name, ..
|
|
} => {
|
|
let collection_reg = self.compile_value_or_expr(value, span)?;
|
|
self.compile_count_any_loop(
|
|
collection_reg,
|
|
name.as_ref().map(|n| n.name.clone()),
|
|
None,
|
|
where_constraint,
|
|
span,
|
|
)?
|
|
}
|
|
CountNode::Field { span, field, .. } => {
|
|
// Multi-wildcard field paths use nested Any loops.
|
|
// Resolve outer bindings so we start from the bound element.
|
|
let field_path = self.extract_field_count_path(field, span)?;
|
|
let (_, suffix) = split_count_wildcard_path(&field_path)
|
|
.map_err(|e| span.error(&e.to_string()))?;
|
|
if suffix.as_ref().is_some_and(|s| s.contains("[*]")) {
|
|
// Skip the nested Any optimization when there is no where
|
|
// clause. The non-optimized path in `compile_count_nested`
|
|
// uses `Instruction::Count` for the innermost level, which
|
|
// returns `Undefined` when the collection is missing or
|
|
// non-iterable. The Any-based lowering instead treats a
|
|
// missing collection as empty (Any → false), so
|
|
// `Not(false)` → true, changing `count == 0` from false to
|
|
// true. Falling back to the generic count+compare path
|
|
// preserves the Undefined-propagation semantics.
|
|
let Some(wc) = where_constraint else {
|
|
return Ok(None);
|
|
};
|
|
|
|
if let Some(binding) = self.resolve_count_binding(&field_path)? {
|
|
if let Some(outer_prefix) = &binding.field_wildcard_prefix {
|
|
let lc_prefix = outer_prefix.to_ascii_lowercase();
|
|
let expected_prefix = format!("{}[*].", lc_prefix);
|
|
if let Some(inner_path) = field_path
|
|
.to_ascii_lowercase()
|
|
.strip_prefix(&expected_prefix)
|
|
{
|
|
let inner_path = inner_path.to_string();
|
|
return self.compile_count_nested_any(
|
|
Some(binding.current_reg),
|
|
&inner_path,
|
|
wc,
|
|
outer_prefix,
|
|
exists,
|
|
span,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return self.compile_count_nested_any(None, &field_path, wc, "", exists, span);
|
|
}
|
|
|
|
let (collection_reg, prefix) = self.resolve_count_field_collection(field, span)?;
|
|
self.compile_count_any_loop(
|
|
collection_reg,
|
|
None,
|
|
Some(prefix),
|
|
where_constraint,
|
|
span,
|
|
)?
|
|
}
|
|
};
|
|
|
|
if exists {
|
|
Ok(Some(any_result))
|
|
} else {
|
|
let dest = self.alloc_register()?;
|
|
self.emit(
|
|
Instruction::PolicyCondition {
|
|
dest,
|
|
left: any_result,
|
|
right: 0,
|
|
op: PolicyOp::Not,
|
|
},
|
|
&condition.span,
|
|
);
|
|
Ok(Some(dest))
|
|
}
|
|
}
|
|
|
|
/// Check whether a count condition's operator + RHS form an existence
|
|
/// pattern. Returns `Some(true)` for "at least one" semantics,
|
|
/// `Some(false)` for "none" semantics, or `None` if not applicable.
|
|
fn classify_existence_pattern(condition: &Condition) -> Option<bool> {
|
|
let n = match &condition.rhs {
|
|
ValueOrExpr::Value(JsonValue::Number(_, s)) => s.parse::<i64>().ok()?,
|
|
_ => return None,
|
|
};
|
|
match (&condition.operator.kind, n) {
|
|
(OperatorKind::Greater, 0)
|
|
| (OperatorKind::GreaterOrEquals, 1)
|
|
| (OperatorKind::NotEquals, 0) => Some(true),
|
|
(OperatorKind::Equals, 0)
|
|
| (OperatorKind::Less, 1)
|
|
| (OperatorKind::LessOrEquals, 0) => Some(false),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Compile a count's where clause as a `LoopMode::Any` loop.
|
|
///
|
|
/// The result register is `true` if any element satisfies the where
|
|
/// constraint (or simply exists when `where_constraint` is `None`),
|
|
/// `false` otherwise. The loop exits on the first match.
|
|
fn compile_count_any_loop(
|
|
&mut self,
|
|
collection_reg: u8,
|
|
binding_name: Option<String>,
|
|
field_wildcard_prefix: Option<String>,
|
|
where_constraint: Option<&Constraint>,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<u8> {
|
|
let key_reg = self.alloc_register()?;
|
|
let current_reg = self.alloc_register()?;
|
|
let result_reg = self.alloc_register()?;
|
|
|
|
let params_index = self.program.add_loop_params(LoopStartParams {
|
|
mode: LoopMode::Any,
|
|
collection: collection_reg,
|
|
key_reg,
|
|
value_reg: current_reg,
|
|
result_reg,
|
|
body_start: 0,
|
|
loop_end: 0,
|
|
});
|
|
|
|
self.emit(Instruction::LoopStart { params_index }, span);
|
|
|
|
let body_start = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
self.count_bindings.push(CountBinding {
|
|
name: binding_name,
|
|
field_wildcard_prefix,
|
|
current_reg,
|
|
});
|
|
|
|
if let Some(wc) = where_constraint {
|
|
let where_reg = self.compile_constraint(wc)?;
|
|
self.emit(
|
|
Instruction::Guard {
|
|
register: where_reg,
|
|
mode: GuardMode::Condition,
|
|
},
|
|
span,
|
|
);
|
|
}
|
|
|
|
self.count_bindings.pop();
|
|
|
|
self.emit(
|
|
Instruction::LoopNext {
|
|
body_start,
|
|
loop_end: 0,
|
|
},
|
|
span,
|
|
);
|
|
|
|
let loop_end = u16::try_from(self.program.instructions.len())
|
|
.map_err(|_| anyhow!("instruction index overflow"))?;
|
|
|
|
self.program.update_loop_params(params_index, |params| {
|
|
params.body_start = body_start;
|
|
params.loop_end = loop_end;
|
|
});
|
|
|
|
if let Some(Instruction::LoopNext { loop_end: le, .. }) =
|
|
self.program.instructions.last_mut()
|
|
{
|
|
*le = loop_end;
|
|
}
|
|
|
|
Ok(result_reg)
|
|
}
|
|
|
|
/// Find the innermost active count binding that covers `field_path`.
|
|
///
|
|
/// Matching rules (all case-insensitive):
|
|
/// 1. **Named binding** — `field_path` equals the binding's `name`.
|
|
/// 2. **Wildcard prefix** — `field_path` matches the binding's prefix,
|
|
/// its `prefix[*]` form, or starts with `prefix.` / `prefix[*].`.
|
|
///
|
|
/// Bindings are searched innermost-first (reverse stack order) so a
|
|
/// nested count's binding shadows an outer one for the same prefix.
|
|
pub(super) fn resolve_count_binding(&self, field_path: &str) -> Result<Option<CountBinding>> {
|
|
let fp = field_path.to_ascii_lowercase();
|
|
for binding in self.count_bindings.iter().rev() {
|
|
if let Some(name) = &binding.name {
|
|
if fp.eq_ignore_ascii_case(name) {
|
|
return Ok(Some(binding.clone()));
|
|
}
|
|
}
|
|
|
|
if let Some(prefix) = &binding.field_wildcard_prefix {
|
|
let lc_prefix = prefix.to_ascii_lowercase();
|
|
let wildcard_prefix = format!("{}[*]", lc_prefix);
|
|
let prefix_dot = format!("{}.", lc_prefix);
|
|
let wildcard_dot = format!("{}.", wildcard_prefix);
|
|
if fp == lc_prefix
|
|
|| fp.starts_with(&prefix_dot)
|
|
|| fp == wildcard_prefix
|
|
|| fp.starts_with(&wildcard_dot)
|
|
{
|
|
return Ok(Some(binding.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
/// Compile a field reference relative to an active count binding.
|
|
///
|
|
/// If `field_path` matches the binding exactly (name or prefix),
|
|
/// emits a `Move` from the binding's current-element register.
|
|
/// If `field_path` extends past the binding (e.g. `prefix.sub.key`),
|
|
/// navigates the suffix via chained index lookups. All comparisons
|
|
/// are case-insensitive.
|
|
pub(super) fn compile_from_binding(
|
|
&mut self,
|
|
binding: &CountBinding,
|
|
field_path: &str,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<u8> {
|
|
let fp = field_path.to_ascii_lowercase();
|
|
|
|
if let Some(name) = &binding.name {
|
|
if fp.eq_ignore_ascii_case(name) {
|
|
let dest = self.alloc_register()?;
|
|
self.emit(
|
|
Instruction::Move {
|
|
dest,
|
|
src: binding.current_reg,
|
|
},
|
|
span,
|
|
);
|
|
return Ok(dest);
|
|
}
|
|
}
|
|
|
|
if let Some(prefix) = &binding.field_wildcard_prefix {
|
|
let lc_prefix = prefix.to_ascii_lowercase();
|
|
let wildcard_prefix = format!("{}[*]", lc_prefix);
|
|
|
|
if fp == lc_prefix || fp == wildcard_prefix {
|
|
let dest = self.alloc_register()?;
|
|
self.emit(
|
|
Instruction::Move {
|
|
dest,
|
|
src: binding.current_reg,
|
|
},
|
|
span,
|
|
);
|
|
return Ok(dest);
|
|
}
|
|
|
|
let prefix_dot = format!("{}.", lc_prefix);
|
|
if let Some(suffix) = fp.strip_prefix(&prefix_dot) {
|
|
return self.compile_suffix_from_binding(binding.current_reg, suffix, span);
|
|
}
|
|
|
|
let wildcard_dot = format!("{}[*].", lc_prefix);
|
|
if let Some(suffix) = fp.strip_prefix(&wildcard_dot) {
|
|
return self.compile_suffix_from_binding(binding.current_reg, suffix, span);
|
|
}
|
|
}
|
|
|
|
bail!(span.error(&format!(
|
|
"invalid current count binding for field path '{}'",
|
|
field_path
|
|
)))
|
|
}
|
|
|
|
/// Compile a suffix path from a binding's current register.
|
|
///
|
|
/// If the suffix contains `[*]` (from a nested count context), only the
|
|
/// portion before the first `[*]` is used for navigation. The inner
|
|
/// count's loop will handle the iteration.
|
|
fn compile_suffix_from_binding(
|
|
&mut self,
|
|
base_reg: u8,
|
|
suffix: &str,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<u8> {
|
|
// Strip any trailing [*] or [*].suffix — we only navigate to the
|
|
// array itself; the count loop iterates its elements.
|
|
let nav_path = suffix
|
|
.split_once("[*]")
|
|
.map_or(suffix, |(prefix, _)| prefix);
|
|
// Lowercase to match normalizer-lowercased keys.
|
|
let nav_path = nav_path.to_ascii_lowercase();
|
|
let parts = split_path_without_wildcards(&nav_path)?;
|
|
let refs = parts.iter().map(String::as_str).collect::<Vec<_>>();
|
|
self.emit_chained_index_literal_path(base_reg, &refs, span)
|
|
}
|
|
|
|
/// Compile a `current('key')` reference inside a count's where clause.
|
|
///
|
|
/// Resolution is two-phase:
|
|
/// 1. Try matching `key` directly against the active binding stack
|
|
/// (case-insensitive). This handles literal alias paths and
|
|
/// named value-count bindings.
|
|
/// 2. If no direct match, resolve `key` through the alias catalog
|
|
/// and retry. When the catalog is loaded and fallback is disabled,
|
|
/// alias-resolution errors propagate so the caller sees "unknown
|
|
/// alias" rather than a generic scope error.
|
|
///
|
|
/// Bails with a "used outside an active count scope" error if neither
|
|
/// phase finds a matching binding.
|
|
pub(super) fn compile_current_reference(
|
|
&mut self,
|
|
key: &str,
|
|
span: &crate::lexer::Span,
|
|
) -> Result<u8> {
|
|
let resolve_for_key = |compiler: &mut Self, candidate: &str| -> Result<Option<u8>> {
|
|
let lc_candidate = candidate.to_ascii_lowercase();
|
|
for binding in compiler.count_bindings.iter().rev() {
|
|
if let Some(name) = &binding.name {
|
|
let lc_name = name.to_ascii_lowercase();
|
|
if lc_candidate == lc_name {
|
|
let current_reg = binding.current_reg;
|
|
let dest = compiler.alloc_register()?;
|
|
compiler.emit(
|
|
Instruction::Move {
|
|
dest,
|
|
src: current_reg,
|
|
},
|
|
span,
|
|
);
|
|
return Ok(Some(dest));
|
|
}
|
|
|
|
let name_dot = format!("{}.", lc_name);
|
|
if let Some(suffix) = lc_candidate.strip_prefix(&name_dot) {
|
|
let parts = split_path_without_wildcards(suffix)?;
|
|
let refs = parts.iter().map(String::as_str).collect::<Vec<_>>();
|
|
return compiler
|
|
.emit_chained_index_literal_path(binding.current_reg, &refs, span)
|
|
.map(Some);
|
|
}
|
|
}
|
|
|
|
if let Some(prefix) = &binding.field_wildcard_prefix {
|
|
let lc_prefix = prefix.to_ascii_lowercase();
|
|
if lc_candidate == lc_prefix || lc_candidate == format!("{}[*]", lc_prefix) {
|
|
let current_reg = binding.current_reg;
|
|
let dest = compiler.alloc_register()?;
|
|
compiler.emit(
|
|
Instruction::Move {
|
|
dest,
|
|
src: current_reg,
|
|
},
|
|
span,
|
|
);
|
|
return Ok(Some(dest));
|
|
}
|
|
|
|
let prefix_dot = format!("{}.", lc_prefix);
|
|
if let Some(suffix) = lc_candidate.strip_prefix(&prefix_dot) {
|
|
return compiler
|
|
.compile_suffix_from_binding(binding.current_reg, suffix, span)
|
|
.map(Some);
|
|
}
|
|
|
|
let prefix_wildcard_dot = format!("{}[*].", lc_prefix);
|
|
if let Some(suffix) = lc_candidate.strip_prefix(&prefix_wildcard_dot) {
|
|
return compiler
|
|
.compile_suffix_from_binding(binding.current_reg, suffix, span)
|
|
.map(Some);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
};
|
|
|
|
if let Some(result) = resolve_for_key(self, key)? {
|
|
return Ok(result);
|
|
}
|
|
|
|
// Try resolving via the alias catalog. When the catalog is loaded
|
|
// and fallback is disabled, propagate alias-resolution errors so the
|
|
// caller sees "unknown alias" instead of the generic "outside an
|
|
// active count scope" message.
|
|
match self.resolve_alias_path(key, span) {
|
|
Ok(normalized_key) if normalized_key != key => {
|
|
if let Some(result) = resolve_for_key(self, &normalized_key)? {
|
|
return Ok(result);
|
|
}
|
|
}
|
|
Err(e) if !self.alias_map.is_empty() && !self.alias_fallback_to_raw => {
|
|
return Err(e);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
bail!(span.error(&format!(
|
|
"current('{}') is used outside an active count scope",
|
|
key
|
|
)))
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Tests
|
|
// ===========================================================================
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
|
|
mod tests {
|
|
use alloc::string::ToString as _;
|
|
use alloc::vec;
|
|
use alloc::vec::Vec;
|
|
|
|
use crate::languages::azure_policy::ast::{
|
|
Condition, Constraint, CountNode, FieldKind, FieldNode, JsonValue, OperatorKind,
|
|
OperatorNode, ValueOrExpr,
|
|
};
|
|
use crate::languages::azure_policy::compiler::core::{Compiler, CountBinding};
|
|
use crate::lexer::Source;
|
|
use crate::rvm::instructions::{GuardMode, LoopMode, PolicyOp};
|
|
use crate::rvm::Instruction;
|
|
|
|
fn dummy_span() -> crate::lexer::Span {
|
|
let source = Source::from_contents("test".into(), " ".into()).unwrap();
|
|
crate::lexer::Span {
|
|
source,
|
|
line: 1,
|
|
col: 1,
|
|
start: 0,
|
|
end: 0,
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// resolve_count_binding
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn resolve_binding_empty_stack() {
|
|
let c = Compiler::new();
|
|
assert!(c.resolve_count_binding("a[*].b").unwrap().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_binding_by_field_prefix() {
|
|
let mut c = Compiler::new();
|
|
c.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some("a".to_string()),
|
|
current_reg: 5,
|
|
});
|
|
let binding = c.resolve_count_binding("a[*].b").unwrap().unwrap();
|
|
assert_eq!(binding.current_reg, 5);
|
|
assert_eq!(binding.field_wildcard_prefix.as_deref(), Some("a"));
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_binding_by_name() {
|
|
let mut c = Compiler::new();
|
|
c.count_bindings.push(CountBinding {
|
|
name: Some("myCollection".to_string()),
|
|
field_wildcard_prefix: None,
|
|
current_reg: 3,
|
|
});
|
|
let binding = c.resolve_count_binding("myCollection").unwrap().unwrap();
|
|
assert_eq!(binding.current_reg, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_binding_case_insensitive() {
|
|
let mut c = Compiler::new();
|
|
c.count_bindings.push(CountBinding {
|
|
name: Some("MyCollection".to_string()),
|
|
field_wildcard_prefix: None,
|
|
current_reg: 4,
|
|
});
|
|
// Lookup with different casing should still match.
|
|
let binding = c.resolve_count_binding("mycollection").unwrap().unwrap();
|
|
assert_eq!(binding.current_reg, 4);
|
|
|
|
let binding_upper = c.resolve_count_binding("MYCOLLECTION").unwrap().unwrap();
|
|
assert_eq!(binding_upper.current_reg, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_binding_field_prefix_case_insensitive() {
|
|
let mut c = Compiler::new();
|
|
c.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some("Microsoft.Test/resource".to_string()),
|
|
current_reg: 6,
|
|
});
|
|
// Mixed-case lookup against the prefix.
|
|
let binding = c
|
|
.resolve_count_binding("microsoft.test/resource[*].prop")
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(binding.current_reg, 6);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_binding_innermost_wins() {
|
|
let mut c = Compiler::new();
|
|
c.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some("a".to_string()),
|
|
current_reg: 1,
|
|
});
|
|
c.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some("a[*].b".to_string()),
|
|
current_reg: 2,
|
|
});
|
|
// The inner binding (a[*].b) matches a[*].b[*].c, and since we
|
|
// iterate in reverse, it wins.
|
|
let binding = c.resolve_count_binding("a[*].b[*].c").unwrap().unwrap();
|
|
assert_eq!(binding.current_reg, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_binding_no_match() {
|
|
let mut c = Compiler::new();
|
|
c.count_bindings.push(CountBinding {
|
|
name: None,
|
|
field_wildcard_prefix: Some("x".to_string()),
|
|
current_reg: 1,
|
|
});
|
|
assert!(c.resolve_count_binding("y[*].z").unwrap().is_none());
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// compile_count_nested — instruction shape for multi-wildcard paths
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn nested_count_no_where_emits_foreach_and_count() {
|
|
let mut c = Compiler::new();
|
|
let span = dummy_span();
|
|
// Compile a[*].b[*] (no where clause) starting from resource root.
|
|
let result_reg = c
|
|
.compile_count_nested(None, "a[*].b[*]", None, "", &span)
|
|
.unwrap();
|
|
|
|
// The outer loop should be ForEach (accumulating inner counts).
|
|
// Find the first LoopStart and check its mode.
|
|
let first_loop_idx = c
|
|
.program
|
|
.instructions
|
|
.iter()
|
|
.position(|i| matches!(i, Instruction::LoopStart { .. }))
|
|
.expect("should have a LoopStart");
|
|
|
|
if let Instruction::LoopStart { params_index } = c.program.instructions[first_loop_idx] {
|
|
let params = c
|
|
.program
|
|
.instruction_data
|
|
.get_loop_params(params_index)
|
|
.unwrap();
|
|
assert_eq!(
|
|
params.mode,
|
|
LoopMode::ForEach,
|
|
"outer loop should be ForEach"
|
|
);
|
|
}
|
|
|
|
// The innermost level has no where clause, so it should use Count
|
|
// instruction (direct count, no loop).
|
|
assert!(
|
|
c.program
|
|
.instructions
|
|
.iter()
|
|
.any(|i| matches!(i, Instruction::Count { .. })),
|
|
"innermost level without where should emit Count"
|
|
);
|
|
|
|
// Should also have an Add instruction to accumulate.
|
|
assert!(
|
|
c.program
|
|
.instructions
|
|
.iter()
|
|
.any(|i| matches!(i, Instruction::Add { .. })),
|
|
"should accumulate inner counts via Add"
|
|
);
|
|
|
|
// The result register should be valid.
|
|
assert!(result_reg < c.register_counter);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_count_with_where_emits_foreach_loops() {
|
|
let mut c = Compiler::new();
|
|
let span = dummy_span();
|
|
|
|
// A simple where clause: { field: "type", equals: "someType" }
|
|
let where_clause = Constraint::Condition(alloc::boxed::Box::new(Condition {
|
|
span: dummy_span(),
|
|
lhs: crate::languages::azure_policy::ast::Lhs::Field(FieldNode {
|
|
span: dummy_span(),
|
|
kind: FieldKind::Type,
|
|
}),
|
|
operator: OperatorNode {
|
|
span: dummy_span(),
|
|
kind: OperatorKind::Equals,
|
|
},
|
|
rhs: ValueOrExpr::Value(JsonValue::Str(dummy_span(), "someType".to_string())),
|
|
}));
|
|
|
|
let _result_reg = c
|
|
.compile_count_nested(None, "a[*].b[*]", Some(&where_clause), "", &span)
|
|
.unwrap();
|
|
|
|
// With a where clause the innermost level should emit a loop (not
|
|
// a bare Count instruction).
|
|
let loop_starts: Vec<_> = c
|
|
.program
|
|
.instructions
|
|
.iter()
|
|
.filter(|i| matches!(i, Instruction::LoopStart { .. }))
|
|
.collect();
|
|
assert!(
|
|
loop_starts.len() >= 2,
|
|
"nested count with where should emit at least 2 LoopStart instructions, got {}",
|
|
loop_starts.len()
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// compile_count_nested_any — existence-pattern optimization for nested paths
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn nested_any_exists_true_emits_any_loops() {
|
|
let mut c = Compiler::new();
|
|
let span = dummy_span();
|
|
|
|
let where_clause = Constraint::Condition(alloc::boxed::Box::new(Condition {
|
|
span: dummy_span(),
|
|
lhs: crate::languages::azure_policy::ast::Lhs::Field(FieldNode {
|
|
span: dummy_span(),
|
|
kind: FieldKind::Type,
|
|
}),
|
|
operator: OperatorNode {
|
|
span: dummy_span(),
|
|
kind: OperatorKind::Equals,
|
|
},
|
|
rhs: ValueOrExpr::Value(JsonValue::Str(dummy_span(), "someType".to_string())),
|
|
}));
|
|
|
|
let result = c
|
|
.compile_count_nested_any(
|
|
None,
|
|
"a[*].b[*]",
|
|
&where_clause,
|
|
"",
|
|
true, // exists = true → count > 0
|
|
&span,
|
|
)
|
|
.unwrap();
|
|
assert!(result.is_some(), "nested any should return Some");
|
|
|
|
// All loops should be LoopMode::Any for the existence optimization.
|
|
for instr in &c.program.instructions {
|
|
if let Instruction::LoopStart { params_index } = instr {
|
|
let params = c
|
|
.program
|
|
.instruction_data
|
|
.get_loop_params(*params_index)
|
|
.unwrap();
|
|
assert_eq!(
|
|
params.mode,
|
|
LoopMode::Any,
|
|
"existence pattern should use Any loops"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_any_exists_false_emits_not() {
|
|
let mut c = Compiler::new();
|
|
let span = dummy_span();
|
|
|
|
let where_clause = Constraint::Condition(alloc::boxed::Box::new(Condition {
|
|
span: dummy_span(),
|
|
lhs: crate::languages::azure_policy::ast::Lhs::Field(FieldNode {
|
|
span: dummy_span(),
|
|
kind: FieldKind::Type,
|
|
}),
|
|
operator: OperatorNode {
|
|
span: dummy_span(),
|
|
kind: OperatorKind::Equals,
|
|
},
|
|
rhs: ValueOrExpr::Value(JsonValue::Str(dummy_span(), "someType".to_string())),
|
|
}));
|
|
|
|
let result = c
|
|
.compile_count_nested_any(
|
|
None,
|
|
"a[*].b[*]",
|
|
&where_clause,
|
|
"",
|
|
false, // exists = false → count == 0
|
|
&span,
|
|
)
|
|
.unwrap();
|
|
assert!(result.is_some());
|
|
|
|
// Should have a PolicyCondition with Not op for the negation.
|
|
assert!(
|
|
c.program.instructions.iter().any(|i| matches!(
|
|
i,
|
|
Instruction::PolicyCondition { op, .. } if *op == PolicyOp::Not
|
|
)),
|
|
"count == 0 pattern should negate with PolicyCondition::Not"
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// try_compile_count_as_any — existence detection via operator + RHS
|
|
// -----------------------------------------------------------------------
|
|
|
|
/// Helper: build a Condition with count LHS, given operator and numeric RHS.
|
|
fn make_count_condition(count_node: CountNode, op: OperatorKind, rhs_number: i64) -> Condition {
|
|
Condition {
|
|
span: dummy_span(),
|
|
lhs: crate::languages::azure_policy::ast::Lhs::Count(count_node),
|
|
operator: OperatorNode {
|
|
span: dummy_span(),
|
|
kind: op,
|
|
},
|
|
rhs: ValueOrExpr::Value(JsonValue::Number(dummy_span(), rhs_number.to_string())),
|
|
}
|
|
}
|
|
|
|
fn make_value_count_with_where() -> CountNode {
|
|
CountNode::Value {
|
|
span: dummy_span(),
|
|
value: ValueOrExpr::Value(JsonValue::Array(
|
|
dummy_span(),
|
|
vec![
|
|
JsonValue::Number(dummy_span(), "1".to_string()),
|
|
JsonValue::Number(dummy_span(), "2".to_string()),
|
|
JsonValue::Number(dummy_span(), "3".to_string()),
|
|
],
|
|
)),
|
|
name: None,
|
|
where_: Some(alloc::boxed::Box::new(Constraint::Condition(
|
|
alloc::boxed::Box::new(Condition {
|
|
span: dummy_span(),
|
|
lhs: crate::languages::azure_policy::ast::Lhs::Value {
|
|
key_span: dummy_span(),
|
|
value: ValueOrExpr::Value(JsonValue::Number(dummy_span(), "1".to_string())),
|
|
},
|
|
operator: OperatorNode {
|
|
span: dummy_span(),
|
|
kind: OperatorKind::Equals,
|
|
},
|
|
rhs: ValueOrExpr::Value(JsonValue::Number(dummy_span(), "1".to_string())),
|
|
}),
|
|
))),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn any_optimization_greater_zero() {
|
|
let mut c = Compiler::new();
|
|
let count_node = make_value_count_with_where();
|
|
let condition = make_count_condition(count_node.clone(), OperatorKind::Greater, 0);
|
|
let result = c.try_compile_count_as_any(&count_node, &condition).unwrap();
|
|
assert!(
|
|
result.is_some(),
|
|
"count > 0 should trigger Any optimization"
|
|
);
|
|
|
|
// The loop should use LoopMode::Any.
|
|
for instr in &c.program.instructions {
|
|
if let Instruction::LoopStart { params_index } = instr {
|
|
let params = c
|
|
.program
|
|
.instruction_data
|
|
.get_loop_params(*params_index)
|
|
.unwrap();
|
|
assert_eq!(params.mode, LoopMode::Any);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn any_optimization_equals_zero_negates() {
|
|
let mut c = Compiler::new();
|
|
let count_node = make_value_count_with_where();
|
|
let condition = make_count_condition(count_node.clone(), OperatorKind::Equals, 0);
|
|
let result = c.try_compile_count_as_any(&count_node, &condition).unwrap();
|
|
assert!(
|
|
result.is_some(),
|
|
"count == 0 should trigger Any optimization"
|
|
);
|
|
|
|
// Should negate: PolicyCondition with Not.
|
|
assert!(
|
|
c.program.instructions.iter().any(|i| matches!(
|
|
i,
|
|
Instruction::PolicyCondition { op, .. } if *op == PolicyOp::Not
|
|
)),
|
|
"count == 0 should negate"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn any_optimization_not_triggered_for_equals_two() {
|
|
let mut c = Compiler::new();
|
|
let count_node = make_value_count_with_where();
|
|
let condition = make_count_condition(count_node.clone(), OperatorKind::Equals, 2);
|
|
let result = c.try_compile_count_as_any(&count_node, &condition).unwrap();
|
|
assert!(
|
|
result.is_none(),
|
|
"count == 2 is not an existence pattern, should return None"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn any_optimization_no_where_uses_any_loop() {
|
|
let mut c = Compiler::new();
|
|
let count_node = CountNode::Value {
|
|
span: dummy_span(),
|
|
value: ValueOrExpr::Value(JsonValue::Array(dummy_span(), vec![])),
|
|
name: None,
|
|
where_: None,
|
|
};
|
|
let condition = make_count_condition(count_node.clone(), OperatorKind::Greater, 0);
|
|
let result = c.try_compile_count_as_any(&count_node, &condition).unwrap();
|
|
assert!(
|
|
result.is_some(),
|
|
"without where clause, Any optimization should still apply for existence patterns"
|
|
);
|
|
|
|
// Verify it emitted an Any loop.
|
|
let has_any_loop = c.program.instructions.iter().any(|instr| {
|
|
if let Instruction::LoopStart { params_index } = instr {
|
|
let params = c
|
|
.program
|
|
.instruction_data
|
|
.get_loop_params(*params_index)
|
|
.unwrap();
|
|
params.mode == LoopMode::Any
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
assert!(has_any_loop, "should emit a LoopMode::Any loop");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// compile_count — value-based count loop
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn compile_value_count_without_where() {
|
|
let mut c = Compiler::new();
|
|
let count_node = CountNode::Value {
|
|
span: dummy_span(),
|
|
value: ValueOrExpr::Value(JsonValue::Array(
|
|
dummy_span(),
|
|
vec![
|
|
JsonValue::Number(dummy_span(), "1".to_string()),
|
|
JsonValue::Number(dummy_span(), "2".to_string()),
|
|
],
|
|
)),
|
|
name: None,
|
|
where_: None,
|
|
};
|
|
|
|
let result_reg = c.compile_count(&count_node).unwrap();
|
|
assert!(result_reg < c.register_counter);
|
|
|
|
// Should emit a ForEach loop with Add to increment count.
|
|
let has_loop = c
|
|
.program
|
|
.instructions
|
|
.iter()
|
|
.any(|i| matches!(i, Instruction::LoopStart { .. }));
|
|
let has_add = c
|
|
.program
|
|
.instructions
|
|
.iter()
|
|
.any(|i| matches!(i, Instruction::Add { .. }));
|
|
assert!(has_loop, "value count should emit a loop");
|
|
assert!(has_add, "value count should emit Add to increment");
|
|
}
|
|
|
|
#[test]
|
|
fn compile_value_count_with_where() {
|
|
let mut c = Compiler::new();
|
|
let count_node = make_value_count_with_where();
|
|
|
|
let result_reg = c.compile_count(&count_node).unwrap();
|
|
assert!(result_reg < c.register_counter);
|
|
|
|
// Should have Guard instruction for the where clause.
|
|
assert!(
|
|
c.program.instructions.iter().any(|i| matches!(
|
|
i,
|
|
Instruction::Guard {
|
|
mode: GuardMode::Condition,
|
|
..
|
|
}
|
|
)),
|
|
"count with where should emit Guard for where condition"
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// classify_existence_pattern — direct coverage of all recognized patterns
|
|
// -----------------------------------------------------------------------
|
|
|
|
/// Helper to build a Condition with the given operator and numeric RHS
|
|
/// (LHS is irrelevant for classify_existence_pattern).
|
|
fn make_condition_for_classify(op: OperatorKind, rhs: i64) -> Condition {
|
|
Condition {
|
|
span: dummy_span(),
|
|
lhs: crate::languages::azure_policy::ast::Lhs::Field(FieldNode {
|
|
span: dummy_span(),
|
|
kind: FieldKind::Type,
|
|
}),
|
|
operator: OperatorNode {
|
|
span: dummy_span(),
|
|
kind: op,
|
|
},
|
|
rhs: ValueOrExpr::Value(JsonValue::Number(dummy_span(), rhs.to_string())),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn classify_existence_all_patterns() {
|
|
// "at least one" patterns → Some(true)
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::Greater,
|
|
0
|
|
)),
|
|
Some(true),
|
|
"> 0"
|
|
);
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::GreaterOrEquals,
|
|
1
|
|
)),
|
|
Some(true),
|
|
">= 1"
|
|
);
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::NotEquals,
|
|
0
|
|
)),
|
|
Some(true),
|
|
"!= 0"
|
|
);
|
|
|
|
// "none" patterns → Some(false)
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::Equals,
|
|
0
|
|
)),
|
|
Some(false),
|
|
"== 0"
|
|
);
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::Less,
|
|
1
|
|
)),
|
|
Some(false),
|
|
"< 1"
|
|
);
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::LessOrEquals,
|
|
0
|
|
)),
|
|
Some(false),
|
|
"<= 0"
|
|
);
|
|
|
|
// Non-existence patterns → None
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::Equals,
|
|
2
|
|
)),
|
|
None,
|
|
"== 2"
|
|
);
|
|
assert_eq!(
|
|
Compiler::classify_existence_pattern(&make_condition_for_classify(
|
|
OperatorKind::Greater,
|
|
1
|
|
)),
|
|
None,
|
|
"> 1"
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// try_compile_count_as_any — nested no-where field count is skipped
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn any_optimization_skips_nested_no_where_field_count() {
|
|
// A nested wildcard field path without a where clause should NOT be
|
|
// optimised into Any loops because of Undefined-propagation semantics.
|
|
let mut c = Compiler::new();
|
|
let count_node = CountNode::Field {
|
|
span: dummy_span(),
|
|
field: FieldNode {
|
|
span: dummy_span(),
|
|
kind: FieldKind::Alias("a[*].b[*]".to_string()),
|
|
},
|
|
where_: None,
|
|
};
|
|
let condition = make_count_condition(count_node.clone(), OperatorKind::Equals, 0);
|
|
let result = c.try_compile_count_as_any(&count_node, &condition).unwrap();
|
|
assert!(
|
|
result.is_none(),
|
|
"nested no-where field count should fall back to generic path"
|
|
);
|
|
}
|
|
}
|