Skip to content

Fix partial object iteration dropping bindings and guard unsupported RVM patterns#718

Merged
anakrish merged 1 commit into
mainfrom
copilot/fix-partial-set-rule-iteration
May 18, 2026
Merged

Fix partial object iteration dropping bindings and guard unsupported RVM patterns#718
anakrish merged 1 commit into
mainfrom
copilot/fix-partial-set-rule-iteration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 7, 2026

Partial object rules with dynamic keys (violations[k] if { ... }) only produced a single entry instead of collecting all bindings. Two independent bugs:

  1. Interpreter: the early-return optimization in eval_output_expr_in_loop never checked whether the key expression was constant. A variable key like k was treated as constant output, causing the loop to exit after the first match.

  2. RVM: compute_rule_type misclassified p[k] if { ... } as PartialSet instead of PartialObject. Added compiler error guards for patterns the codegen cannot yet handle (constant keys, nested brackets), so they fall back to the interpreter cleanly.

Example

package test
import rego.v1

violations[k] if {
  some k, _ in input.items
}

With {"items": {"FOO": 1, "BAR": 2, "BAZ": 3}}, now correctly evaluates to {"BAR": true, "BAZ": true, "FOO": true} instead of just {"FOO": true}.

Tests

Closes #712

Copilot AI changed the title [WIP] Fix partial set rule iteration to emit all bindings Fix partial object iteration dropping bindings in interpreter and RVM May 7, 2026
Copilot AI requested a review from anakrish May 7, 2026 11:46
@anakrish anakrish requested a review from Copilot May 7, 2026 12:07
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

Fixes a correctness bug where partial object rules with dynamic keys would stop after the first matching binding, causing missing keys in the produced object. The fix aligns interpreter constantness detection and RVM rule typing so both execution paths emit all bindings for dynamic-key partial objects.

Changes:

  • Interpreter: tighten “constant rule” detection so early-return is only enabled when the object key expression is a simple literal.
  • RVM compiler: classify bracket-head comprehension rules (p[k] if ...) as PartialObject instead of PartialSet.
  • Add regression YAML cases covering dynamic-key partial objects, explicit-value partial objects, multi-body iteration, array iteration, and contains partial sets (plus interpreter-only v1 coverage for constant-key and multilevel shapes).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/rvm/rego/cases/partial_object_rules.yaml Adds RVM+interpreter regression cases for partial object iteration (and contains partial set) behavior.
tests/interpreter/cases/rule/partial_object_v1.yaml Adds interpreter-focused Rego v1 cases for constant-key and multilevel partial object shapes.
src/languages/rego/compiler/rules.rs Adjusts rule-type classification so p[k] if { ... } compiles as a partial object.
src/interpreter.rs Prevents incorrect early-return for dynamic-key partial object rules by tightening constness detection.

BAZ: 3
FOO: 1

- note: partial_object_constant_key_still_works
@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 7, 2026

