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.
This commit is contained in:
Anand Krishnamoorthi
2026-04-21 16:51:08 -05:00
committed by GitHub
parent f727096a1d
commit f50a9744ff
12 changed files with 1600 additions and 86 deletions

View File

@@ -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<u8> {
let _ = self;
bail!("condition compilation not yet implemented")
pub(super) fn compile_constraint(&mut self, constraint: &Constraint) -> Result<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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)
}
}

View File

@@ -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<Option<String>> {
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<Option<(CountBinding, String)>> {
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<u8> {
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<u8>,
remaining_path: &str,
rhs_reg: u8,
condition: &Condition,
) -> Result<u8> {
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::<Vec<_>>();
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::<Vec<_>>();
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)
}
}

View File

@@ -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<String, bool>,
/// Default values for policy parameters.
pub(super) parameter_defaults: Option<Value>,
/// Cached register for the parameter defaults literal.
pub(super) cached_defaults_reg: Option<u8>,
/// When set, field conditions resolve against this register instead of
/// `input.resource`. Used for `existenceCondition`.
pub(super) resource_override_reg: Option<u8>,
@@ -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<u16> {
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<u8> {
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<u8> {
// 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<u8> {
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::<u64>()
.map_or_else(|_| Value::from((*segment).to_string()), Value::from);
self.add_literal_u16(value).map(LiteralOrRegister::Literal)
})
.collect::<Result<Vec<_>>>()?;
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<u8> {
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<u8> {
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<u16> {
@@ -192,4 +331,54 @@ impl Compiler {
}
Ok(())
}
// -- alias resolution --------------------------------------------------
pub(super) fn resolve_alias_path(
&self,
path: &str,
span: &crate::lexer::Span,
) -> Result<String> {
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()
}
}

View File

@@ -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<u8> {
pub(super) fn compile_count(&mut self, count_node: &CountNode) -> Result<u8> {
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(

View File

@@ -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<u8> {
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<u8> {
let _ = self;
bail!("current() reference not yet implemented")
bail!(span.error("current() reference not yet implemented"))
}
}

View File

@@ -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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<alloc::vec::Vec<u8>> {
let _ = self;
bail!("call args compilation not yet implemented")
pub(super) fn compile_expr(&mut self, expr: &Expr) -> Result<u8> {
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<u8> {
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<Vec<u8>> {
let mut out = Vec::with_capacity(args.len());
for arg in args {
out.push(self.compile_expr(arg)?);
}
Ok(out)
}
}

View File

@@ -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<u8> {
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<u8> {
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<u8> {
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<u8> {
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::<Vec<_>>();
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::<Vec<_>>();
self.emit_chained_index_literal_path(input_reg, &refs, span)
}
pub(super) fn compile_resource_root(&mut self, _span: &crate::lexer::Span) -> Result<u8> {
let _ = self;
bail!("resource root compilation not yet implemented")
pub(super) fn compile_resource_root(&mut self, span: &crate::lexer::Span) -> Result<u8> {
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<u8> {
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<u8>,
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::<Vec<_>>();
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::<Vec<_>>();
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(())
}
}

View File

@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Azure Policy AST → RVM compiler.
//!

View File

@@ -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<Option<u8>> {
_ = 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", &regs, 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", &regs, span)?
}
"toupper" => {
let regs = self.compile_call_args(args)?;
self.emit_builtin_call("upper", &regs, span)?
}
"replace" => {
let regs = self.compile_call_args(args)?;
self.emit_builtin_call("replace", &regs, span)?
}
"substring" => {
let regs = self.compile_call_args(args)?;
self.emit_builtin_call("substring", &regs, span)?
}
"length" => {
let regs = self.compile_call_args(args)?;
self.emit_builtin_call("count", &regs, 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", &regs, 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<u8> {
let regs = self.compile_call_args(args)?;
self.emit_builtin_call(name, &regs, 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<u8> {
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)
}
}

View File

@@ -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<String> {
pub(super) fn split_count_wildcard_path(path: &str) -> Result<(String, Option<String>)> {
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<Vec<String>> {
}
/// Convert a parsed JSON value from the Azure Policy AST into a runtime [`Value`].
pub(crate) fn json_value_to_runtime(value: &JsonValue) -> Result<Value> {
pub(super) fn json_value_to_runtime(value: &JsonValue) -> Result<Value> {
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(

View File

@@ -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;

View File

@@ -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(())
}