Skip to content

fix(rvm): support nested partial object rule yields#665

Draft
anakrish wants to merge 3 commits intomicrosoft:mainfrom
anakrish:fix-nested-partial-object-yields
Draft

fix(rvm): support nested partial object rule yields#665
anakrish wants to merge 3 commits intomicrosoft:mainfrom
anakrish:fix-nested-partial-object-yields

Conversation

@anakrish
Copy link
Copy Markdown
Collaborator

@anakrish anakrish commented Apr 7, 2026

Partial object rules like foo[a] contains v were being compiled and executed like flat sets in the RVM path, which dropped the object key and caused real policies to return empty or incorrect results.

This changes the compiler and VM to treat bracketed and dotted rule heads as partial-object writes, adds an ObjectDeepSet instruction for nested object updates and multi-value leaves, and wires that through the listing, parser, and static provenance handling.

It also adds regression coverage for multi-value partial objects so cases like foo[a] contains v keep working in the RVM engine.

@anakrish anakrish requested a review from Copilot April 7, 2026 18:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes RVM compilation/execution of nested partial-object rule yields (including foo[a] contains v) so object keys are preserved and multi-value leaves are accumulated correctly instead of being treated as flat partial sets.

Changes:

  • Add ObjectDeepSet VM instruction + parameter table to support nested object updates and multi-value (set) leaves.
  • Update Rego compiler to classify bracketed/dotted set heads as partial-object rules and emit ObjectDeepSet when needed.
  • Add regression YAML cases for multi-value partial objects in the RVM test suite.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/rvm/rego/cases/partial_object_multivalue.yaml Adds regression cases asserting correct object-of-sets output for foo[key] contains value.
src/rvm/vm/dispatch.rs Implements ObjectDeepSet execution and a recursive helper for nested writes.
src/rvm/tests/instruction_parser.rs Adds parsing support for the new ObjectDeepSet instruction.
src/rvm/program/listing.rs Adds readable listing and opcode name for ObjectDeepSet.
src/rvm/instructions/params.rs Introduces ObjectDeepSetParams and storage in InstructionData.
src/rvm/instructions/mod.rs Exposes ObjectDeepSetParams and adds Instruction::ObjectDeepSet.
src/rvm/instructions/display.rs Adds Display formatting for ObjectDeepSet.
src/languages/rego/compiler/rules.rs Detects partial-object set heads and extracts nested ref keys for deep writes.
src/languages/rego/compiler/queries.rs Emits ObjectDeepSet for multi-key and/or multi-value partial-object yields.
src/languages/rego/compiler/mod.rs Extends CompilationContext with extra_key_exprs and multi_value.
src/languages/rego/compiler/loops.rs Initializes new context fields for loop compilation contexts.
src/languages/rego/compiler/comprehensions.rs Initializes new context fields for comprehension compilation contexts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/languages/rego/compiler/rules.rs:110

  • compute_rule_type treats RuleHead::Compr as PartialObject only when refr is Expr::RefBrack. With this PR intending to support dotted rule heads as partial-object writes, Expr::RefDot heads with := will still be classified as Complete, which can route compilation/VM execution down the wrong yield path. Consider using the same refr_has_bracket_or_dot(refr) logic here when assign.is_some().
                        RuleHead::Compr { refr, assign, .. } => match refr.as_ref() {
                            crate::ast::Expr::RefBrack { .. } if assign.is_some() => {
                                RuleType::PartialObject
                            }
                            crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet,
                            _ => RuleType::Complete,
                        },

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +471 to +486
let params = program
.instruction_data
.get_object_deep_set_params(params_index)
.ok_or(VmError::InvalidObjectCreateParams {
index: params_index,
pc: self.pc,
available: program.instruction_data.object_deep_set_params.len(),
})?
.clone();

// Read all key values and the leaf value upfront
let key_values: alloc::vec::Vec<Value> = params
.keys
.iter()
.map(|&k| self.get_register(k).cloned())
.collect::<core::result::Result<_, _>>()?;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectDeepSetParams is cloned on every ObjectDeepSet execution (.clone() after fetching from the param table). This is in the VM dispatch hot path and can add unnecessary allocation/copy (notably cloning the keys: Vec<u8>). Prefer borrowing the params (&ObjectDeepSetParams) and only cloning the register values you actually need.

Copilot uses AI. Check for mistakes.
obj_register: u8,
) -> Result<()> {
let Some((first_key, remaining_keys)) = key_values.split_first() else {
return Ok(());
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object_deep_set silently succeeds when key_values is empty (split_first() else-branch returns Ok(())). An ObjectDeepSet instruction with an empty key-path is invalid bytecode and should fail fast (e.g., return a dedicated VM error) to avoid hiding compiler/IR bugs.

Suggested change
return Ok(());
unreachable!("invalid bytecode: ObjectDeepSet requires a non-empty key path");

Copilot uses AI. Check for mistakes.
Comment on lines +930 to +935
let offending = current.clone();
let object = current
.as_object_mut()
.map_err(|_| VmError::RegisterNotObject {
register: obj_register,
value: offending,
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object_deep_set clones current into offending unconditionally before checking as_object_mut(). This can be expensive for large objects/arrays and is on the execution path for every deep-set. Only clone the value when constructing the error (i.e., on the Err branch) to avoid unnecessary copying.

Suggested change
let offending = current.clone();
let object = current
.as_object_mut()
.map_err(|_| VmError::RegisterNotObject {
register: obj_register,
value: offending,
let object = current
.as_object_mut()
.map_err(|_| VmError::RegisterNotObject {
register: obj_register,
value: current.clone(),

Copilot uses AI. Check for mistakes.
anakrish added 3 commits April 7, 2026 20:56
Partial object rules like foo[a] contains v were being compiled and executed like flat sets in the RVM path, which dropped the object key and caused real policies to return empty or incorrect results.

This changes the compiler and VM to treat bracketed and dotted rule heads as partial-object writes, adds an ObjectDeepSet instruction for nested object updates and multi-value leaves, and wires that through the listing, parser, and static provenance handling.

It also adds regression coverage for multi-value partial objects so cases like foo[a] contains v keep working in the RVM engine.
Add 16 new test cases covering corner cases for the foo[key] contains value
pattern:

- Non-scalar leaf values (objects, arrays, booleans)
- Numeric keys
- Empty iteration producing no results
- Consuming multivalue results in downstream rules (direct access, count)
- Single key with single value
- Multiple rule definitions producing different keys
- Multiple rule definitions accumulating into the same key
- Body condition filtering
- Input-derived data
- Computed string keys from iteration
- Large number of values per key
- Multivalue rule used alongside complete rules
- Data document lookup into multivalue results
- Use InvalidObjectDeepSetParams error variant instead of reusing
  InvalidObjectCreateParams for ObjectDeepSet instruction failures
- Remove unnecessary .clone() on ObjectDeepSetParams in dispatch hot path;
  borrow params and copy only the needed scalar fields
- Fail fast with VmError::Internal on empty key path in object_deep_set
  instead of silently returning Ok(())
- Fix RefDot fields compiled as Expr::Var (variable reference) to use
  Expr::String (literal key) so foo[a].b uses string "b" not variable b
- Fix duplicate-key test case to use array-of-pairs iteration so it
  actually validates multi-value accumulation for the same category
- Add 5 multi-bracket rule head tests (two-key assign, two-key contains,
  three-key nesting, merge across definitions, contains accumulation)
  exercised via wrapper rules to work around entry point limitations
@anakrish anakrish force-pushed the fix-nested-partial-object-yields branch from 0f2af40 to b202aa8 Compare April 8, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants