Repository: open-policy-agent/opa
Pull request #7578: topdown: Improved default rule value inlining
Author: login: johanfylling
Created: 2025-05-13T15:25:38Z | Merged: 2025-05-19T10:00:20Z

Not generating support rules for cases where the default rule value is `false` and the rule is simply referenced; making the defaule value inconsequential.

Adding tests for asserting the trivial case, with only default rules, is already supported.

Fixes: #1418



Diff:
diff --git a/docs/content/rest-api.md b/docs/content/rest-api.md
index 283070d55f..e334e6e2f5 100644
--- a/docs/content/rest-api.md
+++ b/docs/content/rest-api.md
@@ -1483,7 +1483,7 @@ Content-Type: application/json
 
 When you partially evaluate a query with the Compile API, OPA returns a new set of queries and supporting policies. However, in some cases, the result of Partial Evaluation is a conclusive, unconditional answer.
 
-For example, if you extend to policy above to include a "break glass" condition, the decision may be to allow all requests regardless of clearance level.
+For example, if you extend the policy above to include a "break glass" condition, the decision may be to allow all requests regardless of clearance level.
 
 ```live:compile_unconditional_example:module:read_only
 package example
diff --git a/v1/topdown/eval.go b/v1/topdown/eval.go
index 248e2c17c8..9a02d763e4 100644
--- a/v1/topdown/eval.go
+++ b/v1/topdown/eval.go
@@ -447,6 +447,11 @@ func (e *eval) evalStep(iter evalIterator) error {
 		case *ast.Term:
 			// generateVar inlined here to avoid extra allocations in hot path
 			rterm := ast.VarTerm(e.fmtVarTerm())
+
+			if e.partial() {
+				e.inliningControl.PushDisable(rterm.Value, true)
+			}
+
 			err = e.unify(terms, rterm, func() error {
 				if e.saveSet.Contains(rterm, e.bindings) {
 					return e.saveExpr(ast.NewExpr(rterm), e.bindings, func() error {
@@ -461,6 +466,10 @@ func (e *eval) evalStep(iter evalIterator) error {
 				}
 				return nil
 			})
+
+			if e.partial() {
+				e.inliningControl.PopDisable()
+			}
 		case *ast.Every:
 			eval := evalEvery{
 				Every: terms,
@@ -3426,13 +3435,17 @@ func (e evalVirtualComplete) eval(iter unifyIterator) error {
 	var generateSupport bool
 
 	if e.ir.Default != nil {
-		// If the other term is not constant OR it's equal to the default value, then
-		// a support rule must be produced as the default value _may_ be required. On
-		// the other hand, if the other term is constant (i.e., it does not require
-		// evaluation) and it differs from the default value then the default value is
-		// _not_ required, so partially evaluate the rule normally.
-		rterm := e.rbindings.Plug(e.rterm)
-		generateSupport = !ast.IsConstant(rterm.Value) || e.ir.Default.Head.Value.Equal(rterm)
+		// If inlining has been disabled for the rterm, and the default rule has a 'false' result value,
+		// the default value is inconsequential, and support does not need to be generated.
+		if !(e.ir.Default.Head.Value.Equal(ast.InternedBooleanTerm(false)) && e.e.inliningControl.Disabled(e.rterm.Value, false)) {
+			// If the other term is not constant OR it's equal to the default value, then
+			// a support rule must be produced as the default value _may_ be required. On
+			// the other hand, if the other term is constant (i.e., it does not require
+			// evaluation) and it differs from the default value then the default value is
+			// _not_ required, so partially evaluate the rule normally.
+			rterm := e.rbindings.Plug(e.rterm)
+			generateSupport = !ast.IsConstant(rterm.Value) || e.ir.Default.Head.Value.Equal(rterm)
+		}
 	}
 
 	if generateSupport || e.e.inliningControl.shallow || e.e.inliningControl.Disabled(e.plugged[:e.pos+1], false) {
diff --git a/v1/topdown/save.go b/v1/topdown/save.go
index 15b61a7f77..4574469231 100644
--- a/v1/topdown/save.go
+++ b/v1/topdown/save.go
@@ -436,18 +436,44 @@ type inliningControl struct {
 type disableInliningFrame struct {
 	internal bool
 	refs     []ast.Ref
+	v        ast.Var
 }
 
-func (i *inliningControl) PushDisable(refs []ast.Ref, internal bool) {
+func (i *inliningControl) PushDisable(x any, internal bool) {
 	if i == nil {
 		return
 	}
+
+	switch x := x.(type) {
+	case []ast.Ref:
+		i.PushDisableRefs(x, internal)
+	case ast.Var:
+		i.PushDisableVar(x, internal)
+	}
+}
+
+func (i *inliningControl) PushDisableRefs(refs []ast.Ref, internal bool) {
+	if i == nil {
+		return
+	}
+
 	i.disable = append(i.disable, disableInliningFrame{
 		internal: internal,
 		refs:     refs,
 	})
 }
 
+func (i *inliningControl) PushDisableVar(v ast.Var, internal bool) {
+	if i == nil {
+		return
+	}
+
+	i.disable = append(i.disable, disableInliningFrame{
+		internal: internal,
+		v:        v,
+	})
+}
+
 func (i *inliningControl) PopDisable() {
 	if i == nil {
 		return
@@ -455,10 +481,26 @@ func (i *inliningControl) PopDisable() {
 	i.disable = i.disable[:len(i.disable)-1]
 }
 
-func (i *inliningControl) Disabled(ref ast.Ref, ignoreInternal bool) bool {
+func (i *inliningControl) Disabled(x any, ignoreInternal bool) bool {
 	if i == nil {
 		return false
 	}
+
+	switch x := x.(type) {
+	case ast.Ref:
+		return i.DisabledRef(x, ignoreInternal)
+	case ast.Var:
+		return i.DisabledVar(x, ignoreInternal)
+	}
+
+	return false
+}
+
+func (i *inliningControl) DisabledRef(ref ast.Ref, ignoreInternal bool) bool {
+	if i == nil {
+		return false
+	}
+
 	for _, frame := range i.disable {
 		if !frame.internal || !ignoreInternal {
 			for _, other := range frame.refs {
@@ -470,3 +512,16 @@ func (i *inliningControl) Disabled(ref ast.Ref, ignoreInternal bool) bool {
 	}
 	return false
 }
+
+func (i *inliningControl) DisabledVar(v ast.Var, ignoreInternal bool) bool {
+	if i == nil {
+		return false
+	}
+
+	for _, frame := range i.disable {
+		if (!frame.internal || !ignoreInternal) && frame.v.Equal(v) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/v1/topdown/topdown_partial_test.go b/v1/topdown/topdown_partial_test.go
index 68690bc545..e4f2870a8d 100644
--- a/v1/topdown/topdown_partial_test.go
+++ b/v1/topdown/topdown_partial_test.go
@@ -4200,6 +4200,768 @@ func TestTopDownPartialEval(t *testing.T) {
 					f(__local1__2) = true if { __local2__2 = __local1__2.size; lt(__local2__2, 100) }`,
 			},
 		},
