From 117671d959b99f281ae0dd2e60de00b145aef325 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 19:34:41 +0000 Subject: [PATCH] fix: preserve all bindings for partial object iteration Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/34a4e1b3-d364-46c4-9998-b00780f5d339 Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com> --- src/interpreter.rs | 5 +- src/languages/rego/compiler/rules.rs | 7 +- src/rvm/vm/dispatch.rs | 14 +- src/rvm/vm/errors.rs | 3 + src/rvm/vm/rules.rs | 1 + .../cases/rule/partial_object_iteration.yaml | 201 ++++++++++++++++++ .../rvm/rego/cases/partial_object_rules.yaml | 200 +++++++++++++++++ 7 files changed, 424 insertions(+), 7 deletions(-) create mode 100644 tests/interpreter/cases/rule/partial_object_iteration.yaml create mode 100644 tests/rvm/rego/cases/partial_object_rules.yaml diff --git a/src/interpreter.rs b/src/interpreter.rs index e44490a..fe969f9 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1797,7 +1797,10 @@ impl Interpreter { comps.pop(); output } else { - // Rule's constness is determined only by its ref. + // Implicit-true partial object rules can vary with each successful key binding. + if key_expr.is_some() && !is_old_style_set { + is_const_rule = false; + } Value::Bool(true) }; diff --git a/src/languages/rego/compiler/rules.rs b/src/languages/rego/compiler/rules.rs index 990bef3..ad0efcc 100644 --- a/src/languages/rego/compiler/rules.rs +++ b/src/languages/rego/compiler/rules.rs @@ -55,11 +55,8 @@ impl<'a> Compiler<'a> { if let Rule::Spec { head, .. } = def.as_ref() { match head { RuleHead::Set { .. } => RuleType::PartialSet, - RuleHead::Compr { refr, assign, .. } => match refr.as_ref() { - crate::ast::Expr::RefBrack { .. } if assign.is_some() => { - RuleType::PartialObject - } - crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet, + RuleHead::Compr { refr, .. } => match refr.as_ref() { + crate::ast::Expr::RefBrack { .. } => RuleType::PartialObject, _ => RuleType::Complete, }, _ => RuleType::Complete, diff --git a/src/rvm/vm/dispatch.rs b/src/rvm/vm/dispatch.rs index d0cb044..2f2dbef 100644 --- a/src/rvm/vm/dispatch.rs +++ b/src/rvm/vm/dispatch.rs @@ -454,7 +454,19 @@ impl RegoVM { let mut obj_value = self.take_register(obj)?; if let Ok(obj_mut) = obj_value.as_object_mut() { - obj_mut.insert(key_value, value_value); + match obj_mut.get(&key_value) { + Some(existing_value) if existing_value != &value_value => { + self.set_register(obj, obj_value)?; + return Err(VmError::RuleMultipleOutputs { pc: self.pc }); + } + Some(_) => { + self.set_register(obj, obj_value)?; + return Ok(InstructionOutcome::Continue); + } + None => { + obj_mut.insert(key_value, value_value); + } + } self.set_register(obj, obj_value)?; } else { let offending = obj_value.clone(); diff --git a/src/rvm/vm/errors.rs b/src/rvm/vm/errors.rs index 4688c02..9ac8d87 100644 --- a/src/rvm/vm/errors.rs +++ b/src/rvm/vm/errors.rs @@ -209,6 +209,9 @@ pub enum VmError { #[error("Rule-data conflict: {message} (pc={pc})")] RuleDataConflict { message: String, pc: usize }, + #[error("rules must not produce multiple outputs (pc={pc})")] + RuleMultipleOutputs { pc: usize }, + #[error("Arithmetic error: {message} (pc={pc})")] ArithmeticError { message: String, pc: usize }, diff --git a/src/rvm/vm/rules.rs b/src/rvm/vm/rules.rs index 4ecf535..a30af85 100644 --- a/src/rvm/vm/rules.rs +++ b/src/rvm/vm/rules.rs @@ -26,6 +26,7 @@ impl RegoVM { | VmError::MemoryLimitExceeded { .. } | VmError::RegexSizeLimitExceeded { .. } | VmError::InstructionLimitExceeded { .. } + | VmError::RuleMultipleOutputs { .. } ) } diff --git a/tests/interpreter/cases/rule/partial_object_iteration.yaml b/tests/interpreter/cases/rule/partial_object_iteration.yaml new file mode 100644 index 0000000..438baca --- /dev/null +++ b/tests/interpreter/cases/rule/partial_object_iteration.yaml @@ -0,0 +1,201 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: partial_object_iteration_some_in_object_v1 + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.x + } + query: data.test + want_result: + violations: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_iteration_some_in_array_v1 + data: {} + input: + arr: ["FOO", "BAR", "BAZ"] + modules: + - | + package test + import rego.v1 + + violations[v] if { + some _, v in input.arr + } + query: data.test + want_result: + violations: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_iteration_with_filter_v1 + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.x + k != "BAR" + } + query: data.test + want_result: + violations: + BAZ: true + FOO: true + + - note: partial_object_iteration_input_lookup_future_keywords + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import future.keywords.if + + violations[k] if { + input.x[k] + } + query: data.test + want_result: + violations: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_multiple_bodies_collect_all_keys_v1 + data: {} + input: + primary: + FOO: 1 + BAR: 2 + secondary: + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.primary + } + + violations[k] if { + some k, _ in input.secondary + } + query: data.test + want_result: + violations: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_duplicate_keys_same_value_are_ok_v1 + data: {} + input: + arr: ["FOO", "FOO", "BAR"] + modules: + - | + package test + import rego.v1 + + violations[v] if { + some _, v in input.arr + } + query: data.test + want_result: + violations: + BAR: true + FOO: true + + - note: partial_object_duplicate_keys_different_values_error_v1 + data: {} + input: + entries: + - k: "FOO" + v: 1 + - k: "FOO" + v: 2 + modules: + - | + package test + import rego.v1 + + violations[k] := v if { + some entry in input.entries + k := entry.k + v := entry.v + } + query: data.test.violations + error: "rules must not produce multiple outputs" + + - note: partial_object_and_partial_set_iteration_coexist_v1 + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.x + } + + seen contains k if { + some k, _ in input.x + } + query: data.test + want_result: + seen: + set!: ["BAR", "BAZ", "FOO"] + violations: + BAR: true + BAZ: true + FOO: true + + - note: complete_rule_same_value_definitions_still_work_v1 + data: {} + input: + role: "superuser" + modules: + - | + package test + import rego.v1 + + allowed if { + input.role == "admin" + } + + allowed if { + input.role == "superuser" + } + query: data.test.allowed + want_result: true diff --git a/tests/rvm/rego/cases/partial_object_rules.yaml b/tests/rvm/rego/cases/partial_object_rules.yaml new file mode 100644 index 0000000..eeacb99 --- /dev/null +++ b/tests/rvm/rego/cases/partial_object_rules.yaml @@ -0,0 +1,200 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: partial_object_iteration_some_in_object_v1 + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.x + } + query: data.test.violations + want_result: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_iteration_some_in_array_v1 + data: {} + input: + arr: ["FOO", "BAR", "BAZ"] + modules: + - | + package test + import rego.v1 + + violations[v] if { + some _, v in input.arr + } + query: data.test.violations + want_result: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_iteration_with_filter_v1 + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.x + k != "BAR" + } + query: data.test.violations + want_result: + BAZ: true + FOO: true + + - note: partial_object_iteration_input_lookup_future_keywords + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import future.keywords.if + + violations[k] if { + input.x[k] + } + query: data.test.violations + want_result: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_multiple_bodies_collect_all_keys_v1 + data: {} + input: + primary: + FOO: 1 + BAR: 2 + secondary: + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.primary + } + + violations[k] if { + some k, _ in input.secondary + } + query: data.test.violations + want_result: + BAR: true + BAZ: true + FOO: true + + - note: partial_object_duplicate_keys_same_value_are_ok_v1 + data: {} + input: + arr: ["FOO", "FOO", "BAR"] + modules: + - | + package test + import rego.v1 + + violations[v] if { + some _, v in input.arr + } + query: data.test.violations + want_result: + BAR: true + FOO: true + + - note: partial_object_duplicate_keys_different_values_error_v1 + data: {} + input: + entries: + - k: "FOO" + v: 1 + - k: "FOO" + v: 2 + modules: + - | + package test + import rego.v1 + + violations[k] := v if { + some entry in input.entries + k := entry.k + v := entry.v + } + query: data.test.violations + want_error: "multiple outputs" + + - note: partial_object_and_partial_set_iteration_coexist_v1 + data: {} + input: + x: + FOO: 1 + BAR: 2 + BAZ: 3 + modules: + - | + package test + import rego.v1 + + violations[k] if { + some k, _ in input.x + } + + seen contains k if { + some k, _ in input.x + } + + main := { + "seen": seen, + "violations": violations, + } + query: data.test.main + want_result: + seen: + set!: ["BAR", "BAZ", "FOO"] + violations: + BAR: true + BAZ: true + FOO: true + + - note: complete_rule_same_value_definitions_still_work_v1 + data: {} + input: + role: "superuser" + modules: + - | + package test + import rego.v1 + + allowed if { + input.role == "admin" + } + + allowed if { + input.role == "superuser" + } + query: data.test.allowed + want_result: true