Skip to content

eval: nested struct unification in list comprehension guard differs from let binding #4300

@myitcv

Description

@myitcv

What version of CUE are you using?

cue version v0.16.0-rc.1.0.20260303225502-310f61c32cf1

What did you do?

A deeply nested struct unification expression evaluates differently when used inline as a list comprehension guard vs when bound to a field and then referenced in the guard.

Minimal reproducer:

exec cue export x.cue
cmp stdout stdout.golden

-- x.cue --
_nonzero: {
	#arg?: _
	out: [if #arg != _|_ {
		[
			if (#arg & bool) != _|_ {#arg},
			false,
		][0]
	}, false][0]
}

// Bound to a field: evaluates correctly (true)
cond: (_nonzero & {#arg: [if (_nonzero & {#arg: !((_nonzero & {#arg: false}).out)}).out {
	!((_nonzero & {#arg: false}).out)
}, false][0]}).out

// Using the binding: produces ["yes"]
outputA: [
	if cond {"yes"},
]

// Inline (same expression): produces [] — should produce ["yes"]
outputB: [
	if (_nonzero & {#arg: [if (_nonzero & {#arg: !((_nonzero & {#arg: false}).out)}).out {
		!((_nonzero & {#arg: false}).out)
	}, false][0]}).out {"yes"},
]
-- stdout.golden --
{
    "cond": true,
    "outputA": [
        "yes"
    ],
    "outputB": [
        "yes"
    ]
}

What did you expect to see?

Passing test.

Both outputA and outputB should produce ["yes"] since the guard expression is the same (just inline vs bound).

What did you see instead?

[stdout]
{
    "cond": true,
    "outputA": [
        "yes"
    ],
    "outputB": []
}
> cmp stdout stdout.golden
diff stdout stdout.golden
--- stdout
+++ stdout.golden
@@ -3,5 +3,7 @@
     "outputA": [
         "yes"
     ],
-    "outputB": []
+    "outputB": [
+        "yes"
+    ]
 }

FAIL: repro.txtar:2: stdout and stdout.golden differ

outputB is empty — the inline guard evaluates to false (or incomplete) even though the identical expression bound to cond evaluates to true.

Context

This was discovered while working on helm2cue (cue-exp/helm2cue#120), where the converter generates deeply nested _nonzero truthiness checks. The workaround is to bind the condition to a let/field before using it as a guard, but programmatic CUE generation naturally produces the inline form.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions