feat(azure-policy): implement count/count.where compilation (#688)

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.
This commit is contained in:
Anand Krishnamoorthi
2026-04-23 11:53:43 -05:00
committed by GitHub
parent f50a9744ff
commit ad82227ddb
7 changed files with 1951 additions and 78 deletions

View File

@@ -4,6 +4,7 @@
//! Implicit allOf for unbound `[*]` wildcard fields.
use alloc::format;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
@@ -67,12 +68,12 @@ impl Compiler {
};
if let Some(prefix) = &binding.field_wildcard_prefix {
let bound_len = prefix.len().saturating_add(4); // len("prefix") + len("[*].")
if path.len() > bound_len {
if let Some(remainder) = path.get(bound_len..) {
if remainder.contains("[*]") {
return Ok(Some((binding, remainder.to_string())));
}
let lc_prefix = prefix.to_ascii_lowercase();
let bound_prefix = format!("{}[*].", lc_prefix);
if let Some(remainder) = path.to_ascii_lowercase().strip_prefix(&bound_prefix) {
let remainder = remainder.to_string();
if remainder.contains("[*]") {
return Ok(Some((binding, remainder)));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Existence-pattern optimization (count → Any loop).
//!
//! Stub — real implementation added in a later commit.

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Count-binding resolution and `current()` references.
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use super::core::{Compiler, CountBinding};
impl Compiler {
pub(super) const fn resolve_count_binding(
&self,
_field_path: &str,
) -> Result<Option<CountBinding>> {
_ = self.register_counter;
Ok(None)
}
pub(super) fn compile_from_binding(
&mut self,
_binding: &CountBinding,
_field_path: &str,
span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!(span.error("count binding compilation not yet implemented"))
}
pub(super) fn compile_current_reference(
&mut self,
_key: &str,
span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!(span.error("current() reference not yet implemented"))
}
}

View File

@@ -8,9 +8,8 @@
//! - [`core`]: `Compiler` struct, main pipeline, register/emit helpers
//! - [`conditions`]: constraint / condition / LHS compilation
//! - [`conditions_wildcard`]: implicit allOf for unbound `[*]` fields
//! - [`count`]: `count` / `count.where` loops
//! - [`count_any`]: existence-pattern optimization (count → Any loop)
//! - [`count_bindings`]: count-binding resolution and `current()` references
//! - [`count`]: `count` / `count.where` loops, existence-pattern optimization,
//! count-binding resolution and `current()` references
//! - [`expressions`]: template-expression and call-expression compilation
//! - [`fields`]: field-kind and resource-path compilation
//! - [`template_dispatch`]: ARM template function dispatch
@@ -23,8 +22,6 @@ mod conditions;
mod conditions_wildcard;
mod core;
mod count;
mod count_any;
mod count_bindings;
mod effects;
mod effects_modify_append;
mod expressions;

View File

@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code, clippy::pattern_type_mismatch)]
#![allow(clippy::pattern_type_mismatch)]
//! Free helper functions used by the Azure Policy compiler.
@@ -43,9 +43,6 @@ pub(super) fn split_count_wildcard_path(path: &str) -> Result<(String, Option<St
path
)
})?;
if after_wildcard.contains("[*]") {
bail!("nested [*] wildcards are not supported: {}", path);
}
let suffix_str = after_wildcard.trim_start_matches('.');
let suffix = if suffix_str.is_empty() {
None
@@ -351,7 +348,9 @@ mod tests {
#[test]
fn wildcard_nested() {
split_count_wildcard_path("a[*].b[*].c").unwrap_err();
let (prefix, suffix) = split_count_wildcard_path("a[*].b[*].c").unwrap();
assert_eq!(prefix, "a");
assert_eq!(suffix.as_deref(), Some("b[*].c"));
}
// -----------------------------------------------------------------------