+
+		// Default rule values should be inlined if only definition
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value comparison (true)",
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value unification (false)",
+			query: "data.test.q = false",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value comparison (false)",
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+
+		{
+			note:  "default rule inlining, sole default rule (boolean true), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := true
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean true), result value comparison (true)",
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := true
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean true), result value comparison (false)",
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := true
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+
+		{
+			note:  "default rule inlining, sole default rule (int), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := 42
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (int), result value comparison (same value)",
+			query: "data.test.q == 42",
+			modules: []string{`package test
+					default q := 42
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (int), result value comparison (different value)",
+			query: "data.test.q == 40",
+			modules: []string{`package test
+					default q := 42
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+
+		// Default rule values should be inlined if all other rules fail
+		{
+			note:  "default rule inlining, default rule (boolean false), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean true), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := true
+
+					q := false if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value unification (false)",
+			query: "data.test.q = false",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value comparison (true)",
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value comparison (false)",
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+
+		{
+			note:  "default rule inlining, default rule (int), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true (42 != false)
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value unification (different value)",
+			query: "data.test.q = 1",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value unification (same value)",
+			query: "data.test.q = 42",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value comparison (different value)",
+			query: "data.test.q == 1",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value comparison (same value)",
+			query: "data.test.q == 42",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+
+		{
+			note:  "default rule inlining, default rule (boolean false), unknowns in undefined rule, result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+						false 
+					}
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), unknowns in undefined rule, result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+						false 
+					}
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note: "default rule inlining, default rule (boolean false), unknowns in undefined rule, result value comparison (true)",
+			// Compiled query:
+			// __localq0__ = data.test.q; equal(__localq0__, true)
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+						false 
+					}
+				`},
+			// at eval-time of data.test.q, we don't know that __localq0__ is later compared with 'true' and support generation is superfluous.
+			// Can possibly be optimized into a no-support state by updating the compiler for this condition.
+			// E.g. for scalar values, replace equality (==) with unification (=): 'data.test.q == true' -> 'data.test.q = true'
+			wantQueries: []string{"data.partial.test.q == true"},
+			wantSupport: []string{`package partial.test
+					default q = false
+				`},
+		},
+		{
+			note: "default rule inlining, default rule (boolean false), unknowns in rule undefined, result value comparison (false)",
+			// Compiled query:
+			// __localq0__ = data.test.q; equal(__localq0__, false)
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := false
+
+					q if { 
+						input.x == 7
+						false 
+					}
+				`},
+			wantQueries: []string{"data.partial.test.q == false"},
+			wantSupport: []string{`package partial.test
+					default q = false
+				`},
+		},
+
+		{
+			note:  "default rule inlining, default rule (boolean false), unknowns in rule, result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+					}
+				`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), unknowns in rule, result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+					}
+				`},
+			wantQueries: []string{"input.x = 7"},
+		},
+
+		{
+			note:  "default rule inlining, default rule (boolean true), unknowns in rule, result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := true
+
+					q := false if {
+						input.x == 7
+					}
+				`},
+			// Default rule is not inconsequential, support module must be generated
+			wantQueries: []string{"data.partial.test.q"},
+			wantSupport: []string{`package partial.test
+					default q = true
+
+					q = false if {
+						input.x = 7
+					}
+				`},
+		},
+		{
+			note:  "default rule inlining, default rule (boolean true), unknowns in rule, result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := true
+
+					q := false if {
+						input.x == 7
+					}
+				`},
+			// Default rule is not inconsequential, support module must be generated
+			wantQueries: []string{"data.partial.test.q = true"},
+			wantSupport: []string{`package partial.test
+					default q = true
+
+					q = false if {
+						input.x = 7
+					}
+				`},
+		},
+		{
+			note:  "default rule inlining, default rule (boolean true), unknowns in rule, result value unification (false)",
+			query: "data.test.q = false",
+			modules: []string{`package test
+					default q := true
+
+					q := false if {
+						input.x == 7
+					}
+				`},
+			// Default rule is inconsequential
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note: "default rule inlining, default rule (boolean false), unknowns in rule, result value comparison (true)",
+			// Compiled query:
+			// __localq0__ = data.test.q; equal(__localq0__, true)
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+					}
+				`},
+			wantQueries: []string{"data.partial.test.q == true"},
+			wantSupport: []string{`package partial.test
+					default q = false
+
+					q = true if {
+						input.x = 7
+					}
+				`},
+		},
+
+		// indirect calls
+
+		{
+			note:  "default rule inlining (boolean false), indirect, unknowns in rule, result value presence",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q
+					}
+					
+					default q := false
+					
+					q := true if { input.x = 7 }
+				`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note:  "default rule inlining (boolean false), indirect, unknowns in rule, result value unification (true)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = true
+					}
+					
+					default q := false
+					
+					q := true if { input.x = 7 }
+				`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note:  "default rule inlining (boolean false), indirect, unknowns in rule, result value unification (false)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = false
+					}
+					
+					default q := false
+					
+					q := true if { input.x = 7 }
+				`},
+			wantQueries: []string{"data.partial.test.q = false"},
+			wantSupport: []string{`package partial.test
+					default q = false
+					q = true if { input.x = 7 }
+				`},
+		},
+		{
+			note:  "default rule inlining (boolean false), indirect, unknowns in rule, result value comparison (true)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q == true
+					}
+					
+					default q := false
+					
+					q := true if { input.x = 7 }
+				`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note:  "default rule inlining (boolean false), indirect, unknowns in rule, result value comparison (false)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q == false
+					}
+					
+					default q := false
+					
+					q := true if { input.x = 7 }
+				`},
+			wantQueries: []string{"data.partial.test.q = false"},
+			wantSupport: []string{`package partial.test
+					default q = false
+					q = true if { input.x = 7 }
+				`},
+		},
+
+		{
+			note:  "default rule inlining (boolean true), indirect, unknowns in rule, result value presence",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q
+					}
+					
+					default q := true
+					
+					q := false if { input.x = 7 }
+				`},
+			// Default rule is not inconsequential, support module must be generated
+			wantQueries: []string{"data.partial.test.q"},
+			wantSupport: []string{`package partial.test
+					q = false if { input.x = 7 }
+					default q = true
+				`},
+		},
+		{
+			note:  "default rule inlining (boolean true), indirect, unknowns in rule, result value unification (true)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = true
+					}
+					
+					default q := true
+					
+					q := false if { input.x = 7 }
+				`},
+			// Default rule is not inconsequential, support module must be generated
+			wantQueries: []string{"data.partial.test.q = true"},
+			wantSupport: []string{`package partial.test
+					q = false if { input.x = 7 }
+					default q = true
+				`},
+		},
+		{
+			note:  "default rule inlining (boolean true), indirect, unknowns in rule, result value comparison (true)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q == true
+					}
+					
+					default q := true
+					
+					q := false if { input.x = 7 }
+				`},
+			// Default rule is not inconsequential, support module must be generated
+			wantQueries: []string{"data.partial.test.q = true"},
+			wantSupport: []string{`package partial.test
+					q = false if { input.x = 7 }
+					default q = true
+				`},
+		},
+
+		{
+			note:  "default rule inlining (int), indirect, unknowns in rule, result value presence",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q
+					}
+					
+					default q := 42
+					
+					q := 1 if { input.x = 7 }
+				`},
+			// Default rule is not inconsequential, support module must be generated
+			wantQueries: []string{"data.partial.test.q"},
+			wantSupport: []string{`package partial.test
+					q = 1 if { input.x = 7 }
+					default q = 42
+				`},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in rule, result value unification (value eq non-default rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = 1
+					}
+					
+					default q := 42
+					
+					q := 1 if { input.x = 7 }
+				`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in rule, result value unification (value eq default rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = 42
+					}
+					
+					default q := 42
+					
+					q := 1 if { input.x = 7 }
+				`},
+			wantQueries: []string{"data.partial.test.q = 42"},
+			wantSupport: []string{`package partial.test
+					q = 1 if { input.x = 7 }
+					default q = 42
+				`},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in rule, result value unification (value eq no rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = 20
+					}
+					
+					default q := 42
+					
+					q := 1 if { input.x = 7 }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in rule, var result, result value unification",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = 20
+					}
+					
+					default q := 42
+					
+					q := x if { input.x = 7; x := input.y }
+				`},
+			wantQueries: []string{"input.x = 7; 20 = input.y"},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in rule, var result, result value unification (value eq default rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						q = 42
+					}
+					
+					default q := 42
+					
+					q := x if {
+						input.x = 7
+						x := input.y 
+					}
+				`},
+			wantQueries: []string{"data.partial.test.q = 42"},
+			wantSupport: []string{`package partial.test
+					default q = 42
+					q = __local0__2 if { 
+						input.x = 7
+						__local0__2 = input.y 
+					}
+				`},
+		},
+
+		{
+			note:  "default rule inlining (int), indirect, unknowns in multiple rules, result value presence",
+			query: "data.test.p",
+			modules: []string{`package test
+						p if {
+							q
+						}
+						
+						default q := 40
+						
+						q := 41 if { input.x = 6 }
+						q := 42 if { input.x = 7 }
+						q := 43 if { input.x = 8 }
+					`},
+			wantQueries: []string{"data.partial.test.q"}, // Should be unconditionally true?
+			wantSupport: []string{`package partial.test
+					default q = 40
+					q = 41 if { input.x = 6 }
+					q = 42 if { input.x = 7 }
+					q = 43 if { input.x = 8 }
+				`},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in multiple rules, result value unification (same as default rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+						p if {
+							q = 40
+						}
+						
+						default q := 40
+						
+						q := 41 if { input.x = 6 }
+						q := 42 if { input.x = 7 }
+						q := 43 if { input.x = 8 }
+					`},
+			wantQueries: []string{"data.partial.test.q = 40"},
+			wantSupport: []string{`package partial.test
+					default q = 40
+					q = 41 if { input.x = 6 }
+					q = 42 if { input.x = 7 }
+					q = 43 if { input.x = 8 }
+				`},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in multiple rules, result value unification (same as rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+						p if {
+							q = 42
+						}
+						
+						default q := 40
+						
+						q := 41 if { input.x = 6 }
+						q := 42 if { input.x = 7 }
+						q := 43 if { input.x = 8 }
+					`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in multiple rules, result value unification (same as no rule)",
+			query: "data.test.p",
+			modules: []string{`package test
+						p if {
+							q = 44
+						}
+						
+						default q := 40
+						
+						q := 41 if { input.x = 6 }
+						q := 42 if { input.x = 7 }
+						q := 43 if { input.x = 8 }
+					`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining (int), indirect, unknowns in multiple rules, var result, result value unification",
+			query: "data.test.p",
+			modules: []string{`package test
+						p if {
+							q = 44
+						}
+						
+						default q := 40
+						
+						q := 41 if { input.x = 6 }
+						q := x if { input.x = 7; x := input.y }
+						q := x if { input.x = 8; x := input.y }
+					`},
+			wantQueries: []string{
+				"input.x = 7; 44 = input.y",
+				"input.x = 8; 44 = input.y",
+			},
+		},
+
+		{
+			note:  "default rule inlining, sole ref, not",
+			query: "data.test.p",
+			modules: []string{`package test
+					p if {
+						not q
+					}
+					
+					default q := false
+					
+					q if { input.x = 7 }
+				`},
+			wantQueries: []string{"not input.x = 7"},
+		},
+
+		{ // Regression test for https://github.com/open-policy-agent/opa/issues/1418
+			note:  "regression, good",
+			query: "data.x.p_good",
+			modules: []string{`package x
+
+p_bad if {
+    q
+}
+
+p_good if {
+    q == true
+}
+
+default q := false
+
+q if { input.x = 7 }`},
+			wantQueries: []string{"input.x = 7"},
+		},
+		{ // Regression test for https://github.com/open-policy-agent/opa/issues/1418
+			note:  "regression, bad",
+			query: "data.x.p_bad",
+			modules: []string{`package x
+
+p_bad if {
+    q
+}
+
+p_good if {
+    q == true
+}
+
+default q := false
+
+q if { input.x = 7 }`},
+			wantQueries: []string{"input.x = 7"},
+		},
 	}
 
 	ctx := context.Background()
diff --git a/v1/topdown/trace_test.go b/v1/topdown/trace_test.go
index 3f1bf0b97c..ce513530b0 100644
--- a/v1/topdown/trace_test.go
+++ b/v1/topdown/trace_test.go
@@ -361,6 +361,9 @@ authz_bundle/...ternal/authz/policies/utils/user.rego:14             | | | Eval
 authz_bundle/...ternal/authz/policies/utils/user.rego:14             | | | Save "widgets" = input.action.resource
 authz_bundle/...ternal/authz/policies/utils/user.rego:10             | | | Exit data.utils.role_has_permission
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6     | | Exit data.example_rbac.allow
+query:1                                                              | Exit data.example_rbac.allow
+query:1                                                              Redo data.example_rbac.allow
+query:1                                                              | Redo data.example_rbac.allow
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6     | Redo data.example_rbac.allow
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9     | | Redo data.utils.role_has_permission[role_name]
 authz_bundle/...ternal/authz/policies/utils/user.rego:10             | | Redo data.utils.role_has_permission
@@ -394,6 +397,9 @@ authz_bundle/...ternal/authz/policies/utils/user.rego:14             | | | Eval
 authz_bundle/...ternal/authz/policies/utils/user.rego:14             | | | Save "widgets" = input.action.resource
 authz_bundle/...ternal/authz/policies/utils/user.rego:10             | | | Exit data.utils.role_has_permission
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6     | | Exit data.example_rbac.allow
+query:1                                                              | Exit data.example_rbac.allow
+query:1                                                              Redo data.example_rbac.allow
+query:1                                                              | Redo data.example_rbac.allow
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6     | Redo data.example_rbac.allow
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:9     | | Redo data.utils.role_has_permission[role_name]
 authz_bundle/...ternal/authz/policies/utils/user.rego:10             | | Redo data.utils.role_has_permission
@@ -406,16 +412,6 @@ authz_bundle/...ternal/authz/policies/utils/user.rego:4              | | Redo da
 authz_bundle/...ternal/authz/policies/utils/user.rego:7              | | | Redo role_binding.user = input.subject.user
 authz_bundle/...ternal/authz/policies/utils/user.rego:6              | | | Redo role_binding.role = role_name
 authz_bundle/...ternal/authz/policies/utils/user.rego:5              | | | Redo role_binding = data.bindings[_]
-authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4     | Enter data.example_rbac.allow
-authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4     | | Eval true
-authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4     | | Exit data.example_rbac.allow
-authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4     | Redo data.example_rbac.allow
-authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:4     | | Redo true
-query:1                                                              | Save data.partial.example_rbac.allow = _
-query:1                                                              | Save _
-query:1                                                              | Exit data.example_rbac.allow
-query:1                                                              Redo data.example_rbac.allow
-query:1                                                              | Fail data.example_rbac.allow
 `
 
 	var buf bytes.Buffer


Reviews:
user: login: johanfylling
body: Some notes to aid reviewing.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T12:55:44Z
user: login: srenatus
body: LGTM! Thanks for adding soo many tests 😅 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:25:04Z

Review comments:
diff_hunk: @@ -447,6 +447,11 @@ func (e *eval) evalStep(iter evalIterator) error {
 		case *ast.Term:
 			// generateVar inlined here to avoid extra allocations in hot path
 			rterm := ast.VarTerm(e.fmtVarTerm())
+
+			if e.partial() {
+				e.inliningControl.PushDisable(rterm.Value, true)
path: v1/topdown/eval.go
user: login: johanfylling
body: Here, we're pushing frames on top of the same stack used for the `with` kw. It should be safe, as we should have a consistent unroll sequence. This might however impact performance for large stacks, as membership checks involves traversing the stack until a match is found.
An alternative would be to have two separate data structures for `with` and presence checks for "naked" terms. This new inlining control could possibly even be a simple map instead of a stack, as `rterm` should always be assigned a unique var name(?).
created_at: 2025-05-15T12:51:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,6 +361,9 @@ authz_bundle/...ternal/authz/policies/utils/user.rego:14             | | | Eval
 authz_bundle/...ternal/authz/policies/utils/user.rego:14             | | | Save "widgets" = input.action.resource
 authz_bundle/...ternal/authz/policies/utils/user.rego:10             | | | Exit data.utils.role_has_permission
 authz_bundle/...ternal/authz/policies/rbac/v1/beta/policy.rego:6     | | Exit data.example_rbac.allow
+query:1                                                              | Exit data.example_rbac.allow
+query:1                                                              Redo data.example_rbac.allow
+query:1                                                              | Redo data.example_rbac.allow
path: v1/topdown/trace_test.go
user: login: johanfylling
body: We're no longer generating a support module here, which changes the trace.
created_at: 2025-05-15T12:52:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -470,3 +512,18 @@ func (i *inliningControl) Disabled(ref ast.Ref, ignoreInternal bool) bool {
 	}
 	return false
 }
+
+func (i *inliningControl) DisabledVar(v ast.Var, ignoreInternal bool) bool {
+	if i == nil {
+		return false
+	}
+
+	for _, frame := range i.disable {
+		if !frame.internal || !ignoreInternal {
+			if frame.v.Equal(v) {
+				return true
+			}
+		}
path: v1/topdown/save.go
user: login: srenatus
body: 🤔 
```suggestion
		if !frame.internal || !ignoreInternal && frame.v.Equal(v) {
			return true
		}
```
...or is it not the same? 
created_at: 2025-05-16T12:20:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4200,6 +4200,768 @@ func TestTopDownPartialEval(t *testing.T) {
 					f(__local1__2) = true if { __local2__2 = __local1__2.size; lt(__local2__2, 100) }`,
 			},
 		},
