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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions