Skip to content

Commit 4d5f678

Browse files
committed
internal/core/adt: evaluate pattern values when registering them
The or() builtin returns a Disjunction holding raw, unevaluated list elements. When such a value was used as a pattern label, the pattern was registered as-is, deferring evaluation of its branches until matchPatternValue force-finalized them upon a later field insertion. At that point a node the branches depend on may be mid-evaluation, so a comprehension over it would see a partial field set, collapsing the inner or() and locking the spurious result in permanently. Evaluate vertices held by a pattern value when the pattern constraint task runs, mirroring what already happens for resolvers and literal disjunctions used as pattern labels: at this point dependencies can still be run reentrantly, or block and requeue the task. This keeps or() lazy in all other contexts, retaining the performance win of returning a Disjunction directly. Fixes #4399 Signed-off-by: Daniel Martí <mvdan@mvdan.cc> Change-Id: I5ba836a2a24343b6b6ce1a03c192cf0a5e75b7f4 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1242737 TryBot-Result: CUEcueckoo <cueckoo@cuelang.org> Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com> Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com>
1 parent 164e85f commit 4d5f678

5 files changed

Lines changed: 58 additions & 37 deletions

File tree

cue/testdata/cycle/issue4397.txtar

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ result: {"cue": cue, "tags": tags} @test(eq, {
1919
tags: {cue: {modules: ["login", "mod"]}}
2020
})
2121
-- out/eval/stats --
22-
Leaks: 1
23-
Freed: 26
24-
Reused: 4
25-
Allocs: 23
22+
Leaks: 2
23+
Freed: 28
24+
Reused: 9
25+
Allocs: 21
2626
Retain: 0
2727

2828
Unifications: 18
29-
Conjuncts: 38
30-
Disjuncts: 2
29+
Conjuncts: 43
30+
Disjuncts: 4
3131

32-
NumCloseIDs: 13
32+
NumCloseIDs: 17

cue/testdata/cycle/issue4399.txtar

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# A pattern label using or() over a cyclic reference collapses the inner or()
2-
# disjunction to the first field of cue when close() adds a field that is not
3-
# in tags.cue.modules: the pattern is registered as a Disjunction holding
4-
# unevaluated list elements, which are then force-finalized while cue is
5-
# mid-evaluation, computing the inner or() from a partial field set.
1+
# A pattern label using or() over a cyclic reference used to collapse the
2+
# inner or() disjunction to the first field of cue when close() added a field
3+
# that is not in tags.cue.modules: the pattern was registered as a Disjunction
4+
# holding unevaluated list elements, which were then force-finalized while cue
5+
# was mid-evaluation, computing the inner or() from a partial field set.
66
-- in.cue --
77
cue: {
88
[or(tags.cue.modules)]: y: {}
@@ -17,30 +17,19 @@ cue: close({
1717
mod: _
1818
})
1919

20-
result: {"cue": cue, "tags": tags} @test(err, code=eval, contains="conflicting values \"other\" and \"login\"", pos=[-9:27, -9:49, -9:50, -8:22]) @test(eq:todo, {
20+
result: {"cue": cue, "tags": tags} @test(eq, {
2121
cue: {other: {}, login: {y: {}}, mod: {y: {}}}
2222
tags: {cue: {modules: ["login", "mod"]}}
2323
})
24-
-- out/errors.txt --
25-
[eval] tags.cue.modules.0: conflicting values "other" and "login":
26-
./in.cue:5:27
27-
./in.cue:5:49
28-
./in.cue:5:50
29-
./in.cue:6:22
30-
[eval] tags.cue.modules.1: conflicting values "other" and "mod":
31-
./in.cue:5:27
32-
./in.cue:5:49
33-
./in.cue:5:50
34-
./in.cue:6:31
3524
-- out/eval/stats --
36-
Leaks: 0
37-
Freed: 25
38-
Reused: 2
39-
Allocs: 23
25+
Leaks: 2
26+
Freed: 35
27+
Reused: 12
28+
Allocs: 25
4029
Retain: 0
4130

42-
Unifications: 17
43-
Conjuncts: 30
44-
Disjuncts: 0
31+
Unifications: 19
32+
Conjuncts: 52
33+
Disjuncts: 6
4534

46-
NumCloseIDs: 9
35+
NumCloseIDs: 25

internal/core/adt/constraints.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ func matchPattern(ctx *OpContext, pattern Value, f Feature) bool {
135135
// This is an optimization an intended to be faster than regular CUE evaluation
136136
// for the majority of cases where pattern constraints are used.
137137
func matchPatternValue(ctx *OpContext, pattern Value, f Feature) (result bool) {
138+
// Normally completePatternValue has already evaluated the pattern at
139+
// registration time; this finalize remains as a backstop.
138140
if v, ok := pattern.(*Vertex); ok {
139141
v.unify(ctx, Flags{condition: scalarKnown, mode: finalize, checkTypos: false})
140142
}

internal/core/adt/fields.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,39 @@ func (n *nodeContext) addConstraint(arc *Vertex, mode ArcType, c Conjunct, check
334334
arc.insertConjunct(n.ctx, c, c.CloseInfo, mode, check, false)
335335
}
336336

337+
// completePatternValue evaluates any vertices held by a pattern value before
338+
// it is registered as a pattern constraint. Values returned by builtins, such
339+
// as a Disjunction returned by or(), may hold vertices that have not been
340+
// evaluated yet. If their evaluation were left to matchPatternValue, it could
341+
// be forced at a point where a node they depend on is mid-evaluation, locking
342+
// in results computed from a partial field set. Evaluating them here, while
343+
// dependencies can still be run or block the calling task, avoids that.
344+
// See https://cuelang.org/issue/4399.
345+
//
346+
// The traversal mirrors the Value kinds that matchPatternValue fast-tracks;
347+
// values it does not descend into, such as the arguments of a
348+
// BuiltinValidator, are still finalized late by matchPatternValue.
349+
func completePatternValue(ctx *OpContext, v Value) {
350+
var vals []Value
351+
switch x := v.(type) {
352+
case *Vertex:
353+
x.unify(ctx, Flags{condition: scalarValue, mode: yield})
354+
return
355+
case *Disjunction:
356+
vals = x.Values
357+
case *Conjunction:
358+
vals = x.Values
359+
}
360+
for _, a := range vals {
361+
completePatternValue(ctx, a)
362+
}
363+
}
364+
337365
func (n *nodeContext) insertPattern(pattern Value, c Conjunct) {
338366
n.assertInitialized()
339367

368+
completePatternValue(n.ctx, pattern)
369+
340370
// Collect patterns in root vertex. This allows comparing disjuncts for
341371
// equality as well as inserting new arcs down the line as they are
342372
// inserted.

internal/core/compile/testdata/sync/cycle/issue4399.txtar

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# A pattern label using or() over a cyclic reference collapses the inner or()
2-
# disjunction to the first field of cue when close() adds a field that is not
3-
# in tags.cue.modules: the pattern is registered as a Disjunction holding
4-
# unevaluated list elements, which are then force-finalized while cue is
5-
# mid-evaluation, computing the inner or() from a partial field set.
1+
# A pattern label using or() over a cyclic reference used to collapse the
2+
# inner or() disjunction to the first field of cue when close() added a field
3+
# that is not in tags.cue.modules: the pattern was registered as a Disjunction
4+
# holding unevaluated list elements, which were then force-finalized while cue
5+
# was mid-evaluation, computing the inner or() from a partial field set.
66
-- in.cue --
77
cue: {
88
[or(tags.cue.modules)]: y: {}

0 commit comments

Comments
 (0)