From f50a9744ff2a6c530836db1af9041ca5eac4edf9 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:51:08 -0500 Subject: [PATCH] feat(azure-policy): implement condition, expression, field, and template dispatch compilation (#686) Fill in the compiler stubs for the evaluation layer. Condition and wildcard compilation: - Compile allOf/anyOf/not constraints, operator conditions with value-condition guards, and implicit allOf for unbound [*] fields via recursive Every loops. - Defensively lowercase prefix/suffix path segments in wildcard handling for consistency with the collect path. Expression and field compilation: - Parse ARM template expressions and dispatch calls to parameters, field, current, resourceGroup, subscription, and others. - Compile all FieldKind variants (type, id, name, location, tags, aliases, dynamic if/concat), resolve resource paths, and collect wildcard values via ForEach loops. Template function dispatch: - Wire up 50+ ARM template functions covering string, numeric, encoding, collection, date/time, logical, and comparison categories. Compiler infrastructure (core.rs): - Add emit helpers: load_literal, emit_builtin_call, emit_chained_index_literal_path, load_input, load_context, emit_coalesce_undefined_to_null, add_literal_u16, and get_or_add_builtin_index. - Add alias resolution via resolve_alias_path and strip_fq_prefix. Misc cleanup: - Handle ARM template `[[` escape sequences in json_value_to_runtime and add a test for it. - Tighten module visibility (pub -> pub(crate)/pub(super)) where appropriate. - Add span context to bail errors in stubs so diagnostics carry source locations. - Take CountBinding by reference in compile_from_binding. - Suppress clippy warnings on the no-op memory_check stub. --- .../azure_policy/compiler/conditions.rs | 256 +++++++++++- .../compiler/conditions_wildcard.rs | 199 +++++++++- src/languages/azure_policy/compiler/core.rs | 189 +++++++++ src/languages/azure_policy/compiler/count.rs | 9 +- .../azure_policy/compiler/count_bindings.rs | 10 +- .../azure_policy/compiler/expressions.rs | 318 +++++++++++++-- src/languages/azure_policy/compiler/fields.rs | 309 +++++++++++++-- src/languages/azure_policy/compiler/mod.rs | 1 + .../compiler/template_dispatch.rs | 363 +++++++++++++++++- src/languages/azure_policy/compiler/utils.rs | 29 +- src/languages/azure_policy/mod.rs | 2 +- src/rvm/vm/machine.rs | 1 + 12 files changed, 1600 insertions(+), 86 deletions(-) diff --git a/src/languages/azure_policy/compiler/conditions.rs b/src/languages/azure_policy/compiler/conditions.rs index 3119828..a6073e7 100644 --- a/src/languages/azure_policy/compiler/conditions.rs +++ b/src/languages/azure_policy/compiler/conditions.rs @@ -3,18 +3,260 @@ #![allow(clippy::pattern_type_mismatch)] //! Constraint / condition / LHS compilation. -//! -//! Stub — real implementation added in a later commit. -use anyhow::{bail, Result}; +use alloc::vec::Vec; -use crate::languages::azure_policy::ast::Constraint; +use anyhow::Result; + +use crate::languages::azure_policy::ast::{Condition, Constraint, Lhs, OperatorKind}; +use crate::rvm::instructions::{LogicalBlockMode, PolicyOp}; +use crate::rvm::Instruction; use super::core::Compiler; impl Compiler { - pub(super) fn compile_constraint(&mut self, _constraint: &Constraint) -> Result { - let _ = self; - bail!("condition compilation not yet implemented") + pub(super) fn compile_constraint(&mut self, constraint: &Constraint) -> Result { + match constraint { + Constraint::AllOf { span, constraints } => self.compile_allof(constraints, span), + Constraint::AnyOf { span, constraints } => self.compile_anyof(constraints, span), + Constraint::Not { span, constraint } => { + let inner = self.compile_constraint(constraint)?; + self.emit_coalesce_undefined_to_null(inner, span); + let dest = self.alloc_register()?; + self.emit( + Instruction::PolicyCondition { + dest, + left: inner, + right: 0, + op: PolicyOp::Not, + }, + span, + ); + Ok(dest) + } + Constraint::Condition(condition) => self.compile_condition(condition), + } + } + + // -- allOf with short-circuit ------------------------------------------ + + fn compile_allof( + &mut self, + constraints: &[Constraint], + span: &crate::lexer::Span, + ) -> Result { + let result_reg = self.alloc_register()?; + + let mut patch_pcs = Vec::with_capacity(constraints.len().saturating_add(1)); + + patch_pcs.push(self.current_pc()?); + self.emit( + Instruction::LogicalBlockStart { + mode: LogicalBlockMode::AllOf, + result: result_reg, + end_pc: 0, + }, + span, + ); + + for child in constraints { + let saved_counter = self.register_counter; + let child_reg = self.compile_constraint(child)?; + self.emit_coalesce_undefined_to_null(child_reg, span); + patch_pcs.push(self.current_pc()?); + self.emit( + Instruction::AllOfNext { + check: child_reg, + result: result_reg, + end_pc: 0, + }, + span, + ); + self.restore_register_counter(saved_counter); + } + + let end_pc = self.current_pc()?; + self.emit( + Instruction::LogicalBlockEnd { + mode: LogicalBlockMode::AllOf, + result: result_reg, + }, + span, + ); + + self.patch_end_pc(&patch_pcs, end_pc)?; + + Ok(result_reg) + } + + // -- anyOf with short-circuit ------------------------------------------ + + fn compile_anyof( + &mut self, + constraints: &[Constraint], + span: &crate::lexer::Span, + ) -> Result { + let result_reg = self.alloc_register()?; + + let mut patch_pcs = Vec::with_capacity(constraints.len().saturating_add(1)); + + patch_pcs.push(self.current_pc()?); + self.emit( + Instruction::LogicalBlockStart { + mode: LogicalBlockMode::AnyOf, + result: result_reg, + end_pc: 0, + }, + span, + ); + + for child in constraints { + let saved_counter = self.register_counter; + let child_reg = self.compile_constraint(child)?; + self.emit_coalesce_undefined_to_null(child_reg, span); + patch_pcs.push(self.current_pc()?); + self.emit( + Instruction::AnyOfNext { + check: child_reg, + result: result_reg, + end_pc: 0, + }, + span, + ); + self.restore_register_counter(saved_counter); + } + + let end_pc = self.current_pc()?; + self.emit( + Instruction::LogicalBlockEnd { + mode: LogicalBlockMode::AnyOf, + result: result_reg, + }, + span, + ); + + self.patch_end_pc(&patch_pcs, end_pc)?; + + Ok(result_reg) + } + + // -- operator condition compilation ------------------------------------ + + pub(super) fn compile_condition(&mut self, condition: &Condition) -> Result { + self.record_resource_type_from_condition(condition); + + // Implicit allOf: field with [*] outside count -> every element must match. + if let Some(field_path) = self.has_unbound_wildcard_field(&condition.lhs)? { + return self.compile_condition_wildcard_allof(&field_path, condition); + } + + // Inner unbound [*] within count where clause. + if let Some((binding, inner_path)) = + self.has_inner_unbound_wildcard_field(&condition.lhs)? + { + let span = &condition.span; + let rhs_reg = self.compile_value_or_expr(&condition.rhs, span)?; + return self.compile_allof_loop_inner( + Some(binding.current_reg), + &inner_path, + rhs_reg, + condition, + ); + } + + // Count existence optimization. + if let Lhs::Count(count_node) = &condition.lhs { + if let Some(result) = self.try_compile_count_as_any(count_node, condition)? { + return Ok(result); + } + } + + let lhs = self.compile_lhs(&condition.lhs, &condition.span)?; + + // In Azure Policy, a missing field is semantically null. Coalesce + // undefined → null for field-based LHS so the behaviour matches the + // `field()` template-expression path. `exists` deliberately needs to + // distinguish undefined from null, so we skip coalescing for it. + if matches!(condition.lhs, Lhs::Field(..)) + && !matches!(condition.operator.kind, OperatorKind::Exists) + { + self.emit_coalesce_undefined_to_null(lhs, &condition.span); + } + + let rhs = self.compile_value_or_expr(&condition.rhs, &condition.span)?; + let op_result = self.emit_policy_operator( + &condition.operator.kind, + lhs, + rhs, + &condition.operator.span, + )?; + + // For `value:` conditions, guard against undefined LHS. + if matches!(condition.lhs, Lhs::Value { .. }) { + let guarded = self.alloc_register()?; + self.emit( + Instruction::PolicyCondition { + dest: guarded, + left: lhs, + right: op_result, + op: PolicyOp::ValueConditionGuard, + }, + &condition.span, + ); + return Ok(guarded); + } + + Ok(op_result) + } + + pub(super) fn compile_lhs(&mut self, lhs: &Lhs, span: &crate::lexer::Span) -> Result { + match lhs { + Lhs::Field(field) => self.compile_field_kind(&field.kind, &field.span), + Lhs::Value { value, .. } => self.compile_value_or_expr(value, span), + Lhs::Count(count_node) => self.compile_count(count_node), + } + } + + /// Emit a native policy operator instruction. + pub(super) fn emit_policy_operator( + &mut self, + kind: &OperatorKind, + left: u8, + right: u8, + span: &crate::lexer::Span, + ) -> Result { + self.record_operator(kind); + let dest = self.alloc_register()?; + let op = match kind { + OperatorKind::Equals => PolicyOp::Equals, + OperatorKind::NotEquals => PolicyOp::NotEquals, + OperatorKind::Greater => PolicyOp::Greater, + OperatorKind::GreaterOrEquals => PolicyOp::GreaterOrEquals, + OperatorKind::Less => PolicyOp::Less, + OperatorKind::LessOrEquals => PolicyOp::LessOrEquals, + OperatorKind::In => PolicyOp::In, + OperatorKind::NotIn => PolicyOp::NotIn, + OperatorKind::Contains => PolicyOp::Contains, + OperatorKind::NotContains => PolicyOp::NotContains, + OperatorKind::ContainsKey => PolicyOp::ContainsKey, + OperatorKind::NotContainsKey => PolicyOp::NotContainsKey, + OperatorKind::Like => PolicyOp::Like, + OperatorKind::NotLike => PolicyOp::NotLike, + OperatorKind::Match => PolicyOp::Match, + OperatorKind::NotMatch => PolicyOp::NotMatch, + OperatorKind::MatchInsensitively => PolicyOp::MatchInsensitively, + OperatorKind::NotMatchInsensitively => PolicyOp::NotMatchInsensitively, + OperatorKind::Exists => PolicyOp::Exists, + }; + self.emit( + Instruction::PolicyCondition { + dest, + left, + right, + op, + }, + span, + ); + Ok(dest) } } diff --git a/src/languages/azure_policy/compiler/conditions_wildcard.rs b/src/languages/azure_policy/compiler/conditions_wildcard.rs index 01f237b..37b1aa0 100644 --- a/src/languages/azure_policy/compiler/conditions_wildcard.rs +++ b/src/languages/azure_policy/compiler/conditions_wildcard.rs @@ -1,6 +1,201 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#![allow(clippy::pattern_type_mismatch)] //! Implicit allOf for unbound `[*]` wildcard fields. -//! -//! Stub — real implementation added in a later commit. + +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; + +use anyhow::{anyhow, Result}; + +use crate::languages::azure_policy::ast::{Condition, FieldKind, Lhs}; +use crate::rvm::instructions::{GuardMode, LoopMode, LoopStartParams}; +use crate::rvm::Instruction; + +use super::core::{Compiler, CountBinding}; +use super::utils::{split_count_wildcard_path, split_path_without_wildcards}; + +impl Compiler { + /// Check whether a condition's LHS is a field with an unbound `[*]` + /// wildcard (i.e., not inside a count loop that covers this path). + pub(super) fn has_unbound_wildcard_field(&self, lhs: &Lhs) -> Result> { + let field = match lhs { + Lhs::Field(field_node) => field_node, + _ => return Ok(None), + }; + + let path = match &field.kind { + FieldKind::Alias(alias) => self.resolve_alias_path(alias, &field.span)?, + _ => return Ok(None), + }; + + if !path.contains("[*]") { + return Ok(None); + } + + if self.resolve_count_binding(&path)?.is_some() { + return Ok(None); + } + + Ok(Some(path)) + } + + /// Check whether a condition's LHS has an inner unbound `[*]` that lives + /// *inside* an active count binding. + pub(super) fn has_inner_unbound_wildcard_field( + &self, + lhs: &Lhs, + ) -> Result> { + let field = match lhs { + Lhs::Field(field_node) => field_node, + _ => return Ok(None), + }; + + let path = match &field.kind { + FieldKind::Alias(alias) => self.resolve_alias_path(alias, &field.span)?, + _ => return Ok(None), + }; + + if !path.contains("[*]") { + return Ok(None); + } + + let binding = match self.resolve_count_binding(&path)? { + Some(b) => b, + None => return Ok(None), + }; + + 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()))); + } + } + } + } + + Ok(None) + } + + /// Compile a condition where the field LHS contains `[*]` outside a + /// count loop. Emits implicit *allOf* (Every loop). + pub(super) fn compile_condition_wildcard_allof( + &mut self, + field_path: &str, + condition: &Condition, + ) -> Result { + let span = &condition.span; + let rhs_reg = self.compile_value_or_expr(&condition.rhs, span)?; + self.compile_allof_loop_inner(None, field_path, rhs_reg, condition) + } + + /// Recursive helper: emit one `Every` loop per `[*]` in the path. + pub(super) fn compile_allof_loop_inner( + &mut self, + base_reg: Option, + remaining_path: &str, + rhs_reg: u8, + condition: &Condition, + ) -> Result { + let (prefix, suffix) = split_count_wildcard_path(remaining_path)?; + let prefix = prefix.to_ascii_lowercase(); + let suffix = suffix.map(|s| s.to_ascii_lowercase()); + let span = &condition.span; + + let collection_reg = match base_reg { + Some(base) if prefix.is_empty() => base, + Some(base) => { + let parts = split_path_without_wildcards(&prefix)?; + let refs = parts.iter().map(String::as_str).collect::>(); + self.emit_chained_index_literal_path(base, &refs, span)? + } + None if prefix.is_empty() => self.compile_resource_root(span)?, + None => self.compile_resource_path_value(&prefix, 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::Every, + 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"))?; + + match suffix { + Some(ref s) if s.contains("[*]") => { + let inner_result = + self.compile_allof_loop_inner(Some(current_reg), s, rhs_reg, condition)?; + self.emit( + Instruction::Guard { + register: inner_result, + mode: GuardMode::Condition, + }, + span, + ); + } + _ => { + let element_reg = match &suffix { + Some(s) => { + let parts = split_path_without_wildcards(s)?; + let refs = parts.iter().map(String::as_str).collect::>(); + self.emit_chained_index_literal_path(current_reg, &refs, span)? + } + None => current_reg, + }; + + let cmp_reg = self.emit_policy_operator( + &condition.operator.kind, + element_reg, + rhs_reg, + &condition.operator.span, + )?; + + self.emit( + Instruction::Guard { + register: cmp_reg, + mode: GuardMode::Condition, + }, + span, + ); + } + } + + 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(loop_result_reg) + } +} diff --git a/src/languages/azure_policy/compiler/core.rs b/src/languages/azure_policy/compiler/core.rs index 1729b93..42497a3 100644 --- a/src/languages/azure_policy/compiler/core.rs +++ b/src/languages/azure_policy/compiler/core.rs @@ -7,11 +7,13 @@ //! infrastructure. use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::format; use alloc::string::{String, ToString as _}; use alloc::vec::Vec; use anyhow::{anyhow, bail, Result}; +use crate::rvm::instructions::{BuiltinCallParams, ChainedIndexParams, LiteralOrRegister}; use crate::rvm::program::{Program, SpanInfo}; use crate::rvm::Instruction; use crate::{Rc, Value}; @@ -48,6 +50,8 @@ pub(super) struct Compiler { pub(super) alias_modifiable: BTreeMap, /// Default values for policy parameters. pub(super) parameter_defaults: Option, + /// Cached register for the parameter defaults literal. + pub(super) cached_defaults_reg: Option, /// When set, field conditions resolve against this register instead of /// `input.resource`. Used for `existenceCondition`. pub(super) resource_override_reg: Option, @@ -122,6 +126,9 @@ impl Compiler { if let Some(r) = self.cached_context_reg { floor = floor.max(r.saturating_add(1)); } + if let Some(r) = self.cached_defaults_reg { + floor = floor.max(r.saturating_add(1)); + } self.register_counter = floor; } @@ -157,6 +164,138 @@ impl Compiler { self.program.add_instruction(instruction, Some(span_info)); } + // -- literal / builtin / chained-index helpers ------------------------- + + pub(super) fn add_literal_u16(&mut self, value: Value) -> Result { + let idx = self.program.add_literal(value); + u16::try_from(idx).map_err(|_| anyhow!("literal table exceeds u16 index space")) + } + + pub(super) fn load_literal(&mut self, value: Value, span: &crate::lexer::Span) -> Result { + let literal_idx = self.add_literal_u16(value)?; + let dest = self.alloc_register()?; + self.emit(Instruction::Load { dest, literal_idx }, span); + Ok(dest) + } + + pub(super) fn get_or_add_builtin_index(&mut self, name: &str, num_args: u16) -> u16 { + let key = format!("{}/{}", name, num_args); + if let Some(index) = self.builtin_index.get(&key) { + return *index; + } + + let index = self + .program + .add_builtin_info(crate::rvm::program::BuiltinInfo { + name: name.to_string(), + num_args, + }); + self.builtin_index.insert(key, index); + index + } + + pub(super) fn emit_builtin_call( + &mut self, + name: &str, + args: &[u8], + span: &crate::lexer::Span, + ) -> Result { + // TODO: Some ARM template functions are variadic (e.g. format, + // coalesce, union). If >8 args are needed, consider packing into an + // array or folding/chaining associative calls. + if args.len() > 8 { + bail!(span.error(&format!("builtin call {} exceeds max 8 args", name))); + } + + let dest = self.alloc_register()?; + let builtin_index = self.get_or_add_builtin_index( + name, + u16::try_from(args.len()).map_err(|_| anyhow!("arg count overflow"))?, + ); + + let mut arg_slots = [0_u8; 8]; + for (slot, arg) in arg_slots.iter_mut().zip(args.iter()) { + *slot = *arg; + } + + let params_index = self.program.add_builtin_call_params(BuiltinCallParams { + dest, + builtin_index, + num_args: u8::try_from(args.len()).map_err(|_| anyhow!("arg count overflow"))?, + args: arg_slots, + }); + + self.emit(Instruction::BuiltinCall { params_index }, span); + Ok(dest) + } + + pub(super) fn emit_chained_index_literal_path( + &mut self, + root: u8, + path: &[&str], + span: &crate::lexer::Span, + ) -> Result { + let dest = self.alloc_register()?; + + // TODO: Auto-parsing numeric-looking segments as u64 can mis-index + // object keys that happen to be digits (e.g. a tag named "123" would + // become numeric index 123). Consider carrying type metadata from + // `split_path_without_wildcards` or adding a string-only variant of + // this helper for object key lookups like tags. + let path_components = path + .iter() + .map(|segment| { + let value = segment + .parse::() + .map_or_else(|_| Value::from((*segment).to_string()), Value::from); + self.add_literal_u16(value).map(LiteralOrRegister::Literal) + }) + .collect::>>()?; + + let params_index = + self.program + .instruction_data + .add_chained_index_params(ChainedIndexParams { + dest, + root, + path_components, + }); + self.emit(Instruction::ChainedIndex { params_index }, span); + + Ok(dest) + } + + pub(super) fn load_input(&mut self, span: &crate::lexer::Span) -> Result { + if let Some(reg) = self.cached_input_reg { + return Ok(reg); + } + let dest = self.alloc_register()?; + self.emit(Instruction::LoadInput { dest }, span); + self.cached_input_reg = Some(dest); + Ok(dest) + } + + pub(super) fn load_context(&mut self, span: &crate::lexer::Span) -> Result { + if let Some(reg) = self.cached_context_reg { + return Ok(reg); + } + let dest = self.alloc_register()?; + self.emit(Instruction::LoadContext { dest }, span); + self.cached_context_reg = Some(dest); + Ok(dest) + } + + /// Emit a `CoalesceUndefinedToNull` instruction for the given register. + /// + /// In Azure Policy, a missing field is semantically `null`, not undefined. + pub(super) fn emit_coalesce_undefined_to_null( + &mut self, + register: u8, + span: &crate::lexer::Span, + ) { + self.emit(Instruction::CoalesceUndefinedToNull { register }, span); + } + /// Return the PC (instruction index) that the *next* emitted instruction /// will occupy. pub(super) fn current_pc(&self) -> Result { @@ -192,4 +331,54 @@ impl Compiler { } Ok(()) } + + // -- alias resolution -------------------------------------------------- + + pub(super) fn resolve_alias_path( + &self, + path: &str, + span: &crate::lexer::Span, + ) -> Result { + let lc = path.to_ascii_lowercase(); + if let Some(short) = self.alias_map.get(&lc) { + let resolved = short.clone(); + let result = Self::strip_fq_prefix(&resolved).to_ascii_lowercase(); + return Ok(result); + } + + // Fallback: derive array path from a corresponding `[*]` alias. + if !lc.contains("[*]") { + let wildcard_key = alloc::format!("{}[*]", lc); + if let Some(short) = self.alias_map.get(&wildcard_key) { + let resolved = Self::strip_fq_prefix(short).to_ascii_lowercase(); + if let Some(base) = resolved.strip_suffix("[*]") { + return Ok(base.to_string()); + } + } + } + + if !self.alias_map.is_empty() && !self.alias_fallback_to_raw { + bail!(span.error(&alloc::format!( + "unknown alias '{}': field references must use fully-qualified alias names when an alias catalog is loaded", + path + ))); + } + + if self.alias_map.is_empty() { + Ok(path.to_string()) + } else { + let result = Self::strip_fq_prefix(path).to_ascii_lowercase(); + Ok(result) + } + } + + /// Strip any resource-type prefix segments from a resolved alias short + /// name, keeping only the trailing property path. + pub(super) fn strip_fq_prefix(resolved: &str) -> String { + resolved + .rfind('/') + .and_then(|idx| resolved.get(idx.saturating_add(1)..)) + .unwrap_or(resolved) + .to_string() + } } diff --git a/src/languages/azure_policy/compiler/count.rs b/src/languages/azure_policy/compiler/count.rs index 42f6136..409ba6b 100644 --- a/src/languages/azure_policy/compiler/count.rs +++ b/src/languages/azure_policy/compiler/count.rs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![allow(dead_code)] +#![allow(dead_code, clippy::pattern_type_mismatch)] //! `count` / `count.where` loop compilation. //! @@ -13,9 +13,12 @@ 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 { + pub(super) fn compile_count(&mut self, count_node: &CountNode) -> Result { let _ = self; - bail!("count compilation not yet implemented") + let span = match count_node { + CountNode::Field { span, .. } | CountNode::Value { span, .. } => span, + }; + bail!(span.error("count compilation not yet implemented")) } pub(super) const fn try_compile_count_as_any( diff --git a/src/languages/azure_policy/compiler/count_bindings.rs b/src/languages/azure_policy/compiler/count_bindings.rs index 32644ec..2f573ff 100644 --- a/src/languages/azure_policy/compiler/count_bindings.rs +++ b/src/languages/azure_policy/compiler/count_bindings.rs @@ -21,20 +21,20 @@ impl Compiler { pub(super) fn compile_from_binding( &mut self, - _binding: CountBinding, + _binding: &CountBinding, _field_path: &str, - _span: &crate::lexer::Span, + span: &crate::lexer::Span, ) -> Result { let _ = self; - bail!("count binding compilation not yet implemented") + bail!(span.error("count binding compilation not yet implemented")) } pub(super) fn compile_current_reference( &mut self, _key: &str, - _span: &crate::lexer::Span, + span: &crate::lexer::Span, ) -> Result { let _ = self; - bail!("current() reference not yet implemented") + bail!(span.error("current() reference not yet implemented")) } } diff --git a/src/languages/azure_policy/compiler/expressions.rs b/src/languages/azure_policy/compiler/expressions.rs index 3e2b69e..27c3d59 100644 --- a/src/languages/azure_policy/compiler/expressions.rs +++ b/src/languages/azure_policy/compiler/expressions.rs @@ -1,53 +1,317 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![allow(dead_code)] +#![allow(clippy::pattern_type_mismatch)] //! Template-expression and call-expression compilation. -//! -//! Stub — real implementation added in a later commit. -use anyhow::{bail, Result}; +use alloc::vec::Vec; -use crate::languages::azure_policy::ast::{Expr, JsonValue, ValueOrExpr}; +use anyhow::{anyhow, bail, Result}; + +use crate::languages::azure_policy::ast::{Expr, ExprLiteral, JsonValue, ValueOrExpr}; +use crate::rvm::Instruction; +use crate::Value; use super::core::Compiler; +use super::utils::{extract_string_literal, json_value_to_runtime}; impl Compiler { pub(super) fn compile_value_or_expr( &mut self, - _voe: &ValueOrExpr, - _span: &crate::lexer::Span, + voe: &ValueOrExpr, + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("expression compilation not yet implemented") + match voe { + ValueOrExpr::Value(value) => self.compile_json_value(value, span), + ValueOrExpr::Expr { expr, .. } => self.compile_expr(expr), + } } pub(super) fn compile_json_value( &mut self, - _value: &JsonValue, - _span: &crate::lexer::Span, + value: &crate::languages::azure_policy::ast::JsonValue, + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("JSON value compilation not yet implemented") + // Arrays may contain ARM template expression strings that need + // runtime evaluation. + if let JsonValue::Array(_, items) = value { + if items.iter().any(|item| { + matches!(item, JsonValue::Str(_, s) if crate::languages::azure_policy::parser::is_template_expr(s)) + }) { + return self.compile_dynamic_array(items, span); + } + // Fall through: json_value_to_runtime handles `[[` unescaping for + // string elements, so static arrays are converted correctly. + } + let runtime_value = json_value_to_runtime(value)?; + self.load_literal(runtime_value, span) } - pub(super) fn compile_expr(&mut self, _expr: &Expr) -> Result { - let _ = self; - bail!("expression compilation not yet implemented") - } - - pub(super) fn compile_call_expr( + /// Compile a JSON array where some elements are ARM template expressions. + fn compile_dynamic_array( &mut self, - _span: &crate::lexer::Span, - _func: &str, - _args: &[Expr], + items: &[JsonValue], + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("call expression compilation not yet implemented") + use crate::languages::azure_policy::expr::ExprParser; + + let mut element_regs = Vec::with_capacity(items.len()); + for item in items { + let reg = if let JsonValue::Str(item_span, s) = item { + if crate::languages::azure_policy::parser::is_template_expr(s) { + let inner = s + .strip_prefix('[') + .and_then(|inner| inner.strip_suffix(']')) + .ok_or_else(|| { + item_span.error("invalid template expression: missing brackets") + })?; + let expr = ExprParser::parse_from_brackets(inner, item_span) + .map_err(|e| anyhow!("{}", e))?; + self.compile_expr(&expr)? + } else { + let runtime_value = json_value_to_runtime(item)?; + self.load_literal(runtime_value, item_span)? + } + } else { + let runtime_value = json_value_to_runtime(item)?; + self.load_literal(runtime_value, item.span())? + }; + element_regs.push(reg); + } + + let arr_dest = self.alloc_register()?; + let params = self.program.instruction_data.add_array_create_params( + crate::rvm::instructions::ArrayCreateParams { + dest: arr_dest, + elements: element_regs, + }, + ); + self.emit( + Instruction::ArrayCreate { + params_index: params, + }, + span, + ); + Ok(arr_dest) } - pub(super) fn compile_call_args(&mut self, _args: &[Expr]) -> Result> { - let _ = self; - bail!("call args compilation not yet implemented") + pub(super) fn compile_expr(&mut self, expr: &Expr) -> Result { + match expr { + Expr::Literal { span, value } => { + let v = match value { + ExprLiteral::Number(n) => Value::from_numeric_string(n)?, + ExprLiteral::String(s) => Value::from(s.clone()), + ExprLiteral::Bool(b) => Value::Bool(*b), + }; + self.load_literal(v, span) + } + Expr::Ident { name, span } => match name.to_ascii_lowercase().as_str() { + "true" => self.load_literal(Value::Bool(true), span), + "false" => self.load_literal(Value::Bool(false), span), + "null" => self.load_literal(Value::Null, span), + _ => bail!(span.error(&alloc::format!( + "unsupported bare identifier in template expression: {}", + name + ))), + }, + Expr::Call { span, func, args } => self.compile_call_expr(span, func, args), + Expr::Dot { + span, + object, + field, + .. + } => { + let object_reg = self.compile_expr(object)?; + let dest = self.alloc_register()?; + let literal_idx = self.add_literal_u16(Value::from(field.clone()))?; + self.emit( + Instruction::IndexLiteral { + dest, + container: object_reg, + literal_idx, + }, + span, + ); + Ok(dest) + } + Expr::Index { + span, + object, + index, + } => { + let object_reg = self.compile_expr(object)?; + let index_reg = self.compile_expr(index)?; + let dest = self.alloc_register()?; + self.emit( + Instruction::Index { + dest, + container: object_reg, + key: index_reg, + }, + span, + ); + Ok(dest) + } + } + } + + fn compile_call_expr( + &mut self, + span: &crate::lexer::Span, + func: &Expr, + args: &[Expr], + ) -> Result { + let Expr::Ident { name, .. } = func else { + bail!(span.error("unsupported dynamic function expression")); + }; + + let function_name = name.to_ascii_lowercase(); + + match function_name.as_str() { + "parameters" => { + let [first_arg] = args else { + bail!(span.error("parameters() requires exactly one argument")); + }; + let param_name = extract_string_literal(first_arg)?; + let input_reg = self.load_input(span)?; + let params_reg = + self.emit_chained_index_literal_path(input_reg, &["parameters"], span)?; + let defaults_reg = if let Some(reg) = self.cached_defaults_reg { + reg + } else { + let reg = if let Some(ref defaults) = self.parameter_defaults { + self.load_literal(defaults.clone(), span)? + } else { + self.load_literal(Value::new_object(), span)? + }; + self.cached_defaults_reg = Some(reg); + reg + }; + let name_reg = self.load_literal(Value::from(param_name), span)?; + self.emit_builtin_call( + "azure.policy.get_parameter", + &[params_reg, defaults_reg, name_reg], + span, + ) + } + "field" => { + let [first_arg] = args else { + bail!(span.error("field() requires exactly one argument")); + }; + let field_path = extract_string_literal(first_arg)?; + let resolved = match field_path.to_ascii_lowercase().as_str() { + "type" | "id" | "kind" | "name" | "location" | "fullname" | "tags" + | "identity.type" | "apiversion" => field_path.clone(), + s if s.starts_with("identity.") => field_path.clone(), + s if s.starts_with("tags.") || s.starts_with("tags[") => field_path.clone(), + _ => self.resolve_alias_path(&field_path, span)?, + }; + + // The field() template function always reads from the primary + // resource, even inside existenceCondition. + let saved_override = self.resource_override_reg.take(); + let reg = self.compile_field_path_expression(&resolved, span)?; + self.resource_override_reg = saved_override; + + let reg = if resolved.contains("[*]") { + if self.resolve_count_binding(&resolved)?.is_some() { + let arr = self.alloc_register()?; + self.emit(Instruction::ArrayNew { dest: arr }, span); + self.emit(Instruction::ArrayPush { arr, value: reg }, span); + arr + } else { + reg + } + } else { + reg + }; + + self.emit_coalesce_undefined_to_null(reg, span); + Ok(reg) + } + "current" => match args.first() { + Some(first_arg) => { + let key = extract_string_literal(first_arg)?; + self.compile_current_reference(&key, span) + } + None => { + let binding = self.count_bindings.last().ok_or_else(|| { + anyhow::anyhow!("{}", span.error("current() used outside a count scope")) + })?; + let current_reg = binding.current_reg; + let dest = self.alloc_register()?; + self.emit( + crate::rvm::Instruction::Move { + dest, + src: current_reg, + }, + span, + ); + Ok(dest) + } + }, + "resourcegroup" => { + if !args.is_empty() { + bail!(span.error("resourceGroup() takes no arguments")) + } + let ctx_reg = self.load_context(span)?; + self.emit_chained_index_literal_path(ctx_reg, &["resourceGroup"], span) + } + "subscription" => { + if !args.is_empty() { + bail!(span.error("subscription() takes no arguments")) + } + let ctx_reg = self.load_context(span)?; + self.emit_chained_index_literal_path(ctx_reg, &["subscription"], span) + } + "requestcontext" => { + if !args.is_empty() { + bail!(span.error("requestContext() takes no arguments")) + } + let ctx_reg = self.load_context(span)?; + self.emit_chained_index_literal_path(ctx_reg, &["requestContext"], span) + } + "claims" => { + if !args.is_empty() { + bail!(span.error("claims() takes no arguments")) + } + let ctx_reg = self.load_context(span)?; + self.emit_chained_index_literal_path(ctx_reg, &["claims"], span) + } + "policy" => { + if !args.is_empty() { + bail!(span.error("policy() takes no arguments")) + } + let ctx_reg = self.load_context(span)?; + self.emit_chained_index_literal_path(ctx_reg, &["policy"], span) + } + "utcnow" => { + if !args.is_empty() { + bail!(span.error("utcNow() takes no arguments")) + } + let ctx_reg = self.load_context(span)?; + self.emit_chained_index_literal_path(ctx_reg, &["utcNow"], span) + } + "concat" | "if" | "and" | "not" | "tolower" | "toupper" | "replace" | "substring" + | "length" | "add" | "equals" | "greaterorequals" | "lessorequals" | "contains" => self + .compile_arm_template_function(&function_name, span, args)? + .ok_or_else(|| anyhow!("{}", span.error("unreachable"))), + + other => { + if let Some(dest) = self.compile_arm_template_function(other, span, args)? { + Ok(dest) + } else { + bail!(span.error(&alloc::format!("unsupported template function '{}'", other))) + } + } + } + } + + pub(super) fn compile_call_args(&mut self, args: &[Expr]) -> Result> { + let mut out = Vec::with_capacity(args.len()); + for arg in args { + out.push(self.compile_expr(arg)?); + } + Ok(out) } } diff --git a/src/languages/azure_policy/compiler/fields.rs b/src/languages/azure_policy/compiler/fields.rs index 9c070bd..a99a530 100644 --- a/src/languages/azure_policy/compiler/fields.rs +++ b/src/languages/azure_policy/compiler/fields.rs @@ -1,56 +1,317 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![allow(dead_code)] +#![allow(clippy::pattern_type_mismatch)] //! Field-kind and resource-path compilation. -//! -//! Stub — real implementation added in a later commit. -use anyhow::{bail, Result}; +use alloc::format; +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; -use crate::languages::azure_policy::ast::FieldKind; +use anyhow::{anyhow, bail, Result}; + +use crate::languages::azure_policy::ast::{Expr, ExprLiteral, FieldKind}; +use crate::rvm::instructions::{LoopMode, LoopStartParams}; +use crate::rvm::Instruction; use super::core::Compiler; +use super::utils::{split_count_wildcard_path, split_path_without_wildcards}; impl Compiler { pub(super) fn compile_field_kind( &mut self, - _kind: &FieldKind, - _span: &crate::lexer::Span, + kind: &FieldKind, + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("field compilation not yet implemented") + let reg = match kind { + FieldKind::Type => { + self.record_field_kind("type"); + self.compile_resource_path_value("type", span)? + } + FieldKind::Id => { + self.record_field_kind("id"); + self.compile_resource_path_value("id", span)? + } + FieldKind::Kind => { + self.record_field_kind("kind"); + self.compile_resource_path_value("kind", span)? + } + FieldKind::Name => { + self.record_field_kind("name"); + self.compile_resource_path_value("name", span)? + } + FieldKind::Location => { + self.record_field_kind("location"); + self.compile_resource_path_value("location", span)? + } + FieldKind::FullName => { + self.record_field_kind("fullName"); + self.compile_resource_path_value("fullName", span)? + } + FieldKind::Tags => { + self.record_field_kind("tags"); + self.compile_resource_path_value("tags", span)? + } + FieldKind::IdentityType => { + self.record_field_kind("identity.type"); + self.compile_resource_path_value("identity.type", span)? + } + FieldKind::IdentityField(ref subpath) => { + let path = format!("identity.{}", subpath.to_ascii_lowercase()); + self.record_field_kind(&path); + self.compile_resource_path_value(&path, span)? + } + FieldKind::ApiVersion => { + self.record_field_kind("apiVersion"); + self.compile_resource_path_value("apiVersion", span)? + } + FieldKind::Tag(tag) => { + self.record_field_kind("tags"); + self.record_tag_name(tag); + let tag_lower = tag.to_ascii_lowercase(); + if let Some(override_reg) = self.resource_override_reg { + self.emit_chained_index_literal_path(override_reg, &["tags", &tag_lower], span)? + } else { + let input_reg = self.load_input(span)?; + self.emit_chained_index_literal_path( + input_reg, + &["resource", "tags", &tag_lower], + span, + )? + } + } + FieldKind::Alias(path) => { + self.record_alias(path); + let short = self.resolve_alias_path(path, span)?; + self.compile_field_path_expression(&short, span)? + } + FieldKind::Expr(expr) => self.compile_dynamic_field_expr(expr, span)?, + }; + Ok(reg) + } + + /// Compile a dynamic field expression (`FieldKind::Expr`). + fn compile_dynamic_field_expr(&mut self, expr: &Expr, span: &crate::lexer::Span) -> Result { + if let Expr::Call { func, args, .. } = expr { + if let Expr::Ident { name, .. } = func.as_ref() { + if name.eq_ignore_ascii_case("if") { + if let [cond_arg, Expr::Literal { + value: ExprLiteral::String(alias_a), + .. + }, Expr::Literal { + value: ExprLiteral::String(alias_b), + .. + }] = args.as_slice() + { + self.record_alias(alias_a); + self.record_alias(alias_b); + + let short_a = self.resolve_alias_path(alias_a, span)?; + let short_b = self.resolve_alias_path(alias_b, span)?; + + let cond_reg = self.compile_expr(cond_arg)?; + + let then_reg = self.compile_field_path_expression(&short_a, span)?; + self.emit_coalesce_undefined_to_null(then_reg, span); + + let else_reg = self.compile_field_path_expression(&short_b, span)?; + self.emit_coalesce_undefined_to_null(else_reg, span); + + return self.emit_builtin_call( + "azure.policy.if", + &[cond_reg, then_reg, else_reg], + span, + ); + } + } + } + } + + // Handle concat() that produces a tag path. + if let Expr::Call { func, args, .. } = expr { + if let Expr::Ident { name, .. } = func.as_ref() { + if name.eq_ignore_ascii_case("concat") && !args.is_empty() { + if let Some(Expr::Literal { + value: ExprLiteral::String(first), + .. + }) = args.first() + { + if first == "tags" + || first.starts_with("tags.") + || first.starts_with("tags[") + { + self.observed_has_dynamic_fields = true; + let path_reg = self.compile_expr(expr)?; + let resource_reg = self.compile_resource_root(span)?; + return self.emit_builtin_call( + "azure.policy.resolve_field", + &[resource_reg, path_reg], + span, + ); + } + } + } + } + } + + bail!(span.error( + "unsupported dynamic field expression; only \ + `if(cond, 'alias', 'alias')` and `concat('tags...', ...)` \ + patterns are supported", + )); } pub(super) fn compile_field_path_expression( &mut self, - _field_path: &str, - _span: &crate::lexer::Span, + field_path: &str, + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("field path compilation not yet implemented") + if let Some(binding) = self.resolve_count_binding(field_path)? { + return self.compile_from_binding(&binding, field_path, span); + } + if field_path.contains("[*]") { + return self.compile_field_wildcard_collect(field_path, span); + } + self.compile_resource_path_value(field_path, span) } pub(super) fn compile_resource_path_value( &mut self, - _field_path: &str, - _span: &crate::lexer::Span, + field_path: &str, + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("resource path compilation not yet implemented") + let lowered = field_path.to_ascii_lowercase(); + + if let Some(override_reg) = self.resource_override_reg { + let parts = split_path_without_wildcards(&lowered)?; + let refs = parts.iter().map(String::as_str).collect::>(); + return self.emit_chained_index_literal_path(override_reg, &refs, span); + } + + let input_reg = self.load_input(span)?; + + let mut path = Vec::new(); + path.push("resource".to_string()); + for part in split_path_without_wildcards(&lowered)? { + path.push(part); + } + + let refs = path.iter().map(String::as_str).collect::>(); + self.emit_chained_index_literal_path(input_reg, &refs, span) } - 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_resource_root(&mut self, span: &crate::lexer::Span) -> Result { + if let Some(override_reg) = self.resource_override_reg { + return Ok(override_reg); + } + let input_reg = self.load_input(span)?; + self.emit_chained_index_literal_path(input_reg, &["resource"], span) } + // -- wildcard collection ----------------------------------------------- + pub(super) fn compile_field_wildcard_collect( &mut self, - _field_path: &str, - _span: &crate::lexer::Span, + field_path: &str, + span: &crate::lexer::Span, ) -> Result { - let _ = self; - bail!("wildcard collect not yet implemented") + let result_reg = self.alloc_register()?; + self.emit(Instruction::ArrayNew { dest: result_reg }, span); + self.compile_wildcard_collect_inner(None, field_path, result_reg, span)?; + Ok(result_reg) + } + + fn compile_wildcard_collect_inner( + &mut self, + base_reg: Option, + remaining_path: &str, + result_reg: u8, + span: &crate::lexer::Span, + ) -> Result<()> { + let (prefix, suffix) = split_count_wildcard_path(remaining_path)?; + + let prefix_lower = prefix.to_ascii_lowercase(); + + let collection_reg = match base_reg { + Some(base) if prefix_lower.is_empty() => base, + Some(base) => { + let parts = split_path_without_wildcards(&prefix_lower)?; + let refs = parts.iter().map(String::as_str).collect::>(); + self.emit_chained_index_literal_path(base, &refs, span)? + } + None if prefix_lower.is_empty() => self.compile_resource_root(span)?, + None => self.compile_resource_path_value(&prefix_lower, 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"))?; + + match suffix { + Some(ref s) if s.contains("[*]") => { + self.compile_wildcard_collect_inner(Some(current_reg), s, result_reg, span)?; + } + Some(ref s) => { + let s_lower = s.to_ascii_lowercase(); + let parts = split_path_without_wildcards(&s_lower)?; + let refs = parts.iter().map(String::as_str).collect::>(); + let val_reg = self.emit_chained_index_literal_path(current_reg, &refs, span)?; + self.emit( + Instruction::ArrayPushDefined { + arr: result_reg, + value: val_reg, + }, + span, + ); + } + None => { + self.emit( + Instruction::ArrayPushDefined { + arr: result_reg, + value: current_reg, + }, + span, + ); + } + } + + 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(()) } } diff --git a/src/languages/azure_policy/compiler/mod.rs b/src/languages/azure_policy/compiler/mod.rs index 6e2265d..4f005cb 100644 --- a/src/languages/azure_policy/compiler/mod.rs +++ b/src/languages/azure_policy/compiler/mod.rs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#![allow(dead_code)] //! Azure Policy AST → RVM compiler. //! diff --git a/src/languages/azure_policy/compiler/template_dispatch.rs b/src/languages/azure_policy/compiler/template_dispatch.rs index 5d44916..0a0610d 100644 --- a/src/languages/azure_policy/compiler/template_dispatch.rs +++ b/src/languages/azure_policy/compiler/template_dispatch.rs @@ -1,25 +1,366 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![allow(dead_code)] +#![allow(clippy::pattern_type_mismatch)] -//! ARM template function dispatch. -//! -//! Stub — real implementation added in a later commit. +//! ARM template function dispatch — maps lowercased function names to +//! builtin calls or native instructions. -use anyhow::Result; +use alloc::vec::Vec; + +use anyhow::{bail, Result}; use crate::languages::azure_policy::ast::Expr; +use crate::rvm::instructions::PolicyOp; +use crate::rvm::Instruction; use super::core::Compiler; impl Compiler { - pub(super) const fn compile_arm_template_function( + /// Dispatch an ARM template function call by lowercased name. + /// + /// Returns `Ok(Some(dest))` if the function was handled, `Ok(None)` if + /// the name is not an ARM template function. + pub(super) fn compile_arm_template_function( &mut self, - _function_name: &str, - _span: &crate::lexer::Span, - _args: &[Expr], + function_name: &str, + span: &crate::lexer::Span, + args: &[Expr], ) -> Result> { - _ = self.register_counter; - Ok(None) + let dest = match function_name { + // -- Core ARM template functions -- + "concat" => { + let mut element_regs = Vec::with_capacity(args.len()); + for arg in args { + element_regs.push(self.compile_expr(arg)?); + } + let array_dest = self.alloc_register()?; + let array_params = self.program.instruction_data.add_array_create_params( + crate::rvm::instructions::ArrayCreateParams { + dest: array_dest, + elements: element_regs, + }, + ); + self.emit( + Instruction::ArrayCreate { + params_index: array_params, + }, + span, + ); + let delimiter_reg = self.load_literal(crate::Value::from(""), span)?; + self.emit_builtin_call("concat", &[delimiter_reg, array_dest], span)? + } + "if" => { + let [cond_arg, true_arg, false_arg] = args else { + bail!(span.error("if() requires three arguments")); + }; + let cond = self.compile_expr(cond_arg)?; + let when_true = self.compile_expr(true_arg)?; + let when_false = self.compile_expr(false_arg)?; + self.emit_builtin_call("azure.policy.if", &[cond, when_true, when_false], span)? + } + "and" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("azure.policy.logic_all", ®s, span)? + } + "not" => { + let [inner_arg] = args else { + bail!(span.error("not() requires one argument")); + }; + let inner = self.compile_expr(inner_arg)?; + let dest = self.alloc_register()?; + self.emit( + Instruction::PolicyCondition { + dest, + left: inner, + right: 0, + op: PolicyOp::Not, + }, + span, + ); + dest + } + "tolower" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("lower", ®s, span)? + } + "toupper" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("upper", ®s, span)? + } + "replace" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("replace", ®s, span)? + } + "substring" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("substring", ®s, span)? + } + "length" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("count", ®s, span)? + } + "add" => self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::Add { dest, left, right } + })?, + "equals" => self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::PolicyCondition { + dest, + left, + right, + op: PolicyOp::Equals, + } + })?, + "greaterorequals" => { + self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::PolicyCondition { + dest, + left, + right, + op: PolicyOp::GreaterOrEquals, + } + })? + } + "lessorequals" => self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::PolicyCondition { + dest, + left, + right, + op: PolicyOp::LessOrEquals, + } + })?, + "contains" => self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::PolicyCondition { + dest, + left, + right, + op: PolicyOp::Contains, + } + })?, + "greater" => self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::PolicyCondition { + dest, + left, + right, + op: PolicyOp::Greater, + } + })?, + "less" => self.emit_binary_instruction(args, span, |dest, left, right| { + Instruction::PolicyCondition { + dest, + left, + right, + op: PolicyOp::Less, + } + })?, + + // -- Logical functions -- + "or" => { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call("azure.policy.logic_any", ®s, span)? + } + "true" => { + if !args.is_empty() { + bail!(span.error("true() takes no arguments")); + } + self.load_literal(crate::Value::Bool(true), span)? + } + "false" => { + if !args.is_empty() { + bail!(span.error("false() takes no arguments")); + } + self.load_literal(crate::Value::Bool(false), span)? + } + + // -- Existing ARM template functions -- + "split" => self.emit_builtin_call_from_args("azure.policy.fn.split", args, span)?, + "empty" => self.emit_builtin_call_from_args("azure.policy.fn.empty", args, span)?, + "first" => self.emit_builtin_call_from_args("azure.policy.fn.first", args, span)?, + "last" => self.emit_builtin_call_from_args("azure.policy.fn.last", args, span)?, + "startswith" => { + self.emit_builtin_call_from_args("azure.policy.fn.starts_with", args, span)? + } + "endswith" => { + self.emit_builtin_call_from_args("azure.policy.fn.ends_with", args, span)? + } + "int" => self.emit_builtin_call_from_args("azure.policy.fn.int", args, span)?, + "string" => self.emit_builtin_call_from_args("azure.policy.fn.string", args, span)?, + "bool" => self.emit_builtin_call_from_args("azure.policy.fn.bool", args, span)?, + "padleft" => { + self.emit_builtin_call_from_args("azure.policy.fn.pad_left", args, span)? + } + "iprangecontains" => { + self.emit_builtin_call_from_args("azure.policy.fn.ip_range_contains", args, span)? + } + "createarray" => { + let mut element_regs = Vec::with_capacity(args.len()); + for arg in args { + element_regs.push(self.compile_expr(arg)?); + } + let array_dest = self.alloc_register()?; + let array_params = self.program.instruction_data.add_array_create_params( + crate::rvm::instructions::ArrayCreateParams { + dest: array_dest, + elements: element_regs, + }, + ); + self.emit( + Instruction::ArrayCreate { + params_index: array_params, + }, + span, + ); + array_dest + } + + // -- String functions -- + "indexof" => { + self.emit_builtin_call_from_args("azure.policy.fn.index_of", args, span)? + } + "lastindexof" => { + self.emit_builtin_call_from_args("azure.policy.fn.last_index_of", args, span)? + } + "trim" => self.emit_builtin_call_from_args("azure.policy.fn.trim", args, span)?, + "format" => self.emit_builtin_call_from_args("azure.policy.fn.format", args, span)?, + + // -- Encoding functions -- + "base64" => self.emit_builtin_call_from_args("azure.policy.fn.base64", args, span)?, + "base64tostring" => { + self.emit_builtin_call_from_args("azure.policy.fn.base64_to_string", args, span)? + } + "base64tojson" => { + self.emit_builtin_call_from_args("azure.policy.fn.base64_to_json", args, span)? + } + "uri" => self.emit_builtin_call_from_args("azure.policy.fn.uri", args, span)?, + "uricomponent" => { + self.emit_builtin_call_from_args("azure.policy.fn.uri_component", args, span)? + } + "uricomponenttostring" => self.emit_builtin_call_from_args( + "azure.policy.fn.uri_component_to_string", + args, + span, + )?, + "datauri" => { + self.emit_builtin_call_from_args("azure.policy.fn.data_uri", args, span)? + } + "datauritostring" => { + self.emit_builtin_call_from_args("azure.policy.fn.data_uri_to_string", args, span)? + } + + // -- Collection functions -- + "intersection" => { + self.emit_builtin_call_from_args("azure.policy.fn.intersection", args, span)? + } + "union" => self.emit_builtin_call_from_args("azure.policy.fn.union", args, span)?, + "take" => self.emit_builtin_call_from_args("azure.policy.fn.take", args, span)?, + "skip" => self.emit_builtin_call_from_args("azure.policy.fn.skip", args, span)?, + "range" => self.emit_builtin_call_from_args("azure.policy.fn.range", args, span)?, + "array" => self.emit_builtin_call_from_args("azure.policy.fn.array", args, span)?, + "coalesce" => { + self.emit_builtin_call_from_args("azure.policy.fn.coalesce", args, span)? + } + "createobject" => { + self.emit_builtin_call_from_args("azure.policy.fn.create_object", args, span)? + } + + // -- Numeric functions -- + "sub" => { + let [left_arg, right_arg] = args else { + bail!(span.error("sub() requires two arguments")); + }; + let left = self.compile_expr(left_arg)?; + let right = self.compile_expr(right_arg)?; + let dest = self.alloc_register()?; + self.emit(Instruction::Sub { dest, left, right }, span); + dest + } + "mul" => { + let [left_arg, right_arg] = args else { + bail!(span.error("mul() requires two arguments")); + }; + let left = self.compile_expr(left_arg)?; + let right = self.compile_expr(right_arg)?; + let dest = self.alloc_register()?; + self.emit(Instruction::Mul { dest, left, right }, span); + dest + } + "div" => { + let [_, _] = args else { + bail!(span.error("div() requires two arguments")); + }; + self.emit_builtin_call_from_args("azure.policy.fn.int_div", args, span)? + } + "mod" => { + let [_, _] = args else { + bail!(span.error("mod() requires two arguments")); + }; + self.emit_builtin_call_from_args("azure.policy.fn.int_mod", args, span)? + } + "min" => self.emit_builtin_call_from_args("azure.policy.fn.min", args, span)?, + "max" => self.emit_builtin_call_from_args("azure.policy.fn.max", args, span)?, + "float" => self.emit_builtin_call_from_args("azure.policy.fn.float", args, span)?, + + // -- JSON / misc functions -- + "json" => self.emit_builtin_call_from_args("azure.policy.fn.json", args, span)?, + "join" => self.emit_builtin_call_from_args("azure.policy.fn.join", args, span)?, + "guid" => self.emit_builtin_call_from_args("azure.policy.fn.guid", args, span)?, + "uniquestring" => { + self.emit_builtin_call_from_args("azure.policy.fn.unique_string", args, span)? + } + "items" => self.emit_builtin_call_from_args("azure.policy.fn.items", args, span)?, + "indexfromend" => { + self.emit_builtin_call_from_args("azure.policy.fn.index_from_end", args, span)? + } + "tryget" => self.emit_builtin_call_from_args("azure.policy.fn.try_get", args, span)?, + "tryindexfromend" => { + self.emit_builtin_call_from_args("azure.policy.fn.try_index_from_end", args, span)? + } + + // -- Date/Time functions -- + "datetimeadd" => { + self.emit_builtin_call_from_args("azure.policy.fn.date_time_add", args, span)? + } + "datetimefromepoch" => self.emit_builtin_call_from_args( + "azure.policy.fn.date_time_from_epoch", + args, + span, + )?, + "datetimetoepoch" => { + self.emit_builtin_call_from_args("azure.policy.fn.date_time_to_epoch", args, span)? + } + "adddays" => { + self.emit_builtin_call_from_args("azure.policy.fn.add_days", args, span)? + } + + _ => return Ok(None), + }; + Ok(Some(dest)) + } + + /// Compile arguments and emit a builtin call. + fn emit_builtin_call_from_args( + &mut self, + name: &str, + args: &[Expr], + span: &crate::lexer::Span, + ) -> Result { + let regs = self.compile_call_args(args)?; + self.emit_builtin_call(name, ®s, span) + } + + /// Compile a binary (2-arg) call and emit a native instruction. + fn emit_binary_instruction( + &mut self, + args: &[Expr], + span: &crate::lexer::Span, + make_instr: impl FnOnce(u8, u8, u8) -> Instruction, + ) -> Result { + let [left_arg, right_arg] = args else { + bail!(span.error("expected exactly two arguments")); + }; + let left = self.compile_expr(left_arg)?; + let right = self.compile_expr(right_arg)?; + let dest = self.alloc_register()?; + self.emit(make_instr(dest, left, right), span); + Ok(dest) } } diff --git a/src/languages/azure_policy/compiler/utils.rs b/src/languages/azure_policy/compiler/utils.rs index de9b74a..7a13103 100644 --- a/src/languages/azure_policy/compiler/utils.rs +++ b/src/languages/azure_policy/compiler/utils.rs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![allow(dead_code, clippy::pattern_type_mismatch, clippy::redundant_pub_crate)] +#![allow(dead_code, clippy::pattern_type_mismatch)] //! Free helper functions used by the Azure Policy compiler. @@ -27,17 +27,22 @@ pub(super) fn extract_string_literal(expr: &Expr) -> Result { 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))?; + .ok_or_else(|| anyhow!("wildcard path 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 [*]: {}", + "wildcard path must have a non-empty prefix before [*]: {}", path ); } - let after_wildcard = &rest[3..]; + let after_wildcard = rest.strip_prefix("[*]").ok_or_else(|| { + anyhow!( + "wildcard path could not be parsed after [*] split: {}", + path + ) + })?; if after_wildcard.contains("[*]") { bail!("nested [*] wildcards are not supported: {}", path); } @@ -145,14 +150,20 @@ pub(super) fn split_path_without_wildcards(path: &str) -> Result> { } /// Convert a parsed JSON value from the Azure Policy AST into a runtime [`Value`]. -pub(crate) fn json_value_to_runtime(value: &JsonValue) -> Result { +pub(super) 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::Str(_, s) => { + // Handle ARM template escape: `[[...` → `[...` + s.strip_prefix("[[").map_or_else( + || Ok(Value::from(s.clone())), + |unescaped| Ok(Value::from(alloc::format!("[{unescaped}"))), + ) + } JsonValue::Array(_, items) => { let mut out = Vec::with_capacity(items.len()); for item in items { @@ -269,6 +280,12 @@ mod tests { assert_eq!(v, Value::from("hello".to_string())); } + #[test] + fn json_string_double_bracket_escape() { + let v = json_value_to_runtime(&JsonValue::Str(dummy_span(), "[[escaped]".into())).unwrap(); + assert_eq!(v, Value::from("[escaped]".to_string())); + } + #[test] fn json_array() { let arr = JsonValue::Array( diff --git a/src/languages/azure_policy/mod.rs b/src/languages/azure_policy/mod.rs index 5b7e9a3..3405d6b 100644 --- a/src/languages/azure_policy/mod.rs +++ b/src/languages/azure_policy/mod.rs @@ -7,7 +7,7 @@ pub mod aliases; pub mod ast; #[cfg(feature = "rvm")] -pub mod compiler; +pub(crate) mod compiler; pub mod expr; pub mod parser; pub mod strings; diff --git a/src/rvm/vm/machine.rs b/src/rvm/vm/machine.rs index 7bd0398..a0bb5eb 100644 --- a/src/rvm/vm/machine.rs +++ b/src/rvm/vm/machine.rs @@ -552,6 +552,7 @@ impl RegoVM { } #[cfg(any(miri, not(feature = "allocator-memory-limits")))] + #[allow(clippy::unused_self, clippy::missing_const_for_fn)] pub(super) fn memory_check(&mut self) -> Result<()> { Ok(()) }