// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #![allow(clippy::pattern_type_mismatch)] //! Template-expression and call-expression compilation. use alloc::format; use alloc::vec::Vec; 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}; /// Maximum nesting depth for recursive JSON value compilation. const MAX_JSON_DEPTH: usize = 32; impl Compiler { pub(super) fn compile_value_or_expr( &mut self, voe: &ValueOrExpr, span: &crate::lexer::Span, ) -> Result { match voe { // The parser's `json_to_value_or_expr` already resolved template // expressions and unescaped `[[` → `[` literals. Skip the // top-level template-expression check so an unescaped string like // `"[not-an-expression]"` (originally `"[[not-an-expression]"`) is // not re-parsed as a template expression. Nested arrays/objects // still get full template-expression handling at depth > 0. ValueOrExpr::Value(value) => self.compile_json_value_inner(value, span, 0, true), ValueOrExpr::Expr { expr, .. } => self.compile_expr(expr), } } pub(super) fn compile_json_value( &mut self, value: &crate::languages::azure_policy::ast::JsonValue, span: &crate::lexer::Span, ) -> Result { self.compile_json_value_inner(value, span, 0, false) } /// Compile a JSON value to a register. /// /// `resolved_top` — when `true`, the top-level string has already been /// through `json_to_value_or_expr` (template expressions extracted, `[[` /// unescaped). Skip the template-expression check at this level so that /// an unescaped `"[literal]"` is not re-parsed. Recursive calls for /// array elements and object values always pass `false` since those /// nested values have not been pre-resolved. fn compile_json_value_inner( &mut self, value: &crate::languages::azure_policy::ast::JsonValue, span: &crate::lexer::Span, depth: usize, resolved_top: bool, ) -> Result { if depth > MAX_JSON_DEPTH { bail!(span.error(&format!( "JSON value nesting exceeds maximum depth of {MAX_JSON_DEPTH}" ))); } use crate::languages::azure_policy::expr::ExprParser; use crate::languages::azure_policy::parser::is_template_expr; // Standalone string template expressions like `"[concat(...)]"` // must be compiled so they evaluate at runtime. Skip this check // when the caller has already resolved template expressions (e.g. // values coming from `ValueOrExpr::Value`). if !resolved_top { if let JsonValue::Str(str_span, s) = value { if is_template_expr(s) { let inner = s .strip_prefix('[') .and_then(|inner| inner.strip_suffix(']')) .ok_or_else(|| { str_span.error("invalid template expression: missing brackets") })?; let expr = ExprParser::parse_from_brackets(inner, str_span) .map_err(|e| anyhow!("{}", e))?; return self.compile_expr(&expr); } } } // Arrays: recursively compile elements so nested template expressions // are evaluated at runtime. if let JsonValue::Array(_, items) = value { if contains_template_expr(value) { return self.compile_dynamic_array(items, span, depth.saturating_add(1)); } // Fall through: json_value_to_runtime handles `[[` unescaping for // string elements, so static arrays are converted correctly. } // Objects: recursively compile values so nested template expressions // are evaluated at runtime. if let JsonValue::Object(_, entries) = value { if contains_template_expr(value) { return self.compile_dynamic_object(entries, span, depth.saturating_add(1)); } } // Static value — convert to runtime literal. // Enforce depth limit on static JSON to prevent stack overflow in // json_value_to_runtime's own recursion. Use subtree-local depth (0), // not the compiler recursion depth, since the static subtree's nesting // is independent of how deep we are in dynamic compilation. check_json_depth(value, 0).map_err(|_| { anyhow!(span.error(&alloc::format!( "JSON value nesting exceeds maximum depth of {MAX_JSON_DEPTH}" ))) })?; let runtime_value = json_value_to_runtime(value)?; self.load_literal(runtime_value, span) } /// Compile a JSON array where some elements may contain template expressions. fn compile_dynamic_array( &mut self, items: &[JsonValue], span: &crate::lexer::Span, depth: usize, ) -> Result { let mut element_regs = Vec::with_capacity(items.len()); for item in items { let reg = self.compile_json_value_inner(item, item.span(), depth, false)?; 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) } /// Compile a JSON object where some values may contain template expressions. fn compile_dynamic_object( &mut self, entries: &[crate::languages::azure_policy::ast::ObjectEntry], span: &crate::lexer::Span, depth: usize, ) -> Result { let mut keys: Vec<(u16, u8)> = Vec::with_capacity(entries.len()); for entry in entries { let val_reg = self.compile_json_value_inner(&entry.value, entry.value.span(), depth, false)?; let key_idx = self.add_literal_u16(Value::from(entry.key.clone()))?; keys.push((key_idx, val_reg)); } super::effects::build_object_from_keys(self, keys, span) } 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_literal_idx = match self.cached_defaults_literal_idx { Some(idx) => idx, None => { let val = self .parameter_defaults .clone() .unwrap_or_else(Value::new_object); let idx = self.add_literal_u16(val)?; self.cached_defaults_literal_idx = Some(idx); idx } }; let defaults_reg = self.alloc_register()?; self.emit( Instruction::Load { dest: defaults_reg, literal_idx: defaults_literal_idx, }, span, ); 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) } } /// Recursively check whether a JSON value tree contains any template /// expression strings (e.g. `"[parameters('x')]"`). /// /// Returns `false` (conservatively safe) if nesting exceeds [`MAX_JSON_DEPTH`]. fn contains_template_expr(value: &JsonValue) -> bool { contains_template_expr_inner(value, 0) } fn contains_template_expr_inner(value: &JsonValue, depth: usize) -> bool { if depth > MAX_JSON_DEPTH { return false; } use crate::languages::azure_policy::parser::is_template_expr; match value { JsonValue::Str(_, s) => is_template_expr(s), JsonValue::Array(_, items) => items .iter() .any(|item| contains_template_expr_inner(item, depth.saturating_add(1))), JsonValue::Object(_, entries) => entries .iter() .any(|e| contains_template_expr_inner(&e.value, depth.saturating_add(1))), _ => false, } } /// Verify that a JSON value tree does not exceed the maximum nesting depth. /// /// Called before handing a static value to [`json_value_to_runtime`] so that /// its unbounded recursion cannot overflow the stack. Also used by /// `build_parameter_defaults` to guard parameter default values. pub(super) fn check_json_depth(value: &JsonValue, current_depth: usize) -> Result<()> { if current_depth > MAX_JSON_DEPTH { bail!("JSON value nesting exceeds maximum depth of {MAX_JSON_DEPTH}"); } match value { JsonValue::Array(_, items) => { for item in items { check_json_depth(item, current_depth.saturating_add(1))?; } } JsonValue::Object(_, entries) => { for entry in entries { check_json_depth(&entry.value, current_depth.saturating_add(1))?; } } _ => {} } Ok(()) }