fix(rvm): support nested partial object rule yields#665
fix(rvm): support nested partial object rule yields#665anakrish wants to merge 3 commits intomicrosoft:mainfrom
Conversation
There was a problem hiding this comment.
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
ObjectDeepSetVM 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
ObjectDeepSetwhen 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.
There was a problem hiding this comment.
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_typetreatsRuleHead::ComprasPartialObjectonly whenrefrisExpr::RefBrack. With this PR intending to support dotted rule heads as partial-object writes,Expr::RefDotheads with:=will still be classified asComplete, which can route compilation/VM execution down the wrong yield path. Consider using the samerefr_has_bracket_or_dot(refr)logic here whenassign.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.
| 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<_, _>>()?; |
There was a problem hiding this comment.
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.
src/rvm/vm/dispatch.rs
Outdated
| obj_register: u8, | ||
| ) -> Result<()> { | ||
| let Some((first_key, remaining_keys)) = key_values.split_first() else { | ||
| return Ok(()); |
There was a problem hiding this comment.
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.
| return Ok(()); | |
| unreachable!("invalid bytecode: ObjectDeepSet requires a non-empty key path"); |
src/rvm/vm/dispatch.rs
Outdated
| let offending = current.clone(); | ||
| let object = current | ||
| .as_object_mut() | ||
| .map_err(|_| VmError::RegisterNotObject { | ||
| register: obj_register, | ||
| value: offending, |
There was a problem hiding this comment.
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.
| 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(), |
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
0f2af40 to
b202aa8
Compare
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.