Skip to content

closedness appears to be lost when using a comprehension #4180

@jlongtine

Description

@jlongtine

Summary

CUE fails to properly unify values with disjunction types when the unification occurs inside a for loop or if conditional, resulting in an "incomplete value" error. The same code works correctly when the unification is applied via a pattern constraint after the loop.

CUE Version

cue version (devel)
CUE language version v0.15.0

Expected Behavior

When a value like {Ref: "DBCluster"} is unified with a field constrained to (string) | #Fn where #Fn is a disjunction (#Ref | #Base64), CUE should recognize that the value satisfies the #Ref branch of the disjunction and successfully unify.

This should work consistently regardless of whether the unification happens:

  • Directly in a field assignment
  • Inside a for loop
  • Inside an if conditional
  • Via a pattern constraint

Actual Behavior

When the unification occurs inside a for loop or if conditional with an open struct (Properties?: {...}), CUE cannot determine which branch of the disjunction to take and reports:

incomplete value {Ref:"DBCluster"} | {Ref:"DBCluster","Fn::Base64":string}

The error suggests CUE is considering whether the value could have additional fields (like "Fn::Base64") due to the open struct, even though the concrete value {Ref: "DBCluster"} clearly only has a Ref field. The other thing that surprises me is the fact that it appears it is trying to unify the concrete {Ref:"DBCluster"} with a (closed) definition #Base64: {"Fn::Base64": string }

Minimal Reproduction

Case 1: Works ✅ (Pattern constraint workaround)

File: template-works.cue

package cloudformation

// Typed definitions
#Ref: {Ref: string}

#Base64: {
	"Fn::Base64": string
}

// The problematic union - 2 intrinsics
#Fn: #Ref | #Base64

stacks: "test-stack": {
	let instances = [{Id: "01"}]
	Resources: {
		for instance in instances {
			"DBInstance\(instance.Id)": {
				Properties: DBClusterIdentifier: Ref: "DBCluster"
			}
		}
	}
	Resources: [=~"^DBInstance\\d+$"]: #DBInstance
}

#Resource: {
	Properties?: {...}
}

#DBInstance: #Resource & {
	Properties: DBClusterIdentifier?: (string) | #Fn
}

Command: cue export template-works.cue

Result: ✅ Exports successfully

Note: The constraint is applied after the for loop via a pattern match. This allows the concrete value to be created first, then constrained.


Case 2: Fails ❌ (for loop with direct constraint)

File: template-breaks.cue

package cloudformation

// Typed definitions
#Ref: {Ref: string}

#Base64: {
	"Fn::Base64": string
}

// The problematic union - 2 intrinsics
#Fn: #Ref | #Base64

stacks: "test-stack": {
	let instances = [{Id: "01"}]
	Resources: {
		for instance in instances {
			"DBInstance\(instance.Id)": #DBInstance & {
				Properties: DBClusterIdentifier: Ref: "DBCluster"
			}
		}
	}
}

#Resource: {
	Properties?: {...}
}

#DBInstance: #Resource & {
	Properties: DBClusterIdentifier?: (string) | #Fn
}

Command: cue export template-breaks.cue

Result: ❌ Fails with:

stacks."test-stack".Resources.DBInstance01.Properties.DBClusterIdentifier: incomplete value {Ref:"DBCluster"} | {Ref:"DBCluster","Fn::Base64":string}

Difference: The constraint #DBInstance is applied inside the for loop.


Case 3: Fails ❌ (if conditional with direct constraint)

File: template-breaks-if.cue

package cloudformation

// Typed definitions
#Ref: {Ref: string}

#Base64: {
	"Fn::Base64": string
}

// The problematic union - 2 intrinsics
#Fn: #Ref | #Base64

stacks: "test-stack": {
	let instances = [{Id: "01"}]
	Resources: {
		if instances != _|_ {
			"DBInstance\(instances[0].Id)": #DBInstance & {
				Properties: DBClusterIdentifier: Ref: "DBCluster"
			}
		}
	}
}

#Resource: {
	Properties?: {...}
}

#DBInstance: #Resource & {
	Properties: DBClusterIdentifier?: (string) | #Fn
}

Command: cue export template-breaks-if.cue

Result: ❌ Fails with:

stacks."test-stack".Resources.DBInstance01.Properties.DBClusterIdentifier: incomplete value {Ref:"DBCluster"} | {Ref:"DBCluster","Fn::Base64":string}

Difference: The constraint #DBInstance is applied inside an if conditional.


Root Cause Analysis

The issue appears to be related to how CUE handles disjunction resolution in the presence of:

  1. Open structs (Properties?: {...}) - allows additional fields
  2. Disjunctions (#Fn: #Ref | #Base64) - multiple possible types
  3. Control flow (for loops or if conditionals) - delayed evaluation context

Additional Context

This issue was discovered while building a CloudFormation Infrastructure-as-Code library in CUE, where resource properties need to accept either literal values (strings) or intrinsic functions (various struct types like {Ref: string}, {"Fn::Base64": string}, etc.).

The current definition of #Resource with Properties being open is required because of the large number (~ 1450) of potential CloudFormation Resources types. If I create a disjunction of all of the possibilities, evaluation time gets rather hairy. I'll plan to submit a performance test for that soon.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsFixbisectedRegression which has been bisected to one changeevalv3issues affecting only the evaluator version 3

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions