Skip to content

bug: RVM compiler misclassifies bracket-syntax default rules (config["key"]) as PartialObject #662

@kusha

Description

@kusha

Summary

Rules using bracket-syntax with defaults (e.g., default config["timeout"] := 30) are misclassified by the RVM compiler as PartialObject instead of Complete. This causes incorrect evaluation when querying the specific rule path.

Reproduction

package test

default config["timeout"] := 30

config["timeout"] := 60 if {
  false
}

Expected

Query Expected Result
data.test.config.timeout 30
data.test.config {"timeout": 30}
data.test {"config": {"timeout": 30}}

Actual

Query Interpreter RVM
data.test.config.timeout {} (wrong) {} (wrong)
data.test.config {"timeout": 30} N/A (package query)
data.test {"config": {"timeout": 30}} N/A (package query)

Both the interpreter and RVM return incorrect results for the specific rule query data.test.config.timeout. Only the interpreter's package-level evaluation produces the correct result.

Root Cause

In compute_rule_type() (src/languages/rego/compiler/rules.rs), the rule type classification matches RefBrack with assignment as PartialObject:

RuleHead::Compr { refr, assign, .. } => match refr.as_ref() {
    crate::ast::Expr::RefBrack { .. } if assign.is_some() => {
        RuleType::PartialObject  // ← incorrect for literal keys
    }
    crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet,
    _ => RuleType::Complete,
},

The pattern config["timeout"] := 30 is parsed as RefBrack { index: Expr::String("timeout"), .. } with an assignment. The current code treats all RefBrack + assignment as PartialObject, but only variable keys produce partial objects. A string literal key is semantically equivalent to config.timeout — a complete rule assigned to a specific object key.

The distinction:

  • config[key] := value where key is Expr::VarPartialObject (dynamic key, generates entries for multiple keys)
  • config["timeout"] := value where "timeout" is Expr::StringComplete (static key, equivalent to dot access)

Proposed Fix

In compute_rule_type(), inspect the RefBrack.index expression to distinguish literal keys from variable keys:

RuleHead::Compr { refr, assign, .. } => match refr.as_ref() {
    crate::ast::Expr::RefBrack { index, .. } if assign.is_some() => {
        match index.as_ref() {
            // Literal key like config["timeout"] — treated as complete rule
            crate::ast::Expr::String { .. }
            | crate::ast::Expr::RawString { .. }
            | crate::ast::Expr::Number { .. } => RuleType::Complete,
            // Variable key like config[key] — partial object
            _ => RuleType::PartialObject,
        }
    }
    crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet,
    _ => RuleType::Complete,
},

This same fix should also be verified in the interpreter's eval_rule_in_path code path, which currently also returns {} for the specific query.

The skipped test case default_rule_with_object_key in tests/rvm/rego/cases/default_rules.yaml validates this scenario and should be unskipped after the fix.

Impact

Low — this pattern is uncommon in practice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions