Skip to content

Commit 252ae0e

Browse files
authored
Merge pull request microsoft#516 from anakrish/rvm-opa-2
Handle more OPA semantics in RVM and compiler
2 parents 2a75b3b + bedf667 commit 252ae0e

12 files changed

Lines changed: 250 additions & 49 deletions

File tree

src/languages/rego/compiler/destructuring.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ impl<'a> Compiler<'a> {
9999
},
100100
span,
101101
);
102+
if !self.soft_assert_mode {
103+
self.emit_instruction(Instruction::AssertCondition { condition: dest }, span);
104+
}
102105
Ok(dest)
103106
}
104107
AssignmentPlan::WildcardMatch {

src/languages/rego/compiler/references.rs

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,37 @@ pub(super) enum AccessComponent {
2424
Expression(ExprRef),
2525
}
2626

27+
/// Root of a reference chain - either a named variable or another arbitrary expression
28+
#[derive(Debug, Clone)]
29+
pub(super) enum ReferenceRoot {
30+
Variable(String),
31+
Expression(ExprRef),
32+
}
33+
2734
/// Represents a chained reference like data.a.b[expr].c[expr]
2835
#[derive(Debug, Clone)]
2936
pub(super) struct ReferenceChain {
30-
/// The root variable (e.g., "data", "input", "local_var")
31-
pub(super) root: String,
37+
/// The root of the chain (variable or arbitrary expression)
38+
pub(super) root: ReferenceRoot,
3239
/// Chain of field accesses - either literal field names or dynamic expressions
3340
pub(super) components: Vec<AccessComponent>,
3441
}
3542

3643
impl ReferenceChain {
3744
/// Get the static prefix path (all literal components from the start)
38-
pub(super) fn get_static_prefix(&self) -> Vec<&str> {
39-
let mut prefix = vec![self.root.as_str()];
45+
pub(super) fn get_static_prefix(&self) -> Option<Vec<&str>> {
46+
let ReferenceRoot::Variable(root) = &self.root else {
47+
return None;
48+
};
49+
50+
let mut prefix = vec![root.as_str()];
4051
for component in &self.components {
4152
match component {
4253
AccessComponent::Field(field) => prefix.push(field.as_str()),
4354
AccessComponent::Expression(_) => break,
4455
}
4556
}
46-
prefix
57+
Some(prefix)
4758
}
4859
}
4960

@@ -57,7 +68,7 @@ pub(super) fn parse_reference_chain(expr: &ExprRef) -> Result<ReferenceChain> {
5768
match current_expr.as_ref() {
5869
Expr::Var { span, .. } => {
5970
// Found the root variable
60-
let root = span.text().to_string();
71+
let root = ReferenceRoot::Variable(span.text().to_string());
6172
components.reverse(); // We built backwards, so reverse
6273
return Ok(ReferenceChain { root, components });
6374
}
@@ -81,7 +92,12 @@ pub(super) fn parse_reference_chain(expr: &ExprRef) -> Result<ReferenceChain> {
8192
current_expr = refr;
8293
}
8394
_ => {
84-
return Err(CompilerError::NotSimpleReferenceChain.at(current_expr.span()));
95+
// Fallback root expression (e.g., array literal, function call)
96+
components.reverse();
97+
return Ok(ReferenceChain {
98+
root: ReferenceRoot::Expression(current_expr.clone()),
99+
components,
100+
});
85101
}
86102
}
87103
}
@@ -94,10 +110,17 @@ impl<'a> Compiler<'a> {
94110
// Parse the expression into a reference chain
95111
let chain = parse_reference_chain(expr)?;
96112

97-
match chain.root.as_str() {
98-
"input" => self.compile_input_chain(&chain, span),
99-
"data" => self.compile_data_chain(&chain, span),
100-
_ => self.compile_local_var_chain(&chain, span),
113+
match chain.root.clone() {
114+
ReferenceRoot::Variable(name) => match name.as_str() {
115+
"input" => self.compile_input_chain(&chain, span),
116+
"data" => self.compile_data_chain(&chain, span),
117+
_ => self.compile_local_var_chain(&name, &chain, span),
118+
},
119+
ReferenceRoot::Expression(root_expr) => {
120+
let root_reg =
121+
self.compile_rego_expr_with_span(&root_expr, root_expr.span(), false)?;
122+
self.compile_chain_access(root_reg, &chain.components, span)
123+
}
101124
}
102125
}
103126

@@ -121,7 +144,9 @@ impl<'a> Compiler<'a> {
121144
}
122145

123146
// Build the static prefix path components for rule matching
124-
let static_prefix = chain.get_static_prefix();
147+
let static_prefix = chain
148+
.get_static_prefix()
149+
.expect("data references must have variable roots");
125150

126151
// Try to find the longest matching rule prefix
127152
// Start from the full path and work backwards
@@ -252,17 +277,22 @@ impl<'a> Compiler<'a> {
252277
}
253278

254279
/// Compile local variable access chain
255-
fn compile_local_var_chain(&mut self, chain: &ReferenceChain, span: &Span) -> Result<Register> {
280+
fn compile_local_var_chain(
281+
&mut self,
282+
root: &str,
283+
chain: &ReferenceChain,
284+
span: &Span,
285+
) -> Result<Register> {
256286
// Check if it's a local variable first (precedence over rules)
257-
if let Some(var_reg) = self.lookup_variable(&chain.root) {
287+
if let Some(var_reg) = self.lookup_variable(root) {
258288
if chain.components.is_empty() {
259289
return Ok(var_reg);
260290
}
261291
return self.compile_chain_access(var_reg, &chain.components, span);
262292
}
263293

264294
// Check if there's a rule in the current package that matches
265-
let current_pkg_prefix = format!("{}.{}", &self.current_package, &chain.root);
295+
let current_pkg_prefix = format!("{}.{}", &self.current_package, root);
266296

267297
// Build static path for rule matching
268298
let mut rule_path_parts = vec![current_pkg_prefix.as_str()];
@@ -300,7 +330,7 @@ impl<'a> Compiler<'a> {
300330

301331
// No rule found - undefined variable
302332
Err(CompilerError::UndefinedVariable {
303-
name: chain.root.clone(),
333+
name: root.to_string(),
304334
}
305335
.at(span))
306336
}

src/rvm/vm/comprehension.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,83 @@ impl RegoVM {
457457
}
458458
}
459459

460+
pub(super) fn handle_comprehension_condition_failure_run_to_completion(
461+
&mut self,
462+
) -> Result<bool> {
463+
if let Some(mut context) = self.comprehension_stack.pop() {
464+
self.advance_comprehension_after_failure(&mut context)?;
465+
self.comprehension_stack.push(context);
466+
Ok(true)
467+
} else {
468+
Ok(false)
469+
}
470+
}
471+
472+
pub(super) fn handle_comprehension_condition_failure_suspendable(&mut self) -> Result<bool> {
473+
if let Some(mut frame) = self.execution_stack.pop() {
474+
let handled = if let FrameKind::Comprehension { context, .. } = &mut frame.kind {
475+
self.advance_comprehension_after_failure(context)?;
476+
true
477+
} else {
478+
false
479+
};
480+
481+
self.execution_stack.push(frame);
482+
if handled {
483+
return Ok(true);
484+
}
485+
}
486+
Ok(false)
487+
}
488+
489+
fn advance_comprehension_after_failure(
490+
&mut self,
491+
context: &mut ComprehensionContext,
492+
) -> Result<()> {
493+
if let Some(iter_state) = context.iteration_state.as_mut() {
494+
self.capture_comprehension_iteration_position(
495+
iter_state,
496+
context.key_reg,
497+
context.value_reg,
498+
);
499+
iter_state.advance();
500+
let has_next =
501+
self.setup_next_iteration(iter_state, context.key_reg, context.value_reg)?;
502+
if has_next {
503+
self.pc = context.body_start.saturating_sub(1) as usize;
504+
} else {
505+
context.iteration_state = None;
506+
self.pc = context.comprehension_end.saturating_sub(1) as usize;
507+
}
508+
} else {
509+
self.pc = context.comprehension_end.saturating_sub(1) as usize;
510+
}
511+
512+
Ok(())
513+
}
514+
515+
fn capture_comprehension_iteration_position(
516+
&mut self,
517+
iter_state: &mut IterationState,
518+
key_reg: u8,
519+
value_reg: u8,
520+
) {
521+
match iter_state {
522+
IterationState::Object { current_key, .. } => {
523+
let tracked_key = if key_reg != value_reg {
524+
self.registers[key_reg as usize].clone()
525+
} else {
526+
self.registers[value_reg as usize].clone()
527+
};
528+
*current_key = Some(tracked_key);
529+
}
530+
IterationState::Set { current_item, .. } => {
531+
*current_item = Some(self.registers[value_reg as usize].clone());
532+
}
533+
IterationState::Array { .. } => {}
534+
}
535+
}
536+
460537
fn execute_comprehension_end_run_to_completion(&mut self) -> Result<()> {
461538
if let Some(_context) = self.comprehension_stack.pop() {
462539
Ok(())

src/rvm/vm/functions.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
3+
use crate::builtins;
34
use crate::value::Value;
45
use alloc::string::String;
56
use alloc::vec::Vec;
@@ -41,6 +42,11 @@ impl RegoVM {
4142
});
4243
}
4344

45+
if args.iter().any(|a| a == &Value::Undefined) {
46+
self.registers[params.dest as usize] = Value::Undefined;
47+
return Ok(());
48+
}
49+
4450
if let Some(builtin_fcn) = self.program.get_resolved_builtin(params.builtin_index) {
4551
let dummy_source = crate::lexer::Source::from_contents("arg".into(), String::new())?;
4652
let dummy_span = crate::lexer::Span {
@@ -61,8 +67,31 @@ impl RegoVM {
6167
dummy_exprs.push(crate::ast::Ref::new(dummy_expr));
6268
}
6369

64-
let result = (builtin_fcn.0)(&dummy_span, &dummy_exprs, &args, true)?;
65-
self.registers[params.dest as usize] = result.clone();
70+
let cache_name = builtins::must_cache(builtin_info.name.as_str());
71+
if let Some(name) = cache_name {
72+
if let Some(value) = self.builtins_cache.get(&(name, args.clone())) {
73+
self.registers[params.dest as usize] = value.clone();
74+
return Ok(());
75+
}
76+
}
77+
78+
let result =
79+
match (builtin_fcn.0)(&dummy_span, &dummy_exprs, &args, self.strict_builtin_errors)
80+
{
81+
Ok(value) => value,
82+
Err(_) if !self.strict_builtin_errors => Value::Undefined,
83+
Err(err) => return Err(err.into()),
84+
};
85+
86+
if result == Value::Undefined {
87+
self.registers[params.dest as usize] = Value::Undefined;
88+
} else {
89+
self.registers[params.dest as usize] = result.clone();
90+
}
91+
92+
if let Some(name) = cache_name {
93+
self.builtins_cache.insert((name, args), result);
94+
}
6695
} else {
6796
return Err(VmError::BuiltinNotResolved {
6897
name: builtin_info.name.clone(),

src/rvm/vm/loops.rs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,8 @@ impl RegoVM {
621621
self.pc = loop_next_pc as usize - 1;
622622
}
623623
}
624+
} else if self.handle_comprehension_condition_failure_run_to_completion()? {
625+
// handled by comprehension context
624626
} else {
625627
return Err(VmError::AssertionFailed);
626628
}
@@ -633,29 +635,31 @@ impl RegoVM {
633635
return Ok(());
634636
}
635637

636-
let (resume_pc, loop_ctx) = match self.execution_stack.last_mut() {
637-
Some(ExecutionFrame {
638-
kind: FrameKind::Loop { return_pc, context },
639-
..
640-
}) => (*return_pc, context),
641-
_ => return Err(VmError::AssertionFailed),
642-
};
643-
644-
match loop_ctx.mode {
645-
LoopMode::Any | LoopMode::ForEach => {
646-
loop_ctx.current_iteration_failed = true;
647-
self.pc = loop_ctx.loop_next_pc as usize - 1;
648-
}
649-
LoopMode::Every => {
650-
self.registers[loop_ctx.result_reg as usize] = Value::Bool(false);
651-
let completed_frame = self.execution_stack.pop().expect("loop frame exists");
652-
if let Some(parent) = self.execution_stack.last_mut() {
653-
parent.pc = resume_pc;
638+
if let Some(ExecutionFrame {
639+
kind: FrameKind::Loop { return_pc, context },
640+
..
641+
}) = self.execution_stack.last_mut()
642+
{
643+
let resume_pc = *return_pc;
644+
match context.mode {
645+
LoopMode::Any | LoopMode::ForEach => {
646+
context.current_iteration_failed = true;
647+
self.pc = context.loop_next_pc as usize - 1;
648+
}
649+
LoopMode::Every => {
650+
self.registers[context.result_reg as usize] = Value::Bool(false);
651+
let completed_frame = self.execution_stack.pop().expect("loop frame exists");
652+
if let Some(parent) = self.execution_stack.last_mut() {
653+
parent.pc = resume_pc;
654+
}
655+
drop(completed_frame);
654656
}
655-
drop(completed_frame);
656657
}
658+
Ok(())
659+
} else if self.handle_comprehension_condition_failure_suspendable()? {
660+
Ok(())
661+
} else {
662+
Err(VmError::AssertionFailed)
657663
}
658-
659-
Ok(())
660664
}
661665
}

src/rvm/vm/machine.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ pub struct RegoVM {
9898

9999
/// Whether builtins should raise errors strictly or return undefined on failure
100100
pub(super) strict_builtin_errors: bool,
101+
102+
/// Cache for builtin calls that must stay deterministic across a single evaluation
103+
pub(super) builtins_cache: BTreeMap<(&'static str, Vec<Value>), Value>,
101104
}
102105

103106
impl Default for RegoVM {
@@ -135,6 +138,7 @@ impl RegoVM {
135138
execution_mode: ExecutionMode::RunToCompletion,
136139
frame_pc_overridden: false,
137140
strict_builtin_errors: false,
141+
builtins_cache: BTreeMap::new(),
138142
}
139143
}
140144

src/rvm/vm/state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ impl RegoVM {
3232
self.registers.clear();
3333
self.registers
3434
.resize(self.base_register_count, Value::Undefined);
35+
36+
// Builtin cache entries only live for a single execution
37+
self.builtins_cache.clear();
3538
}
3639

3740
/// Return all active objects to their respective pools for reuse

tests/opa.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,18 @@ const OPA_BRANCH: &str = "v1.2.0";
2222
const OPA_TODO_FOLDERS: &[&str] = &[
2323
"aggregates",
2424
"baseandvirtualdocs",
25-
"comparisonexpr",
2625
"dataderef",
2726
"defaultkeyword",
28-
"disjunction",
2927
"elsekeyword",
30-
"eqexpr",
3128
"every",
32-
"example",
3329
"fix1863",
3430
"functions",
35-
"jsonschema",
3631
"partialdocconstants",
3732
"partialobjectdoc",
3833
"planner-ir",
39-
"rand",
4034
"refheads",
41-
"replacen",
42-
"semverisvalid",
4335
"sets",
44-
"time",
4536
"type",
46-
"varreferences",
4737
"virtualdocs",
4838
"walkbuiltin",
4939
"withkeyword",

0 commit comments

Comments
 (0)