RVM: Add compiler errors for unsupported partial object patterns (addresses #719)

The interpreter fix in this PR is correct and approved. However, the RVM classification change (PartialSetPartialObject at rules.rs:62) is too broad — the RVM codegen only handles variable-key partial objects correctly. Constant-key and multi-level patterns produce wrong results (see #719).

What to do

Add compiler error guards so unsupported patterns fall back to the interpreter. The RVM compiler already does this for other unsupported features (e.g., WithKeywordUnsupported, walk loops).

Step 1: Add error variants to src/languages/rego/compiler/error.rs

#[error("partial object rules with constant keys are not yet supported by the RVM compiler")]
PartialObjectConstantKeyUnsupported,

#[error("partial object rules with nested bracket keys are not yet supported by the RVM compiler")]
PartialObjectNestedKeyUnsupported,

Step 2: Add a validation helper in src/languages/rego/compiler/rules.rs

After compute_rule_type classifies a Compr { refr: RefBrack, assign: None } as PartialObject, validate that the pattern is one the codegen can handle. Add a helper that checks:

  1. The RefBrack has exactly one dynamic bracket at the leaf (no nesting like p[a][b])
  2. The bracket index is a variable (Expr::Var), not a literal/constant (not p["fixed"])

If either check fails, return the appropriate CompilerError. This should be checked in the rule compilation path, before codegen emits ObjectSet.

The simplest place is in compile_worklist_rules (around line 340-360 where key_expr is extracted from RefBrack). Before proceeding with compilation:

// For PartialObject without assign, validate the key is a variable
if rule_type == RuleType::PartialObject && assign.is_none() {
    // Check: index must be a Var (not a literal/constant)
    match index.as_ref() {
        Expr::Var { .. } => {} // OK — variable key like p[k]
        _ => return Err(CompilerError::PartialObjectConstantKeyUnsupported.at(&refr_span)),
    }
    // Check: prefix must not contain another RefBrack (no nesting like p[a][b])
    if matches!(prefix.as_ref(), Expr::RefBrack { .. }) {
        return Err(CompilerError::PartialObjectNestedKeyUnsupported.at(&refr_span));
    }
}

Step 3: Un-skip the RVM tests that now correctly error

The two skipped test cases in tests/rvm/rego/cases/partial_object_rules.yaml:

  • partial_object_constant_key_still_works — change from skip: true to want_error: true (or whatever the test harness uses to expect compilation failure)
  • partial_object_multilevel_key_collects_nested_bindings — same: expect a compiler error

Check tests/rvm/rego/mod.rs for how error expectations work in the test harness (look for want_error, expect_error, or similar fields in TestCase).

Step 4: Run tests

cargo test --features rvm
cargo test --test opa --features opa-testutil

Summary

This is a temporary mitigation for #719. The RVM codegen will be properly fixed in a future PR. For now, unsupported partial object patterns gracefully fall back to the interpreter via compiler errors.

@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 7, 2026

@copilot Take a look at #718 (comment)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 7, 2026

RVM: Add compiler errors for unsupported partial object patterns (addresses #719)

The interpreter fix in this PR is correct and approved. However, the RVM classification change (PartialSet → `...

Addressed in f2ff982. I added explicit RVM compiler errors for constant-key and nested-bracket partial object heads, validated them in the rule compilation path, and updated the RVM regressions to expect those compiler errors while allowing interpreter success.

@copilot Take a look at #718 (comment)

Done in f2ff982.

Copilot AI changed the title Fix partial object iteration dropping bindings in interpreter and RVM Fix partial object iteration dropping bindings and guard unsupported RVM patterns May 7, 2026
@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

Review Findings — Round 2

The interpreter fix is correct and approved. The RVM compiler error guards need adjustments:

Finding 1 (High): Validation guard is too narrow — assign.is_none() skips := forms

The guard at rules.rs:366-370 only validates p[k] if (no assign), but p[k] := v if forms have the same nested-key and constant-key codegen issues.

Example: p[a][b] := 1 if { some a, obj in input.nested; some b, _ in obj } bypasses the guard, and the compiler drops outer key a, producing {"read":1,"deploy":1} instead of {"app":{"read":1},"ops":{"deploy":1}}.

Fix: Remove the assign.is_none() condition. Validate all RuleType::PartialObject RuleHead::Compr heads:

// Change from:
if rule_type == RuleType::PartialObject && assign.is_none() {
    self.validate_partial_object_shape(refr)?;
}

// To:
if rule_type == RuleType::PartialObject {
    self.validate_partial_object_shape(refr)?;
}

Finding 2 (Medium): Dynamic non-Var keys incorrectly rejected

validate_partial_object_shape rejects ALL non-Var keys as "constant keys", but expressions like p[input.x] if and p[x+1] if are dynamic — the RVM codegen handles them fine via compile_rego_expr + ObjectSet.

Fix: Instead of !matches!(index.as_ref(), Expr::Var { .. }), reject only actual literals:

// Change from:
if !matches!(index.as_ref(), Expr::Var { .. }) {
    return Err(CompilerError::PartialObjectConstantKeyUnsupported.at(index.span()));
}

// To: reject only simple literals (strings, numbers, bools, null)
if matches!(index.as_ref(), 
    Expr::String { .. } | Expr::RawString { .. } | Expr::Number { .. } 
    | Expr::Bool { .. } | Expr::Null { .. }) {
    return Err(CompilerError::PartialObjectConstantKeyUnsupported.at(index.span()));
}

Finding 3 (Note): Semver consideration

Adding new public CompilerError enum variants is technically a semver break for downstream code that exhaustively matches on the enum. Consider adding #[non_exhaustive] to CompilerError (can be a separate PR/issue).

Test additions needed

  1. Add a test for p[a][b] := v if with want_error + allow_interpreter_success
  2. Add a test for p["fixed"] := v if with want_error + allow_interpreter_success
  3. The existing partial_object_explicit_value_collects_all_bindings test (p[k] := v if) should continue to pass (variable key with assign is supported)

@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

@copilot Please address the findings in the previous comment.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 8, 2026

Review Findings — Round 2

The interpreter fix is correct and approved. The RVM compiler error guards need adjustments:...

Addressed in bf43dd4. The partial-object guard now applies to all RuleType::PartialObject bracket heads, only rejects literal constant keys (not dynamic expressions like input.aliases[k]), and adds the requested explicit-value regression cases for constant-key and nested-key forms.

@copilot Please address the findings in the previous comment.

Done in bf43dd4.

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 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +4 to +16
cases:
- note: constant_key_partial_object_v1
data: {}
input:
enabled: true
modules:
- |
package test
import rego.v1

p["fixed"] if {
input.enabled
}
@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

Review Findings — Round 3

The core fix looks solid and all tests pass. Four remaining edge cases to consider:

Finding 1 (High): Hidden dynamic bracket in prefix chain

validate_partial_object_shape only checks the immediate prefix for RefBrack:

if matches!(prefix.as_ref(), Expr::RefBrack { .. }) {
    return Err(CompilerError::PartialObjectNestedKeyUnsupported.at(refr.span()));
}

A rule like p[a].q[b] if { ... } has prefix RefDot(RefBrack(Var(p), Var(a)), q), so the immediate prefix is RefDot, bypassing the check. The dynamic [a] is hidden deeper in the chain.

Fix: Walk the full prefix chain to reject any dynamic RefBrack:

fn has_dynamic_bracket(expr: &ExprRef) -> bool {
    match expr.as_ref() {
        Expr::RefBrack { refr, index, .. } => {
            !matches!(index.as_ref(),
                Expr::String { .. } | Expr::RawString { .. } | Expr::Number { .. }
                | Expr::Bool { .. } | Expr::Null { .. }
            ) || has_dynamic_bracket(refr)
        }
        Expr::RefDot { refr, .. } => has_dynamic_bracket(refr),
        _ => false,
    }
}

Then replace the simple matches!(prefix, RefBrack) with has_dynamic_bracket(prefix).

Note: this pattern is exotic and currently errors (safe fallback to interpreter), not a silent miscompile.

Finding 2 (Medium): Static string-bracket prefix over-rejected

p["a"][k] if { ... } has prefix RefBrack(Var(p), String("a")). The current check rejects ALL RefBrack prefixes, but static string-bracket refs like ["a"] are equivalent to .a and should be allowed.

Fix: The has_dynamic_bracket helper above would handle this correctly — it only rejects brackets with non-literal indices.

Finding 3 (Low): Missing interpreter-only tests for := forms

The interpreter test file (partial_object_v1.yaml) only covers implicit-true forms. Add explicit-value variants:

  • p["fixed"] := 7 if { input.enabled }
  • p[a][b] := v if { some a, obj in input.nested; some b, v in obj }

The RVM tests with allow_interpreter_success: true do implicitly test these, but dedicated interpreter tests improve clarity.

Finding 4 (Low): New CompilerError variants are a semver consideration

Adding public enum variants to CompilerError is technically a breaking change for exhaustive downstream matches. Consider #[non_exhaustive] in a future release. Not blocking for this PR.

@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

@copilot Please address the findings in the previous comment.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 8, 2026

Review Findings — Round 3

The core fix looks solid and all tests pass. Four remaining edge cases to consider:...

Addressed in 43698c1. The prefix check now walks the full reference chain so hidden dynamic brackets like p[a].q[b] are rejected, static literal bracket prefixes like p["a"][k] remain supported, and I added the requested interpreter-only := coverage.

@copilot Please address the findings in the previous comment.

Done in 43698c1.

@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

@copilot Please address the findings in the previous comment.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 8, 2026

Fix: Tests should not encode buggy behavior as expected output

Several test cases currently hard-code known-incorrect RVM output as want_result. This blesses bugs — when someone fixes the RVM,...

Addressed in 4f6651a. The undefined-key/value and conflicting-duplicate-key cases now use skip: true with TODO(#719) notes while keeping the correct expected behavior, and the RVM validator now rejects nested literal-prefix bracket heads like p[1][k].

@copilot Please address the findings in the previous comment.

Done in 4f6651a.

@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

@copilot Please take a look at #718 (comment)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 8, 2026

@copilot Please take a look at #718 (comment)

Took another look — this was already addressed in 4f6651a, and there aren’t any additional changes needed beyond that update.

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 5 out of 5 changed files in this pull request and generated no new comments.

@anakrish anakrish marked this pull request as ready for review May 8, 2026 18:37
@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

Missing: Regression tests for the exact issue #712 reproducer

The PR fixes the bug but doesn't have a test for the exact scenario reported in the issue. Please add these to lock down v0/v1 semantics for the original reproducer:

  # --- Issue #712 exact reproducer: violations[k] in v0 vs v1 ---

  # v0: violations[k] { ... } is a partial SET (no "if" keyword)
  - note: issue_712_reproducer_v0_partial_set
    data: {}
    input:
      servers:
        FOO: 1
        BAR: 2
        BAZ: 3
    modules:
      - |
        package test

        violations[k] {
          some k, _ in input.servers
        }
    query: data.test.violations
    want_result:
      set!: ["BAR", "BAZ", "FOO"]

  # v1: violations[k] if { ... } is a partial OBJECT (key -> true)
  # This is the exact bug from issue #712 — was producing only one entry
  - note: issue_712_reproducer_v1_partial_object
    data: {}
    input:
      servers:
        FOO: 1
        BAR: 2
        BAZ: 3
    modules:
      - |
        package test

        violations[k] if {
          some k, _ in input.servers
        }
    query: data.test.violations
    want_result:
      BAR: true
      BAZ: true
      FOO: true

  # v1: violations contains k if { ... } is a partial SET
  - note: issue_712_reproducer_v1_contains_partial_set
    data: {}
    input:
      servers:
        FOO: 1
        BAR: 2
        BAZ: 3
    modules:
      - |
        package test

        violations contains k if {
          some k, _ in input.servers
        }
    query: data.test.violations
    want_result:
      set!: ["BAR", "BAZ", "FOO"]

These three tests together lock down the v0/v1 semantic distinction that caused the original issue. Add them to both tests/rvm/rego/cases/partial_object_rules.yaml and tests/interpreter/cases/rule/partial_object_v1.yaml (the v0 case goes in the interpreter test since it's v0 syntax).

@anakrish
Copy link
Copy Markdown
Collaborator

anakrish commented May 8, 2026

@copilot Please add the issue #712 reproducer tests from the previous comment.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 8, 2026

Missing: Regression tests for the exact issue #712 reproducer

The PR fixes the bug but doesn't have a test for the exact scenario reported in the issue. Please add these to lock down v0/v1 semanti...

Added the issue #712 reproducer coverage in a639b9d. The RVM suite now includes the v1 partial-object and contains cases, and the interpreter suite includes those plus the v0 partial-set case with future.keywords.in.

@copilot Please add the issue #712 reproducer tests from the previous comment.

Done in a639b9d.

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 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +92 to +111
let Expr::RefBrack {
refr: prefix,
index,
..
} = refr.as_ref()
else {
return Ok(());
};

if Self::has_unsupported_bracket_prefix(prefix) {
return Err(CompilerError::PartialObjectNestedKeyUnsupported.at(refr.span()));
}

if Self::is_simple_literal(index) {
return Err(CompilerError::PartialObjectConstantKeyUnsupported.at(index.span()));
}

Ok(())
}

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 6 out of 6 changed files in this pull request and generated no new comments.

…ification

Partial object rules with dynamic keys (e.g. `violations[k] if { ... }`)
only produced a single entry instead of collecting all bindings. Two
independent bugs caused this:

1. Interpreter: the early-return optimization in eval_output_expr_in_loop
   checked whether the rule_ref was constant but never verified whether
   the key expression was also constant. A variable key like `k` was
   treated as constant output, causing the loop to exit after the first
   iteration. Fixed by gating early-return on key_expr constness.

2. RVM: compute_rule_type incorrectly classified `p[k] if { ... }` as
   PartialSet instead of PartialObject. OPA v1 semantics define this
   form as a partial object (key -> true). Fixed the classification and
   added compiler error guards for patterns the RVM codegen cannot yet
   handle (constant keys, nested bracket keys), ensuring graceful
   fallback to the interpreter.

The OPA test harness now skips RVM validation per-case when partial
object compiler errors are raised, rather than blanket-skipping entire
folders. This preserves RVM coverage for unrelated tests in the same
folders.

Closes #712
@anakrish anakrish force-pushed the copilot/fix-partial-set-rule-iteration branch from f81b8f6 to 74445fd Compare May 18, 2026 18:09
@anakrish anakrish merged commit dae3052 into main May 18, 2026
59 checks passed
@anakrish anakrish deleted the copilot/fix-partial-set-rule-iteration branch May 18, 2026 22:10
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.

regorus 0.9.1 — partial set rule with iterating body emits only one element

4 participants