mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
490 lines
19 KiB
Rust
490 lines
19 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
#![allow(
|
|
clippy::indexing_slicing,
|
|
clippy::expect_used,
|
|
clippy::as_conversions,
|
|
clippy::unused_trait_names,
|
|
clippy::pattern_type_mismatch
|
|
)]
|
|
|
|
use super::{Compiler, CompilerError, Register, Result, WorklistEntry};
|
|
use crate::lexer::Span;
|
|
use crate::rvm::instructions::{
|
|
ChainedIndexParams, LiteralOrRegister, VirtualDataDocumentLookupParams,
|
|
};
|
|
use crate::rvm::Instruction;
|
|
use crate::Value;
|
|
use alloc::format;
|
|
use alloc::string::{String, ToString};
|
|
use alloc::vec;
|
|
use alloc::vec::Vec;
|
|
|
|
use crate::ast::{Expr, ExprRef};
|
|
|
|
/// Component of a reference chain - either a literal field or a dynamic expression
|
|
#[derive(Debug, Clone)]
|
|
pub(super) enum AccessComponent {
|
|
/// Static field access (e.g., .field_name)
|
|
Field(String),
|
|
/// Dynamic access (e.g., [expr])
|
|
Expression(ExprRef),
|
|
}
|
|
|
|
/// Root of a reference chain - either a named variable or another arbitrary expression
|
|
#[derive(Debug, Clone)]
|
|
pub(super) enum ReferenceRoot {
|
|
Variable(String),
|
|
Expression(ExprRef),
|
|
}
|
|
|
|
/// Represents a chained reference like data.a.b[expr].c[expr]
|
|
#[derive(Debug, Clone)]
|
|
pub(super) struct ReferenceChain {
|
|
/// The root of the chain (variable or arbitrary expression)
|
|
pub(super) root: ReferenceRoot,
|
|
/// Chain of field accesses - either literal field names or dynamic expressions
|
|
pub(super) components: Vec<AccessComponent>,
|
|
}
|
|
|
|
impl ReferenceChain {
|
|
/// Get the static prefix path (all literal components from the start)
|
|
pub(super) fn get_static_prefix(&self) -> Option<Vec<&str>> {
|
|
let ReferenceRoot::Variable(root) = &self.root else {
|
|
return None;
|
|
};
|
|
|
|
let mut prefix = vec![root.as_str()];
|
|
for component in &self.components {
|
|
match component {
|
|
AccessComponent::Field(field) => prefix.push(field.as_str()),
|
|
AccessComponent::Expression(_) => break,
|
|
}
|
|
}
|
|
Some(prefix)
|
|
}
|
|
}
|
|
|
|
/// Parse a chained reference expression into a ReferenceChain
|
|
pub(super) fn parse_reference_chain(expr: &ExprRef) -> Result<ReferenceChain> {
|
|
let mut components = Vec::new();
|
|
let mut current_expr = expr;
|
|
|
|
// Walk the chain backwards to collect components
|
|
loop {
|
|
match current_expr.as_ref() {
|
|
Expr::Var { span, .. } => {
|
|
// Found the root variable
|
|
let root = ReferenceRoot::Variable(span.text().to_string());
|
|
components.reverse(); // We built backwards, so reverse
|
|
return Ok(ReferenceChain { root, components });
|
|
}
|
|
Expr::RefDot { refr, field, .. } => {
|
|
let (span, _) = field;
|
|
components.push(AccessComponent::Field(span.text().to_string()));
|
|
current_expr = refr;
|
|
}
|
|
Expr::RefBrack { refr, index, .. } => {
|
|
// Bracket access - check if it's a string literal or dynamic
|
|
match index.as_ref() {
|
|
Expr::String { span, .. } => {
|
|
// String literal - treat as static field
|
|
components.push(AccessComponent::Field(span.text().to_string()));
|
|
}
|
|
_ => {
|
|
// Dynamic expression
|
|
components.push(AccessComponent::Expression(index.clone()));
|
|
}
|
|
}
|
|
current_expr = refr;
|
|
}
|
|
_ => {
|
|
// Fallback root expression (e.g., array literal, function call)
|
|
components.reverse();
|
|
return Ok(ReferenceChain {
|
|
root: ReferenceRoot::Expression(current_expr.clone()),
|
|
components,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Compiler<'a> {
|
|
/// Compile chained reference expressions (Var, RefDot, RefBrack chains)
|
|
/// Uses ReferenceChain to analyze and optimize the access pattern
|
|
pub(super) fn compile_chained_ref(&mut self, expr: &ExprRef, span: &Span) -> Result<Register> {
|
|
// Parse the expression into a reference chain
|
|
let chain = parse_reference_chain(expr)?;
|
|
|
|
match chain.root.clone() {
|
|
ReferenceRoot::Variable(name) => match name.as_str() {
|
|
"input" => self.compile_input_chain(&chain, span),
|
|
"data" => self.compile_data_chain(&chain, span),
|
|
_ => self.compile_local_var_chain(&name, &chain, span),
|
|
},
|
|
ReferenceRoot::Expression(root_expr) => {
|
|
let root_reg =
|
|
self.compile_rego_expr_with_span(&root_expr, root_expr.span(), false)?;
|
|
self.compile_chain_access(root_reg, &chain.components, span)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compile input variable access chain
|
|
fn compile_input_chain(&mut self, chain: &ReferenceChain, span: &Span) -> Result<Register> {
|
|
let input_reg = self.resolve_variable("input", span)?;
|
|
|
|
if chain.components.is_empty() {
|
|
// Just "input"
|
|
return Ok(input_reg);
|
|
}
|
|
|
|
self.compile_chain_access(input_reg, &chain.components, span)
|
|
}
|
|
|
|
/// Compile data namespace access chain (may involve rules)
|
|
fn compile_data_chain(&mut self, chain: &ReferenceChain, span: &Span) -> Result<Register> {
|
|
if chain.components.is_empty() {
|
|
// Just "data" - direct access to data root is not allowed
|
|
return Err(CompilerError::DirectDataAccess.at(span));
|
|
}
|
|
|
|
// Build the static prefix path components for rule matching
|
|
let static_prefix = chain
|
|
.get_static_prefix()
|
|
.expect("data references must have variable roots");
|
|
|
|
// Try to find the longest matching rule prefix
|
|
// Start from the full path and work backwards
|
|
for i in (1..static_prefix.len()).rev() {
|
|
// Start from 1 to skip just "data"
|
|
let rule_candidate = static_prefix[0..=i].join(".");
|
|
|
|
if let Ok(rule_index) = self.get_or_assign_rule_index(&rule_candidate) {
|
|
// Found a rule match! Call the rule
|
|
let rule_result_reg = self.alloc_register();
|
|
self.emit_instruction(
|
|
Instruction::CallRule {
|
|
dest: rule_result_reg,
|
|
rule_index,
|
|
},
|
|
span,
|
|
);
|
|
|
|
// Handle remaining components after the matched rule
|
|
let consumed_components = i;
|
|
if consumed_components < chain.components.len() {
|
|
let remaining_components = &chain.components[consumed_components..];
|
|
return self.compile_chain_access(rule_result_reg, remaining_components, span);
|
|
}
|
|
|
|
return Ok(rule_result_reg);
|
|
}
|
|
}
|
|
|
|
// Check if this path could be a prefix of any rules (for virtual document lookup)
|
|
// Convert the full chain to a pattern that includes wildcards for dynamic components
|
|
let path_pattern = self.create_path_pattern(&chain.components);
|
|
let matching_rules: Vec<String> = self
|
|
.policy
|
|
.inner
|
|
.rules
|
|
.keys()
|
|
.filter(|rule_path| self.matches_path_pattern(rule_path, &path_pattern))
|
|
.cloned()
|
|
.collect();
|
|
|
|
if !matching_rules.is_empty() {
|
|
// This path is a prefix of some rules - use DataVirtualDocumentLookup
|
|
for rule_path in &matching_rules {
|
|
if !self
|
|
.rule_worklist
|
|
.iter()
|
|
.any(|entry| entry.rule_path == *rule_path)
|
|
{
|
|
// Assign a rule index for this rule before adding to worklist
|
|
self.get_or_assign_rule_index(rule_path)?;
|
|
let entry =
|
|
WorklistEntry::new(rule_path.clone(), self.current_call_stack.clone());
|
|
self.rule_worklist.push(entry);
|
|
}
|
|
}
|
|
|
|
return self.compile_data_virtual_lookup(&chain.components, span);
|
|
}
|
|
|
|
// No rules involved - simple data access
|
|
let data_reg = self.resolve_variable("data", span)?;
|
|
self.compile_chain_access(data_reg, &chain.components, span)
|
|
}
|
|
|
|
/// Create a path pattern from access components, using '*' for dynamic components
|
|
/// e.g., [Field("a"), Expression(...), Field("b")] becomes "data.a.*.b"
|
|
fn create_path_pattern(&self, components: &[AccessComponent]) -> String {
|
|
let mut pattern_parts = vec!["data"];
|
|
|
|
for component in components {
|
|
match component {
|
|
AccessComponent::Field(field) => pattern_parts.push(field.as_str()),
|
|
AccessComponent::Expression(_) => pattern_parts.push("*"),
|
|
}
|
|
}
|
|
|
|
pattern_parts.join(".")
|
|
}
|
|
|
|
/// Check if a rule path matches the given pattern with wildcards
|
|
/// e.g., "data.test.users.alice_profile" matches "data.test.users.*"
|
|
fn matches_path_pattern(&self, rule_path: &str, pattern: &str) -> bool {
|
|
// Use simple string matching implementation that handles wildcard patterns
|
|
if pattern.contains('*') {
|
|
self.simple_wildcard_match(rule_path, pattern)
|
|
} else {
|
|
// Simple prefix match for patterns without wildcards
|
|
rule_path.starts_with(&format!("{}.", pattern)) || rule_path == pattern
|
|
}
|
|
}
|
|
|
|
/// Simple wildcard matching without regex dependencies
|
|
/// Checks if a rule path could be a prefix of the access pattern
|
|
/// e.g., rule "data.test.users.alice_data" matches pattern "data.*.*.*.*.* because
|
|
/// the rule could be accessed with the first 4 components of the pattern
|
|
/// Ensures exact component matching - "fee" will NOT match "feed"
|
|
fn simple_wildcard_match(&self, rule_path: &str, access_pattern: &str) -> bool {
|
|
let rule_parts: Vec<&str> = rule_path.split('.').collect();
|
|
let pattern_parts: Vec<&str> = access_pattern.split('.').collect();
|
|
|
|
// Check how many components of the pattern the rule can match
|
|
let match_length = rule_parts.len().min(pattern_parts.len());
|
|
|
|
// Check if the rule matches the pattern up to the available components
|
|
for i in 0..match_length {
|
|
let rule_part = rule_parts[i];
|
|
let pattern_part = pattern_parts[i];
|
|
|
|
if pattern_part == "*" {
|
|
// Wildcard in pattern matches any non-empty rule component exactly
|
|
if rule_part.is_empty() {
|
|
return false;
|
|
}
|
|
} else if rule_part != pattern_part {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Rule matches if either:
|
|
// 1. It's at least as long as the pattern, OR
|
|
// 2. It matches all available components and the remaining pattern parts are wildcards
|
|
rule_parts.len() >= pattern_parts.len()
|
|
|| (match_length > 0
|
|
&& pattern_parts[match_length..]
|
|
.iter()
|
|
.all(|&part| part == "*"))
|
|
}
|
|
|
|
/// Compile local variable access chain
|
|
fn compile_local_var_chain(
|
|
&mut self,
|
|
root: &str,
|
|
chain: &ReferenceChain,
|
|
span: &Span,
|
|
) -> Result<Register> {
|
|
// Check if it's a local variable first (precedence over rules)
|
|
if let Some(var_reg) = self.lookup_variable(root) {
|
|
if chain.components.is_empty() {
|
|
return Ok(var_reg);
|
|
}
|
|
return self.compile_chain_access(var_reg, &chain.components, span);
|
|
}
|
|
|
|
// Check if there's a rule in the current package that matches
|
|
let current_pkg_prefix = format!("{}.{}", &self.current_package, root);
|
|
|
|
// Build static path for rule matching
|
|
let mut rule_path_parts = vec![current_pkg_prefix.as_str()];
|
|
for component in &chain.components {
|
|
match component {
|
|
AccessComponent::Field(field) => rule_path_parts.push(field.as_str()),
|
|
AccessComponent::Expression(_) => break, // Stop at first dynamic component
|
|
}
|
|
}
|
|
|
|
// Try to find the longest matching rule prefix
|
|
for i in (0..rule_path_parts.len()).rev() {
|
|
let rule_candidate = rule_path_parts[0..=i].join(".");
|
|
|
|
if let Ok(rule_index) = self.get_or_assign_rule_index(&rule_candidate) {
|
|
let rule_result_reg = self.alloc_register();
|
|
self.emit_instruction(
|
|
Instruction::CallRule {
|
|
dest: rule_result_reg,
|
|
rule_index,
|
|
},
|
|
span,
|
|
);
|
|
|
|
// Handle remaining components after the matched rule
|
|
let consumed_components = i; // Number of components consumed by the rule (excluding root)
|
|
if consumed_components < chain.components.len() {
|
|
let remaining_components = &chain.components[consumed_components..];
|
|
return self.compile_chain_access(rule_result_reg, remaining_components, span);
|
|
}
|
|
|
|
return Ok(rule_result_reg);
|
|
}
|
|
}
|
|
|
|
// No rule found; fall back to module-level imports.
|
|
let import_key = format!("{}.{}", &self.current_package, root);
|
|
if let Some(import_expr) = self.policy.inner.imports.get(&import_key) {
|
|
if let Ok(mut import_chain) = parse_reference_chain(import_expr) {
|
|
if let ReferenceRoot::Variable(import_root) = &import_chain.root {
|
|
if import_root == "data" {
|
|
import_chain.components.extend(chain.components.clone());
|
|
return self.compile_data_chain(&import_chain, span);
|
|
}
|
|
}
|
|
}
|
|
|
|
let import_reg =
|
|
self.compile_rego_expr_with_span(import_expr, import_expr.span(), false)?;
|
|
if chain.components.is_empty() {
|
|
return Ok(import_reg);
|
|
}
|
|
return self.compile_chain_access(import_reg, &chain.components, span);
|
|
}
|
|
|
|
// No rule or import found - undefined variable
|
|
Err(CompilerError::UndefinedVariable {
|
|
name: root.to_string(),
|
|
}
|
|
.at(span))
|
|
}
|
|
|
|
/// Compile chain access using appropriate instructions based on chain length and complexity
|
|
fn compile_chain_access(
|
|
&mut self,
|
|
root_reg: Register,
|
|
components: &[AccessComponent],
|
|
span: &Span,
|
|
) -> Result<Register> {
|
|
if components.is_empty() {
|
|
return Ok(root_reg);
|
|
}
|
|
|
|
if components.len() == 1 {
|
|
// Single level access - use optimized instructions
|
|
match &components[0] {
|
|
AccessComponent::Field(field) => {
|
|
let dest_reg = self.alloc_register();
|
|
let literal_idx = self.add_literal(Value::String(field.clone().into()));
|
|
self.emit_instruction(
|
|
Instruction::IndexLiteral {
|
|
dest: dest_reg,
|
|
container: root_reg,
|
|
literal_idx,
|
|
},
|
|
span,
|
|
);
|
|
Ok(dest_reg)
|
|
}
|
|
AccessComponent::Expression(expr) => {
|
|
let dest_reg = self.alloc_register();
|
|
let key_reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?;
|
|
self.emit_instruction(
|
|
Instruction::Index {
|
|
dest: dest_reg,
|
|
container: root_reg,
|
|
key: key_reg,
|
|
},
|
|
span,
|
|
);
|
|
Ok(dest_reg)
|
|
}
|
|
}
|
|
} else {
|
|
// Multi-level access - use ChainedIndex
|
|
let dest_reg = self.alloc_register();
|
|
let mut path_components = Vec::new();
|
|
|
|
for component in components {
|
|
match component {
|
|
AccessComponent::Field(field) => {
|
|
let literal_idx = self.add_literal(Value::String(field.clone().into()));
|
|
path_components.push(LiteralOrRegister::Literal(literal_idx));
|
|
}
|
|
AccessComponent::Expression(expr) => {
|
|
let reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?;
|
|
path_components.push(LiteralOrRegister::Register(reg));
|
|
}
|
|
}
|
|
}
|
|
|
|
let params = ChainedIndexParams {
|
|
dest: dest_reg,
|
|
root: root_reg,
|
|
path_components,
|
|
};
|
|
|
|
let params_index = self.program.instruction_data.chained_index_params.len() as u16;
|
|
self.program
|
|
.instruction_data
|
|
.chained_index_params
|
|
.push(params);
|
|
|
|
self.emit_instruction(Instruction::ChainedIndex { params_index }, span);
|
|
|
|
Ok(dest_reg)
|
|
}
|
|
}
|
|
|
|
/// Compile data virtual document lookup for rule-involved data access
|
|
fn compile_data_virtual_lookup(
|
|
&mut self,
|
|
components: &[AccessComponent],
|
|
span: &Span,
|
|
) -> Result<Register> {
|
|
let dest_reg = self.alloc_register();
|
|
let mut path_components = Vec::new();
|
|
|
|
for component in components {
|
|
match component {
|
|
AccessComponent::Field(field) => {
|
|
let literal_idx = self.add_literal(Value::String(field.clone().into()));
|
|
path_components.push(LiteralOrRegister::Literal(literal_idx));
|
|
}
|
|
AccessComponent::Expression(expr) => {
|
|
let reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?;
|
|
path_components.push(LiteralOrRegister::Register(reg));
|
|
}
|
|
}
|
|
}
|
|
|
|
let params = VirtualDataDocumentLookupParams {
|
|
dest: dest_reg,
|
|
path_components,
|
|
};
|
|
|
|
let params_index = self
|
|
.program
|
|
.instruction_data
|
|
.virtual_data_document_lookup_params
|
|
.len() as u16;
|
|
self.program
|
|
.instruction_data
|
|
.virtual_data_document_lookup_params
|
|
.push(params);
|
|
|
|
self.emit_instruction(
|
|
Instruction::VirtualDataDocumentLookup { params_index },
|
|
span,
|
|
);
|
|
|
|
// Set flag indicating runtime recursion check is needed
|
|
self.program.needs_runtime_recursion_check = true;
|
|
|
|
Ok(dest_reg)
|
|
}
|
|
}
|