mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
f727096a1d
commit
f50a9744ff
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! Azure Policy AST → RVM compiler.
|
||||
//!
|
||||
|
||||
@@ -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", ®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<u8> {
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user