cue: fix reentrancy issue
even though this is not something we want to support
Change-Id: I84e986d4b8276d6258679ebc37625565f5fb5c25
diff --git a/cue/binop.go b/cue/binop.go
index a637f56..682dd89 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -450,7 +450,7 @@
return x
}
arcs := make(arcs, 0, len(x.arcs)+len(y.arcs))
- obj := &structLit{binSrc(src.Pos(), op, x, other), x.emit, nil, nil, arcs}
+ obj := &structLit{binSrc(src.Pos(), op, x, other), x.emit, nil, nil, arcs, nil}
defer ctx.pushForwards(x, obj, y, obj).popForwards()
tx, ex := evalLambda(ctx, x.template)
diff --git a/cue/copy.go b/cue/copy.go
index 39ef674..880c769 100644
--- a/cue/copy.go
+++ b/cue/copy.go
@@ -31,7 +31,7 @@
case *structLit:
arcs := make(arcs, len(x.arcs))
- obj := &structLit{x.baseValue, nil, nil, nil, arcs}
+ obj := &structLit{x.baseValue, nil, nil, nil, arcs, nil}
defer ctx.pushForwards(x, obj).popForwards()
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index f44da59..6c9032c 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -807,7 +807,7 @@
}
a x: "hey"
`,
- out: `<1>{a: <2>{x: _|_(((<0>.y + "?") & "hey"):constraint violated: _|_("hey":delayed constraint ((<0>.y + "?") & "hey") violated)), y: _|_(cycle detected)}}`,
+ out: `<0>{a: <1>{x: _|_(((<2>.y + "?") & "hey"):constraint violated: _|_(("hey!?" & "hey"):failed to unify: hey!? != hey)), y: "hey!"}}`,
}, {
desc: "resolved self-reference cycles with disjunctions",
in: `
@@ -1087,6 +1087,43 @@
`,
out: `<0>{res: [<1>{<>: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <5>{a: "XDDDD", c: "X", b: <6>{str: "DDDD"}}}], ` +
`t: <7>{<>: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <8>{a: "XDDDD", c: "X", b: <9>{str: "DDDD"}}}}`,
+ }, {
+ // TODO: A nice property for CUE to have would be that evaluation time
+ // is proportional to the number of output nodes (note that this is
+ // not the same as saying that the running time is O(n)).
+ // We should probably disallow shenanigans like the one below. But until
+ // this is allowed, it should at least be correct. At least we are not
+ // making reentrant coding easy.
+ desc: "reentrance",
+ in: `
+ // This indirection is needed to avoid binding references to fib
+ // within fib to the instantiated version.
+ fibRec: {nn: int, out: (fib & {n: nn}).out}
+ fib: {
+ n: int
+
+ out: (fibRec & {nn: n - 2}).out + (fibRec & {nn: n - 1}).out if n >= 2
+ out: n if n < 2
+ }
+ fib2: (fib & {n: 2}).out
+ fib7: (fib & {n: 7}).out
+ fib12: (fib & {n: 12}).out
+ `,
+ out: `<0>{fibRec: <1>{nn: int, out: _|_((<2>.fib & <3>{n: <4>.nn}).out:undefined field "out")}, fib: <5>{n: int}, ` +
+ `fib2: 1, ` +
+ `fib7: 13, ` +
+ `fib12: 144}`,
+ }}
+ rewriteHelper(t, testCases, evalFull)
+}
+
+func TestX(t *testing.T) {
+ t.Skip()
+
+ // Don't remove. For debugging.
+ testCases := []testCase{{
+ in: `
+ `,
}}
rewriteHelper(t, testCases, evalFull)
}
diff --git a/cue/rewrite_test.go b/cue/rewrite_test.go
index a6db968..19e8d8b 100644
--- a/cue/rewrite_test.go
+++ b/cue/rewrite_test.go
@@ -74,7 +74,7 @@
}
x = e.(*structLit)
}
- x.expand(ctx)
+ x = x.expandFields(ctx)
arcs := make(arcs, len(x.arcs))
for i, a := range x.arcs {
v := x.at(ctx, i)
@@ -89,7 +89,7 @@
t = v
}
emit := testResolve(ctx, x.emit, m)
- obj := &structLit{x.baseValue, emit, t, nil, arcs}
+ obj := &structLit{x.baseValue, emit, t, nil, arcs, nil}
return obj
case *list:
a := make([]value, len(x.a))
diff --git a/cue/strip.go b/cue/strip.go
index 3822ab2..aaf585a 100644
--- a/cue/strip.go
+++ b/cue/strip.go
@@ -30,7 +30,7 @@
eval := ctx.manifest(v)
switch x := eval.(type) {
case *structLit:
- x.expand(ctx)
+ x = x.expandFields(ctx)
if x.template != nil {
arcs := make(arcs, len(x.arcs))
for i, a := range x.arcs {
@@ -38,7 +38,7 @@
arcs[i] = arc{a.feature, v, nil}
}
// TODO: verify that len(x.comprehensions) == 0
- return &structLit{x.baseValue, x.emit, nil, nil, arcs}, false
+ return &structLit{x.baseValue, x.emit, nil, nil, arcs, nil}, false
}
}
return eval, true
diff --git a/cue/types.go b/cue/types.go
index 42a3a54..8f669e3 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -794,7 +794,8 @@
}
obj := v.eval(ctx).(*structLit)
- obj.expand(ctx) // expand comprehensions
+ // TODO: This is expansion appropriate?
+ obj = obj.expandFields(ctx) // expand comprehensions
// check if any labels are hidden
f := label(0)
@@ -818,6 +819,7 @@
obj.template,
nil,
arcs,
+ nil,
}
}
@@ -829,7 +831,7 @@
return structValue{}, err
}
obj := v.eval(ctx).(*structLit)
- obj.expand(ctx)
+ obj = obj.expandFields(ctx)
return structValue{ctx, v.path, obj}, nil
}
diff --git a/cue/validate.go b/cue/validate.go
index 336f411..7e17216 100644
--- a/cue/validate.go
+++ b/cue/validate.go
@@ -22,7 +22,7 @@
}
switch x := eval.(type) {
case *structLit:
- x.expand(ctx)
+ x = x.expandFields(ctx)
for i := range x.arcs {
if err := validate(ctx, x.at(ctx, i)); err != nil {
return err
diff --git a/cue/value.go b/cue/value.go
index 3be1282..b458284 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -563,7 +563,8 @@
comprehensions []*fieldComprehension
// TODO: consider hoisting the template arc to its own value.
- arcs []arc
+ arcs []arc
+ expanded *structLit
}
func newStruct(src source) *structLit {
@@ -580,7 +581,7 @@
// lookup returns the node for the given label f, if present, or nil otherwise.
func (x *structLit) lookup(ctx *context, f label) (v evaluated, raw value) {
- x.expand(ctx)
+ x = x.expandFields(ctx)
// Lookup is done by selector or index references. Either this is done on
// literal nodes or nodes obtained from references. In the later case,
// noderef will have ensured that the ancestors were evaluated.
@@ -593,7 +594,7 @@
}
func (x *structLit) iterAt(ctx *context, i int) (evaluated, value, label) {
- x.expand(ctx)
+ x = x.expandFields(ctx)
if i >= len(x.arcs) {
return nil, nil, 0
}
@@ -602,7 +603,7 @@
}
func (x *structLit) at(ctx *context, i int) evaluated {
- x.expand(ctx)
+ x = x.expandFields(ctx)
// if x.emit != nil && isBottom(x.emit) {
// return x.emit.(evaluated)
// }
@@ -642,15 +643,22 @@
return x.arcs[i].cache
}
-func (x *structLit) expand(ctx *context) {
+func (x *structLit) expandFields(ctx *context) *structLit {
+ if x.expanded != nil {
+ return x.expanded
+ }
if x.comprehensions == nil {
- return
+ x.expanded = x
+ return x
}
- comprehensions := x.comprehensions
- x.comprehensions = nil
+ x.expanded = x
+ comprehensions := x.comprehensions
+ emit := x.emit
+ template := x.template
newArcs := []arc{}
+
for _, c := range comprehensions {
result := c.clauses.yield(ctx, func(k, v evaluated) *bottom {
if !k.kind().isAnyOf(stringKind) {
@@ -658,10 +666,10 @@
}
if c.isTemplate {
// TODO: disallow altogether or only when it refers to fields.
- if x.template == nil {
- x.template = v
+ if template == nil {
+ template = v
} else {
- x.template = mkBin(ctx, c.Pos(), opUnify, x.template, v)
+ template = mkBin(ctx, c.Pos(), opUnify, template, v)
}
return nil
}
@@ -679,7 +687,7 @@
switch {
case result == nil:
case isBottom(result):
- x.emit = result
+ emit = result
default:
panic("should not happen")
}
@@ -688,6 +696,11 @@
// new arcs may be merged with old ones, but only if the old ones were not
// referred to in the evaluation of any of the arcs.
// TODO(perf): improve big O
+ arcs := make([]arc, 0, len(x.arcs)+len(newArcs))
+ arcs = append(arcs, x.arcs...)
+ x = &structLit{x.baseValue, emit, template, nil, arcs, nil}
+ x.expanded = x
+
outer:
for _, na := range newArcs {
f := na.feature
@@ -705,6 +718,7 @@
x.arcs = append(x.arcs, arc{feature: f, v: na.v})
}
sort.Stable(x)
+ return x
}
func (x *structLit) applyTemplate(ctx *context, i int, v evaluated) evaluated {
@@ -1130,7 +1144,7 @@
switch src := source.(type) {
case *structLit:
- src.expand(ctx)
+ src = src.expandFields(ctx)
for i, a := range src.arcs {
key := &stringLit{
x.baseValue,
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index 2d30c51..7e0691c 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -2073,6 +2073,23 @@
```
<!--
+Consider banning any construct that makes CUE not having a linear
+running time expressed in the number of nodes in the output.
+
+This would require restricting constructs like:
+
+(fib&{n:2}).out
+
+fib: {
+ n: int
+
+ out: (fib&{n:n-2}).out + (fib&{n:n-1}).out if n >= 2
+ out: fib({n:n-2}).out + fib({n:n-1}).out if n >= 2
+ out: n if n < 2
+}
+
+-->
+<!--
### Unused fields
TODO: rules for detection of unused fields
@@ -2081,7 +2098,6 @@
-->
-
## Modules, instances, and packages
CUE configurations are constructed combining _instances_.