From 68d935f5c17ff5a3b358e726b684db0ce485a20c Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Fri, 10 Apr 2026 11:21:06 -0500 Subject: [PATCH] feat(azure-policy): add compiler skeleton with core types and stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the compiler module structure with: - core.rs: Compiler struct, CountBinding, new(), compile() pipeline, register allocation, span/emit helpers - mod.rs: module declarations, public entry points (compile_policy_rule, compile_policy_definition, etc.) - utils.rs: pure helper functions (path splitting, JSON conversion) - Stub files for conditions, expressions, fields, template dispatch, count, effects, and metadata — real implementations follow in subsequent commits. --- .../azure_policy/compiler/conditions.rs | 20 + .../compiler/conditions_wildcard.rs | 6 + src/languages/azure_policy/compiler/core.rs | 195 ++++++++ src/languages/azure_policy/compiler/count.rs | 29 ++ .../azure_policy/compiler/count_any.rs | 6 + .../azure_policy/compiler/count_bindings.rs | 40 ++ .../azure_policy/compiler/effects.rs | 30 ++ .../compiler/effects_modify_append.rs | 6 + .../azure_policy/compiler/expressions.rs | 53 +++ src/languages/azure_policy/compiler/fields.rs | 56 +++ .../azure_policy/compiler/metadata.rs | 52 +++ src/languages/azure_policy/compiler/mod.rs | 153 +++++++ .../compiler/template_dispatch.rs | 25 + src/languages/azure_policy/compiler/utils.rs | 427 ++++++++++++++++++ src/languages/azure_policy/mod.rs | 2 + 15 files changed, 1100 insertions(+) create mode 100644 src/languages/azure_policy/compiler/conditions.rs create mode 100644 src/languages/azure_policy/compiler/conditions_wildcard.rs create mode 100644 src/languages/azure_policy/compiler/core.rs create mode 100644 src/languages/azure_policy/compiler/count.rs create mode 100644 src/languages/azure_policy/compiler/count_any.rs create mode 100644 src/languages/azure_policy/compiler/count_bindings.rs create mode 100644 src/languages/azure_policy/compiler/effects.rs create mode 100644 src/languages/azure_policy/compiler/effects_modify_append.rs create mode 100644 src/languages/azure_policy/compiler/expressions.rs create mode 100644 src/languages/azure_policy/compiler/fields.rs create mode 100644 src/languages/azure_policy/compiler/metadata.rs create mode 100644 src/languages/azure_policy/compiler/mod.rs create mode 100644 src/languages/azure_policy/compiler/template_dispatch.rs create mode 100644 src/languages/azure_policy/compiler/utils.rs diff --git a/src/languages/azure_policy/compiler/conditions.rs b/src/languages/azure_policy/compiler/conditions.rs new file mode 100644 index 00000000..31198287 --- /dev/null +++ b/src/languages/azure_policy/compiler/conditions.rs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(clippy::pattern_type_mismatch)] + +//! Constraint / condition / LHS compilation. +//! +//! Stub — real implementation added in a later commit. + +use anyhow::{bail, Result}; + +use crate::languages::azure_policy::ast::Constraint; + +use super::core::Compiler; + +impl Compiler { + pub(super) fn compile_constraint(&mut self, _constraint: &Constraint) -> Result { + let _ = self; + bail!("condition compilation not yet implemented") + } +} diff --git a/src/languages/azure_policy/compiler/conditions_wildcard.rs b/src/languages/azure_policy/compiler/conditions_wildcard.rs new file mode 100644 index 00000000..01f237b1 --- /dev/null +++ b/src/languages/azure_policy/compiler/conditions_wildcard.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Implicit allOf for unbound `[*]` wildcard fields. +//! +//! Stub — real implementation added in a later commit. diff --git a/src/languages/azure_policy/compiler/core.rs b/src/languages/azure_policy/compiler/core.rs new file mode 100644 index 00000000..1729b933 --- /dev/null +++ b/src/languages/azure_policy/compiler/core.rs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] +#![allow(clippy::pattern_type_mismatch)] + +//! Core `Compiler` struct, main compilation pipeline, and register/emit +//! infrastructure. + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; + +use anyhow::{anyhow, bail, Result}; + +use crate::rvm::program::{Program, SpanInfo}; +use crate::rvm::Instruction; +use crate::{Rc, Value}; + +use crate::languages::azure_policy::ast::PolicyRule; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +pub(super) struct CountBinding { + pub(super) name: Option, + pub(super) field_wildcard_prefix: Option, + pub(super) current_reg: u8, +} + +#[derive(Debug, Default)] +pub(super) struct Compiler { + pub(super) program: Program, + pub(super) register_counter: u8, + /// High-water mark of `register_counter`. + pub(super) register_high_water: u8, + pub(super) source_to_index: BTreeMap, + pub(super) builtin_index: BTreeMap, + pub(super) count_bindings: Vec, + /// Cached register for `LoadInput` — allocated once on first use. + pub(super) cached_input_reg: Option, + /// Cached register for `LoadContext` — allocated once on first use. + pub(super) cached_context_reg: Option, + /// Map from lowercase fully-qualified alias name → short name. + pub(super) alias_map: BTreeMap, + /// Map from lowercase fully-qualified alias name → modifiable flag. + pub(super) alias_modifiable: BTreeMap, + /// Default values for policy parameters. + pub(super) parameter_defaults: Option, + /// When set, field conditions resolve against this register instead of + /// `input.resource`. Used for `existenceCondition`. + pub(super) resource_override_reg: Option, + + // -- Metadata accumulators --------------------------------------------- + pub(super) observed_field_kinds: BTreeSet, + pub(super) observed_aliases: BTreeSet, + pub(super) observed_tag_names: BTreeSet, + pub(super) observed_operators: BTreeSet, + pub(super) observed_resource_types: BTreeSet, + pub(super) observed_uses_count: bool, + pub(super) observed_has_dynamic_fields: bool, + pub(super) observed_has_wildcard_aliases: bool, + + /// When `true`, unknown aliases are silently treated as raw property paths. + pub(super) alias_fallback_to_raw: bool, +} + +// --------------------------------------------------------------------------- +// Core infrastructure +// --------------------------------------------------------------------------- + +impl Compiler { + pub(super) fn new() -> Self { + Self { + register_counter: 0, + ..Self::default() + } + } + + pub(super) fn compile(mut self, rule: &PolicyRule) -> Result> { + let cond_reg = self.compile_constraint(&rule.condition)?; + self.emit( + Instruction::ReturnUndefinedIfNotTrue { + condition: cond_reg, + }, + &rule.span, + ); + + let effect_reg = self.compile_effect(rule)?; + self.emit( + Instruction::Return { value: effect_reg }, + &rule.then_block.span, + ); + + self.program.main_entry_point = 0; + self.program.entry_points.insert("main".to_string(), 0); + self.program.dispatch_window_size = self.register_high_water.max(2); + self.program.max_rule_window_size = 0; + + if !self.program.builtin_info_table.is_empty() { + self.program.initialize_resolved_builtins()?; + } + + self.program + .validate_limits() + .map_err(|message| anyhow!(message))?; + + self.populate_compiled_annotations(); + + Ok(Rc::new(self.program)) + } + + // -- register / span / emit helpers ------------------------------------ + + /// Restore `register_counter` to `saved` while protecting cached registers. + pub(super) fn restore_register_counter(&mut self, saved: u8) { + let mut floor = saved; + if let Some(r) = self.cached_input_reg { + floor = floor.max(r.saturating_add(1)); + } + if let Some(r) = self.cached_context_reg { + floor = floor.max(r.saturating_add(1)); + } + self.register_counter = floor; + } + + pub(super) fn alloc_register(&mut self) -> Result { + if self.register_counter == u8::MAX { + bail!("azure-policy compiler exhausted RVM registers"); + } + let reg = self.register_counter; + self.register_counter = self.register_counter.saturating_add(1); + if self.register_counter > self.register_high_water { + self.register_high_water = self.register_counter; + } + Ok(reg) + } + + pub(super) fn span_info(&mut self, span: &crate::lexer::Span) -> SpanInfo { + let path = span.source.get_path().to_string(); + let source_index = if let Some(index) = self.source_to_index.get(path.as_str()) { + *index + } else { + let index = self + .program + .add_source(path.clone(), span.source.get_contents().to_string()); + self.source_to_index.insert(path, index); + index + }; + + SpanInfo::from_lexer_span(span, source_index) + } + + pub(super) fn emit(&mut self, instruction: Instruction, span: &crate::lexer::Span) { + let span_info = self.span_info(span); + self.program.add_instruction(instruction, Some(span_info)); + } + + /// Return the PC (instruction index) that the *next* emitted instruction + /// will occupy. + pub(super) fn current_pc(&self) -> Result { + u16::try_from(self.program.instructions.len()) + .map_err(|_| anyhow!("instruction index overflow")) + } + + /// Patch tracked instruction indices, setting their `end_pc` field. + pub(super) fn patch_end_pc(&mut self, pcs: &[u16], end_pc: u16) -> Result<()> { + for &pc in pcs { + let idx = usize::from(pc); + let instr = self + .program + .instructions + .get_mut(idx) + .ok_or_else(|| anyhow!("patch_end_pc: pc {} out of bounds", pc))?; + match instr { + Instruction::LogicalBlockStart { + end_pc: ref mut ep, .. + } + | Instruction::AllOfNext { + end_pc: ref mut ep, .. + } + | Instruction::AnyOfNext { + end_pc: ref mut ep, .. + } => { + *ep = end_pc; + } + _ => { + bail!("patch_end_pc: unexpected instruction at pc {}", pc); + } + } + } + Ok(()) + } +} diff --git a/src/languages/azure_policy/compiler/count.rs b/src/languages/azure_policy/compiler/count.rs new file mode 100644 index 00000000..42f61365 --- /dev/null +++ b/src/languages/azure_policy/compiler/count.rs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] + +//! `count` / `count.where` loop compilation. +//! +//! Stub — real implementation added in a later commit. + +use anyhow::{bail, Result}; + +use crate::languages::azure_policy::ast::{Condition, CountNode}; + +use super::core::Compiler; + +impl Compiler { + pub(super) fn compile_count(&mut self, _count_node: &CountNode) -> Result { + let _ = self; + bail!("count compilation not yet implemented") + } + + pub(super) const fn try_compile_count_as_any( + &mut self, + _count_node: &CountNode, + _condition: &Condition, + ) -> Result> { + _ = self.register_counter; + Ok(None) + } +} diff --git a/src/languages/azure_policy/compiler/count_any.rs b/src/languages/azure_policy/compiler/count_any.rs new file mode 100644 index 00000000..03d17e8b --- /dev/null +++ b/src/languages/azure_policy/compiler/count_any.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Existence-pattern optimization (count → Any loop). +//! +//! Stub — real implementation added in a later commit. diff --git a/src/languages/azure_policy/compiler/count_bindings.rs b/src/languages/azure_policy/compiler/count_bindings.rs new file mode 100644 index 00000000..32644ecb --- /dev/null +++ b/src/languages/azure_policy/compiler/count_bindings.rs @@ -0,0 +1,40 @@ +// 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> { + _ = self.register_counter; + Ok(None) + } + + pub(super) fn compile_from_binding( + &mut self, + _binding: CountBinding, + _field_path: &str, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("count binding compilation not yet implemented") + } + + pub(super) fn compile_current_reference( + &mut self, + _key: &str, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("current() reference not yet implemented") + } +} diff --git a/src/languages/azure_policy/compiler/effects.rs b/src/languages/azure_policy/compiler/effects.rs new file mode 100644 index 00000000..09701f1e --- /dev/null +++ b/src/languages/azure_policy/compiler/effects.rs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] + +//! Effect compilation (dispatch + cross-resource). +//! +//! Stub — real implementation added in a later commit. + +use anyhow::{bail, Result}; + +use crate::languages::azure_policy::ast::PolicyRule; + +use super::core::Compiler; + +impl Compiler { + pub(super) fn compile_effect(&mut self, _rule: &PolicyRule) -> Result { + let _ = self; + bail!("effect compilation not yet implemented") + } + + pub(super) fn wrap_effect_result( + &mut self, + _effect_name_reg: u8, + _details_reg: Option, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("wrap_effect_result not yet implemented") + } +} diff --git a/src/languages/azure_policy/compiler/effects_modify_append.rs b/src/languages/azure_policy/compiler/effects_modify_append.rs new file mode 100644 index 00000000..af24f5f7 --- /dev/null +++ b/src/languages/azure_policy/compiler/effects_modify_append.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Modify / Append effect detail compilation. +//! +//! Stub — real implementation added in a later commit. diff --git a/src/languages/azure_policy/compiler/expressions.rs b/src/languages/azure_policy/compiler/expressions.rs new file mode 100644 index 00000000..3e2b69e6 --- /dev/null +++ b/src/languages/azure_policy/compiler/expressions.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] + +//! Template-expression and call-expression compilation. +//! +//! Stub — real implementation added in a later commit. + +use anyhow::{bail, Result}; + +use crate::languages::azure_policy::ast::{Expr, JsonValue, ValueOrExpr}; + +use super::core::Compiler; + +impl Compiler { + pub(super) fn compile_value_or_expr( + &mut self, + _voe: &ValueOrExpr, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("expression compilation not yet implemented") + } + + pub(super) fn compile_json_value( + &mut self, + _value: &JsonValue, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("JSON value compilation not yet implemented") + } + + pub(super) fn compile_expr(&mut self, _expr: &Expr) -> Result { + let _ = self; + bail!("expression compilation not yet implemented") + } + + pub(super) fn compile_call_expr( + &mut self, + _span: &crate::lexer::Span, + _func: &str, + _args: &[Expr], + ) -> Result { + let _ = self; + bail!("call expression compilation not yet implemented") + } + + pub(super) fn compile_call_args(&mut self, _args: &[Expr]) -> Result> { + let _ = self; + bail!("call args compilation not yet implemented") + } +} diff --git a/src/languages/azure_policy/compiler/fields.rs b/src/languages/azure_policy/compiler/fields.rs new file mode 100644 index 00000000..9c070bd8 --- /dev/null +++ b/src/languages/azure_policy/compiler/fields.rs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] + +//! Field-kind and resource-path compilation. +//! +//! Stub — real implementation added in a later commit. + +use anyhow::{bail, Result}; + +use crate::languages::azure_policy::ast::FieldKind; + +use super::core::Compiler; + +impl Compiler { + pub(super) fn compile_field_kind( + &mut self, + _kind: &FieldKind, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("field compilation not yet implemented") + } + + pub(super) fn compile_field_path_expression( + &mut self, + _field_path: &str, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("field path compilation not yet implemented") + } + + pub(super) fn compile_resource_path_value( + &mut self, + _field_path: &str, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("resource path compilation not yet implemented") + } + + pub(super) fn compile_resource_root(&mut self, _span: &crate::lexer::Span) -> Result { + let _ = self; + bail!("resource root compilation not yet implemented") + } + + pub(super) fn compile_field_wildcard_collect( + &mut self, + _field_path: &str, + _span: &crate::lexer::Span, + ) -> Result { + let _ = self; + bail!("wildcard collect not yet implemented") + } +} diff --git a/src/languages/azure_policy/compiler/metadata.rs b/src/languages/azure_policy/compiler/metadata.rs new file mode 100644 index 00000000..6af026d4 --- /dev/null +++ b/src/languages/azure_policy/compiler/metadata.rs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] + +//! Annotation accumulation and metadata population. +//! +//! Stub — real implementation added in a later commit. + +use crate::languages::azure_policy::ast::{EffectNode, OperatorKind, PolicyDefinition, PolicyRule}; + +use super::core::Compiler; + +impl Compiler { + pub(super) const fn record_field_kind(&mut self, _name: &str) { + _ = self.register_counter; + } + pub(super) const fn record_alias(&mut self, _path: &str) { + _ = self.register_counter; + } + pub(super) const fn record_tag_name(&mut self, _tag: &str) { + _ = self.register_counter; + } + pub(super) const fn record_operator(&mut self, _kind: &OperatorKind) { + _ = self.register_counter; + } + pub(super) const fn record_resource_type_from_condition( + &mut self, + _condition: &crate::languages::azure_policy::ast::Condition, + ) { + _ = self.register_counter; + } + + #[allow(clippy::unused_self)] + pub(super) fn resolve_effect_annotation(&self, rule: &PolicyRule) -> alloc::string::String { + rule.then_block.effect.raw.clone() + } + + #[allow(clippy::unused_self)] + pub(super) fn resolve_effect_kind( + &self, + effect: &EffectNode, + ) -> crate::languages::azure_policy::ast::EffectKind { + effect.kind.clone() + } + + pub(super) const fn populate_compiled_annotations(&mut self) { + _ = self.register_counter; + } + pub(super) const fn populate_definition_metadata(&mut self, _defn: &PolicyDefinition) { + _ = self.register_counter; + } +} diff --git a/src/languages/azure_policy/compiler/mod.rs b/src/languages/azure_policy/compiler/mod.rs new file mode 100644 index 00000000..6e2265de --- /dev/null +++ b/src/languages/azure_policy/compiler/mod.rs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Azure Policy AST → RVM compiler. +//! +//! The compiler is split across several files: +//! - [`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 +//! - [`expressions`]: template-expression and call-expression compilation +//! - [`fields`]: field-kind and resource-path compilation +//! - [`template_dispatch`]: ARM template function dispatch +//! - [`effects`]: effect compilation (dispatch + cross-resource) +//! - [`effects_modify_append`]: Modify / Append detail compilation +//! - [`metadata`]: annotation accumulation and population +//! - [`utils`]: pure helper functions (path splitting, JSON conversion) + +mod conditions; +mod conditions_wildcard; +mod core; +mod count; +mod count_any; +mod count_bindings; +mod effects; +mod effects_modify_append; +mod expressions; +mod fields; +mod metadata; +mod template_dispatch; +mod utils; + +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString as _}; + +use anyhow::Result; + +use crate::languages::azure_policy::ast::{PolicyDefinition, PolicyRule}; +use crate::rvm::program::Program; +use crate::{Rc, Value}; + +use self::core::Compiler; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Initialise compiler language metadata and effect annotation. +fn init_effect_annotation(compiler: &mut Compiler, rule: &PolicyRule) { + compiler.program.metadata.language = "azure_policy".to_string(); + let effect = compiler.resolve_effect_annotation(rule); + compiler + .program + .metadata + .annotations + .insert("effect".to_string(), Value::String(effect.as_str().into())); +} + +// --------------------------------------------------------------------------- +// Public entry points +// --------------------------------------------------------------------------- + +/// Compile a parsed Azure Policy rule into an RVM program. +pub fn compile_policy_rule(rule: &PolicyRule) -> Result> { + let mut compiler = Compiler::new(); + init_effect_annotation(&mut compiler, rule); + compiler.compile(rule) +} + +/// Compile a parsed Azure Policy rule with alias resolution. +/// +/// The `alias_map` maps lowercase fully-qualified alias names to their short +/// names. Obtain it from +/// [`AliasRegistry::alias_map()`](crate::languages::azure_policy::aliases::AliasRegistry::alias_map). +pub fn compile_policy_rule_with_aliases( + rule: &PolicyRule, + alias_map: BTreeMap, + alias_modifiable: BTreeMap, +) -> Result> { + let mut compiler = Compiler::new(); + compiler.alias_map = alias_map; + compiler.alias_modifiable = alias_modifiable; + init_effect_annotation(&mut compiler, rule); + compiler.compile(rule) +} + +/// Compile a parsed Azure Policy definition into an RVM program. +/// +/// This extracts the `policyRule` from the definition and compiles it. +/// Parameter `defaultValue`s are collected so that later compiler passes +/// (effect compilation, metadata population) can reference them. +pub fn compile_policy_definition(defn: &PolicyDefinition) -> Result> { + let mut compiler = Compiler::new(); + compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?); + compiler.populate_definition_metadata(defn); + init_effect_annotation(&mut compiler, &defn.policy_rule); + compiler.compile(&defn.policy_rule) +} + +/// Compile a parsed Azure Policy definition with alias resolution. +pub fn compile_policy_definition_with_aliases( + defn: &PolicyDefinition, + alias_map: BTreeMap, + alias_modifiable: BTreeMap, +) -> Result> { + let mut compiler = Compiler::new(); + compiler.alias_map = alias_map; + compiler.alias_modifiable = alias_modifiable; + compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?); + compiler.populate_definition_metadata(defn); + init_effect_annotation(&mut compiler, &defn.policy_rule); + compiler.compile(&defn.policy_rule) +} + +/// Compile a parsed Azure Policy definition with alias resolution and +/// optional fallback behaviour for unknown aliases. +/// +/// When `alias_fallback_to_raw` is `true`, field paths that do not resolve to +/// a known alias are silently treated as raw property paths. +pub fn compile_policy_definition_with_aliases_opts( + defn: &PolicyDefinition, + alias_map: BTreeMap, + alias_modifiable: BTreeMap, + alias_fallback_to_raw: bool, +) -> Result> { + let mut compiler = Compiler::new(); + compiler.alias_map = alias_map; + compiler.alias_modifiable = alias_modifiable; + compiler.alias_fallback_to_raw = alias_fallback_to_raw; + compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?); + compiler.populate_definition_metadata(defn); + init_effect_annotation(&mut compiler, &defn.policy_rule); + compiler.compile(&defn.policy_rule) +} + +/// Build a `Value::Object` of `{ param_name: defaultValue }` from +/// the parsed parameter definitions. +fn build_parameter_defaults( + params: &[crate::languages::azure_policy::ast::ParameterDefinition], +) -> Result { + use crate::languages::azure_policy::compiler::utils::json_value_to_runtime; + let mut obj = Value::new_object(); + let map = obj.as_object_mut()?; + for param in params { + if let Some(ref default_val) = param.default_value { + let runtime_val = json_value_to_runtime(default_val)?; + map.insert(Value::from(param.name.clone()), runtime_val); + } + } + Ok(obj) +} diff --git a/src/languages/azure_policy/compiler/template_dispatch.rs b/src/languages/azure_policy/compiler/template_dispatch.rs new file mode 100644 index 00000000..5d449168 --- /dev/null +++ b/src/languages/azure_policy/compiler/template_dispatch.rs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code)] + +//! ARM template function dispatch. +//! +//! Stub — real implementation added in a later commit. + +use anyhow::Result; + +use crate::languages::azure_policy::ast::Expr; + +use super::core::Compiler; + +impl Compiler { + pub(super) const fn compile_arm_template_function( + &mut self, + _function_name: &str, + _span: &crate::lexer::Span, + _args: &[Expr], + ) -> Result> { + _ = self.register_counter; + Ok(None) + } +} diff --git a/src/languages/azure_policy/compiler/utils.rs b/src/languages/azure_policy/compiler/utils.rs new file mode 100644 index 00000000..de9b74a4 --- /dev/null +++ b/src/languages/azure_policy/compiler/utils.rs @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(dead_code, clippy::pattern_type_mismatch, clippy::redundant_pub_crate)] + +//! Free helper functions used by the Azure Policy compiler. + +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; + +use anyhow::{anyhow, bail, Result}; + +use crate::languages::azure_policy::ast::{Expr, ExprLiteral, JsonValue, ObjectEntry}; +use crate::Value; + +/// Extract a string literal from an expression, or bail. +pub(super) fn extract_string_literal(expr: &Expr) -> Result { + match expr { + Expr::Literal { + value: ExprLiteral::String(value), + .. + } => Ok(value.clone()), + other => bail!("expected string literal argument, found {:?}", other), + } +} + +/// Split a count field path at the `[*]` wildcard into `(prefix, optional_suffix)`. +pub(super) fn split_count_wildcard_path(path: &str) -> Result<(String, Option)> { + let wildcard_index = path + .find("[*]") + .ok_or_else(|| anyhow!("count.field must contain [*]: {}", path))?; + + let (prefix_str, rest) = path.split_at(wildcard_index); + let prefix = prefix_str.trim_end_matches('.'); + if prefix.is_empty() { + bail!( + "count.field must have a non-empty prefix before [*]: {}", + path + ); + } + let after_wildcard = &rest[3..]; + 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 + } else { + Some(suffix_str.to_string()) + }; + + Ok((prefix.to_string(), suffix)) +} + +/// Split a dotted path (without `[*]` wildcards) into its component segments. +/// +/// Handles bracket notation: +/// - `tags['key']` → `["tags", "key"]` +/// - `properties['network-acls']` → `["properties", "network-acls"]` +/// - `properties.ipRules[0].value` → `["properties", "ipRules", "0", "value"]` +pub(super) fn split_path_without_wildcards(path: &str) -> Result> { + if path.trim().is_empty() { + bail!("empty path"); + } + if path.contains("[*]") { + bail!( + "wildcard field paths are not supported in this context: {}", + path + ); + } + if path.ends_with('.') { + bail!("path must not end with '.': {}", path); + } + + let mut parts = Vec::new(); + let mut token = String::new(); + let mut bracket = String::new(); + let mut in_bracket = false; + let mut after_bracket = false; + + for ch in path.chars() { + match ch { + '.' if !in_bracket => { + let t = token.trim(); + if t.is_empty() && !after_bracket { + bail!("empty segment in path: {}", path); + } + if !t.is_empty() { + parts.push(t.to_string()); + } + token.clear(); + after_bracket = false; + } + '[' => { + if in_bracket { + bail!("nested brackets in path: {}", path); + } + in_bracket = true; + after_bracket = false; + let t = token.trim(); + if !t.is_empty() { + parts.push(t.to_string()); + } + token.clear(); + } + ']' => { + if !in_bracket { + bail!("unexpected closing bracket in path: {}", path); + } + in_bracket = false; + after_bracket = true; + let cleaned = bracket + .trim() + .trim_matches('"') + .trim_matches('\'') + .to_string(); + if cleaned.is_empty() { + bail!("empty bracket in path: {}", path); + } + parts.push(cleaned); + bracket.clear(); + } + _ => { + if in_bracket { + bracket.push(ch); + } else { + if after_bracket { + bail!("expected '.' or '[' after bracket in path: {}", path); + } + token.push(ch); + } + } + } + } + + if in_bracket { + bail!("unclosed bracket in path: {}", path); + } + + let t = token.trim(); + if !t.is_empty() { + parts.push(t.to_string()); + } + + Ok(parts) +} + +/// Convert a parsed JSON value from the Azure Policy AST into a runtime [`Value`]. +pub(crate) fn json_value_to_runtime(value: &JsonValue) -> Result { + match value { + JsonValue::Null(_) => Ok(Value::Null), + JsonValue::Bool(_, b) => Ok(Value::Bool(*b)), + JsonValue::Number(_, raw) => { + Value::from_numeric_string(raw).map_err(|_| anyhow!("invalid number literal: {}", raw)) + } + JsonValue::Str(_, s) => Ok(Value::from(s.clone())), + JsonValue::Array(_, items) => { + let mut out = Vec::with_capacity(items.len()); + for item in items { + out.push(json_value_to_runtime(item)?); + } + Ok(Value::from(out)) + } + JsonValue::Object(_, entries) => { + let mut obj = Value::new_object(); + let map = obj.as_object_mut()?; + for ObjectEntry { + key, + value: entry_value, + .. + } in entries + { + map.insert( + Value::from(key.clone()), + json_value_to_runtime(entry_value)?, + ); + } + Ok(obj) + } + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use alloc::vec; + + use crate::lexer::Source; + + use super::*; + + 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, + } + } + + // ----------------------------------------------------------------------- + // extract_string_literal + // ----------------------------------------------------------------------- + + #[test] + fn extract_string_literal_ok() { + let expr = Expr::Literal { + span: dummy_span(), + value: ExprLiteral::String("hello".into()), + }; + assert_eq!(extract_string_literal(&expr).unwrap(), "hello"); + } + + #[test] + fn extract_string_literal_number_err() { + let expr = Expr::Literal { + span: dummy_span(), + value: ExprLiteral::Number("42".into()), + }; + extract_string_literal(&expr).unwrap_err(); + } + + #[test] + fn extract_string_literal_ident_err() { + let expr = Expr::Ident { + span: dummy_span(), + name: "x".into(), + }; + extract_string_literal(&expr).unwrap_err(); + } + + // ----------------------------------------------------------------------- + // json_value_to_runtime + // ----------------------------------------------------------------------- + + #[test] + fn json_null() { + let v = json_value_to_runtime(&JsonValue::Null(dummy_span())).unwrap(); + assert_eq!(v, Value::Null); + } + + #[test] + fn json_bool() { + let v = json_value_to_runtime(&JsonValue::Bool(dummy_span(), true)).unwrap(); + assert_eq!(v, Value::Bool(true)); + } + + #[test] + fn json_number_int() { + let v = json_value_to_runtime(&JsonValue::Number(dummy_span(), "42".into())).unwrap(); + assert_eq!(v, Value::from(42_i64)); + } + + #[test] + fn json_number_float() { + let v = json_value_to_runtime(&JsonValue::Number(dummy_span(), "1.5".into())).unwrap(); + assert_eq!(v, Value::from(1.5_f64)); + } + + #[test] + fn json_number_invalid() { + json_value_to_runtime(&JsonValue::Number(dummy_span(), "abc".into())).unwrap_err(); + } + + #[test] + fn json_string() { + let v = json_value_to_runtime(&JsonValue::Str(dummy_span(), "hello".into())).unwrap(); + assert_eq!(v, Value::from("hello".to_string())); + } + + #[test] + fn json_array() { + let arr = JsonValue::Array( + dummy_span(), + vec![ + JsonValue::Bool(dummy_span(), true), + JsonValue::Null(dummy_span()), + ], + ); + let v = json_value_to_runtime(&arr).unwrap(); + let items = v.as_array().unwrap(); + assert_eq!(items.len(), 2); + } + + #[test] + fn json_object() { + let obj = JsonValue::Object( + dummy_span(), + vec![ObjectEntry { + key_span: dummy_span(), + key: "k".into(), + value: JsonValue::Bool(dummy_span(), false), + }], + ); + let v = json_value_to_runtime(&obj).unwrap(); + let map = v.as_object().unwrap(); + assert_eq!(map.len(), 1); + } + + // ----------------------------------------------------------------------- + // split_count_wildcard_path + // ----------------------------------------------------------------------- + + #[test] + fn wildcard_simple() { + let (prefix, suffix) = split_count_wildcard_path("a.b[*].c").unwrap(); + assert_eq!(prefix, "a.b"); + assert_eq!(suffix.as_deref(), Some("c")); + } + + #[test] + fn wildcard_no_suffix() { + let (prefix, suffix) = split_count_wildcard_path("a[*]").unwrap(); + assert_eq!(prefix, "a"); + assert_eq!(suffix, None); + } + + #[test] + fn wildcard_trailing_dot_prefix() { + let (prefix, _) = split_count_wildcard_path("a.[*].c").unwrap(); + assert_eq!(prefix, "a"); + } + + #[test] + fn wildcard_missing() { + split_count_wildcard_path("a.b.c").unwrap_err(); + } + + #[test] + fn wildcard_empty_prefix() { + split_count_wildcard_path("[*].c").unwrap_err(); + } + + #[test] + fn wildcard_nested() { + split_count_wildcard_path("a[*].b[*].c").unwrap_err(); + } + + // ----------------------------------------------------------------------- + // split_path_without_wildcards + // ----------------------------------------------------------------------- + + #[test] + fn path_simple_dotted() { + assert_eq!( + split_path_without_wildcards("a.b.c").unwrap(), + vec!["a", "b", "c"] + ); + } + + #[test] + fn path_bracket_quoted() { + assert_eq!( + split_path_without_wildcards("tags['key']").unwrap(), + vec!["tags", "key"] + ); + } + + #[test] + fn path_bracket_numeric() { + assert_eq!( + split_path_without_wildcards("a[0].b").unwrap(), + vec!["a", "0", "b"] + ); + } + + #[test] + fn path_single_segment() { + assert_eq!(split_path_without_wildcards("name").unwrap(), vec!["name"]); + } + + #[test] + fn path_empty() { + split_path_without_wildcards("").unwrap_err(); + } + + #[test] + fn path_whitespace_only() { + split_path_without_wildcards(" ").unwrap_err(); + } + + #[test] + fn path_trailing_dot() { + split_path_without_wildcards("a.").unwrap_err(); + } + + #[test] + fn path_leading_dot() { + split_path_without_wildcards(".a").unwrap_err(); + } + + #[test] + fn path_consecutive_dots() { + split_path_without_wildcards("a..b").unwrap_err(); + } + + #[test] + fn path_wildcard_rejected() { + split_path_without_wildcards("a[*].b").unwrap_err(); + } + + #[test] + fn path_nested_brackets() { + split_path_without_wildcards("a[[0]]").unwrap_err(); + } + + #[test] + fn path_stray_close_bracket() { + split_path_without_wildcards("a]b").unwrap_err(); + } + + #[test] + fn path_unclosed_bracket() { + split_path_without_wildcards("a[0").unwrap_err(); + } + + #[test] + fn path_empty_bracket() { + split_path_without_wildcards("a[]").unwrap_err(); + } + + #[test] + fn path_char_after_bracket_without_separator() { + split_path_without_wildcards("a[0]b").unwrap_err(); + } +} diff --git a/src/languages/azure_policy/mod.rs b/src/languages/azure_policy/mod.rs index c46508ce..5b7e9a33 100644 --- a/src/languages/azure_policy/mod.rs +++ b/src/languages/azure_policy/mod.rs @@ -6,6 +6,8 @@ #[allow(clippy::pattern_type_mismatch)] pub mod aliases; pub mod ast; +#[cfg(feature = "rvm")] +pub mod compiler; pub mod expr; pub mod parser; pub mod strings;