+
+		// Default rule values should be inlined if only definition
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value comparison (true)",
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value unification (false)",
+			query: "data.test.q = false",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean false), result value comparison (false)",
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := false
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+
+		{
+			note:  "default rule inlining, sole default rule (boolean true), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := true
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean true), result value comparison (true)",
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := true
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (boolean true), result value comparison (false)",
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := true
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+
+		{
+			note:  "default rule inlining, sole default rule (int), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := 42
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (int), result value comparison (same value)",
+			query: "data.test.q == 42",
+			modules: []string{`package test
+					default q := 42
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, sole default rule (int), result value comparison (different value)",
+			query: "data.test.q == 40",
+			modules: []string{`package test
+					default q := 42
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+
+		// Default rule values should be inlined if all other rules fail
+		{
+			note:  "default rule inlining, default rule (boolean false), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean true), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := true
+
+					q := false if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value unification (false)",
+			query: "data.test.q = false",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value comparison (true)",
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), result value comparison (false)",
+			query: "data.test.q == false",
+			modules: []string{`package test
+					default q := false
+
+					q if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+
+		{
+			note:  "default rule inlining, default rule (int), result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true (42 != false)
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value unification (different value)",
+			query: "data.test.q = 1",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value unification (same value)",
+			query: "data.test.q = 42",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value comparison (different value)",
+			query: "data.test.q == 1",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (int), result value comparison (same value)",
+			query: "data.test.q == 42",
+			modules: []string{`package test
+					default q := 42
+
+					q := 1 if { false }
+				`},
+			wantQueries: []string{""}, // unconditionally true
+		},
+
+		{
+			note:  "default rule inlining, default rule (boolean false), unknowns in undefined rule, result value presence",
+			query: "data.test.q",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+						false 
+					}
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note:  "default rule inlining, default rule (boolean false), unknowns in undefined rule, result value unification (true)",
+			query: "data.test.q = true",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+						false 
+					}
+				`},
+			wantQueries: []string{}, // unconditionally false
+		},
+		{
+			note: "default rule inlining, default rule (boolean false), unknowns in undefined rule, result value comparison (true)",
+			// Compiled query:
+			// __localq0__ = data.test.q; equal(__localq0__, true)
+			query: "data.test.q == true",
+			modules: []string{`package test
+					default q := false
+
+					q if {
+						input.x == 7
+						false 
+					}
+				`},
+			// at eval-time of data.test.q, we don't know that __localq0__ is later compared with 'true' and support generation is superfluous.
+			// Can possibly be optimized into a no-support state by updating the compiler for this condition.
+			// E.g. for scalar values, replace equality (==) with unification (=): 'data.test.q == true' -> 'data.test.q = true'
path: v1/topdown/topdown_partial_test.go
user: login: srenatus
body: Yup. I suppose alternatively, we could have a post-processing step on the support modules? This could _then_ be dropped... 🤔 
created_at: 2025-05-16T12:24:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-13T15:27:27Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *openpolicyagent* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 3e2c0f9d55a6e35027a1d6a3026efa4d93e96f48 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/openpolicyagent/deploys/682afedb3c48460008933f82 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-7578--openpolicyagent.netlify.app](https://deploy-preview-7578--openpolicyagent.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTc1NzgtLW9wZW5wb2xpY3lhZ2VudC5uZXRsaWZ5LmFwcCJ9.qp5VX-dFlCj-NIf-fAwxaU2Pjy6-xv4XBFKjjFrlqts)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [openpolicyagent Preview](https://deploy-preview-7578--openpolicyagent.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/openpolicyagent/configuration/notifications#deploy-notifications)._
author_association: NONE
user: login: johanfylling
created_at: 2025-05-13T17:01:58Z
body: Failing tests. Will look into those.
author_association: CONTRIBUTOR