cue: fix regression in merge
This reimplements the template stripper.
It is considerably more code, but easier to understand,
more performant, but more importantly, easier to debug.
Fixes #38.
Change-Id: Id4e6988941b43e47f02cbbd3c2fdfdc10ac2ffa7
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1880
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/instance.go b/cue/instance.go
index b2c17f2..e3b0fbf 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -17,7 +17,6 @@
import (
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
- "cuelang.org/go/cue/token"
"cuelang.org/go/internal"
"golang.org/x/exp/errors/fmt"
)
@@ -146,21 +145,17 @@
return inst[0]
}
+ values := []value{}
for _, i := range inst {
if i.Err != nil {
return i
}
+ values = append(values, i.rootValue)
}
+ merged := &mergedValues{values: values}
ctx := inst[0].newContext()
- merged := stripTemplates(ctx, inst[0].rootValue)
-
- for _, i := range inst[1:] {
- x := stripTemplates(ctx, i.rootValue)
- merged = mkBin(ctx, token.NoPos, opUnify, merged, x)
- }
-
st, ok := ctx.manifest(merged).(*structLit)
if !ok {
return nil
diff --git a/cue/instance_test.go b/cue/instance_test.go
index fb99905..da8569c 100644
--- a/cue/instance_test.go
+++ b/cue/instance_test.go
@@ -82,6 +82,20 @@
),
out: `{obj:{alpha:{a:A,b:2},beta:{a:B,b:3}}}`,
}, {
+ desc: "top-level comprehensions",
+ instances: insts(`
+ t: {"\(k)": 10 for k, x in s}
+ s <Name>: {}
+ s foo a: 1
+ `,
+ `
+ t: {"\(k)": 10 for k, x in s}
+ s <Name>: {}
+ s bar b: 2
+ `,
+ ),
+ out: `{t:{foo:10,bar:10},s:{foo:{a:1},bar:{b:2}}}`,
+ }, {
desc: "error",
instances: insts(`a:`),
out: `{}`,
diff --git a/cue/strip.go b/cue/strip.go
index 9e8ac78..22defda 100644
--- a/cue/strip.go
+++ b/cue/strip.go
@@ -14,32 +14,112 @@
package cue
-// This file defines a rewriter that strips a fully evaluated value of its
-// template.
+import (
+ "sort"
+)
-// TODO: currently strip templates does a full evaluation as it is hard to keep
-// nodeRef and copied structs in sync. This is far from efficient, but it is the
-// easiest to get correct.
-
-// stripTemplates evaluates v and strips the result of templates.
-func stripTemplates(ctx *context, v value) value {
- return rewrite(ctx, v, stripRewriter)
+// A mergedValues type merges structs without unifying their templates.
+// It evaluates structs in parallel and then creates a new mergedValues
+// for each duplicate arc. The mergedValues do not reappear once there is
+// only a single value per arc.
+//
+// This is used to merge different instances which may have incompatible
+// specializations, but have disjuncts objects that may otherwise be shared
+// in the same namespace.
+type mergedValues struct {
+ baseValue
+ values []value
}
-func stripRewriter(ctx *context, v value) (value, bool) {
- eval := ctx.manifest(v)
- switch x := eval.(type) {
- case *structLit:
- x = x.expandFields(ctx)
- if x.template != nil {
- arcs := make(arcs, len(x.arcs))
- for i, a := range x.arcs {
- a.setValue(rewrite(ctx, x.at(ctx, i), stripRewriter))
- arcs[i] = a
+func (x *mergedValues) evalPartial(ctx *context) evaluated {
+ var structs []*structLit
+ for _, v := range x.values {
+ v = v.evalPartial(ctx)
+ o, ok := v.(*structLit)
+ if !ok {
+ v := x.values[0]
+ for _, w := range x.values[1:] {
+ v = mkBin(ctx, w.Pos(), opUnify, v, w)
}
- // TODO: verify that len(x.comprehensions) == 0
- return &structLit{x.baseValue, x.emit, nil, nil, arcs, nil}, false
+ return v.evalPartial(ctx)
+ }
+ o = o.expandFields(ctx)
+ structs = append(structs, o)
+ }
+
+ // Pre-expand the arcs so that we can discard the templates.
+ obj := &structLit{
+ baseValue: structs[0].baseValue,
+ }
+ var arcs arcs
+ for _, v := range structs {
+ for i := 0; i < len(v.arcs); i++ {
+ w := v.iterAt(ctx, i)
+ arcs = append(arcs, w)
}
}
- return eval, true
+ obj.arcs = arcs
+ sort.Stable(obj)
+
+ values := []value{}
+ for _, v := range structs {
+ if v.emit != nil {
+ values = append(values, v.emit)
+ }
+ }
+ switch len(values) {
+ case 0:
+ case 1:
+ obj.emit = values[0]
+ default:
+ obj.emit = &mergedValues{values[0].base(), values}
+ }
+
+ // merge arcs
+ k := 0
+ for i := 0; i < len(arcs); k++ {
+ a := arcs[i]
+ // TODO: consider storing the evaluated value. This is a performance
+ // versus having more information tradeoff. It results in the same
+ // value.
+ values := []value{a.v}
+ for i++; i < len(arcs) && a.feature == arcs[i].feature; i++ {
+ values = append(values, arcs[i].v)
+ a.optional = a.optional && arcs[i].optional
+ var err evaluated
+ a.attrs, err = unifyAttrs(ctx, a.v, a.attrs, arcs[i].attrs)
+ if err != nil {
+ return err
+ }
+ }
+ if len(values) == 1 {
+ arcs[k] = a
+ continue
+ }
+ a.cache = nil
+ a.v = &mergedValues{a.v.base(), values}
+ arcs[k] = a
+ }
+ obj.arcs = arcs[:k]
+ return obj
+}
+
+func (x *mergedValues) kind() kind {
+ k := x.values[0].kind()
+ for _, v := range x.values {
+ k = unifyType(k, v.kind())
+ }
+ return k
+}
+
+func (x *mergedValues) rewrite(ctx *context, fn rewriteFunc) value {
+ vs := make([]value, len(x.values))
+ for i, v := range x.values {
+ vs[i] = rewrite(ctx, v, fn)
+ }
+ return &mergedValues{x.baseValue, vs}
+}
+
+func (x *mergedValues) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
+ return false
}
diff --git a/cue/strip_test.go b/cue/strip_test.go
deleted file mode 100644
index 897e213..0000000
--- a/cue/strip_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2018 The CUE Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package cue
-
-import "testing"
-
-func TestStripTemplates(t *testing.T) {
- testCases := []testCase{{
- desc: "basic",
- in: `
- foo <Name>: { name: Name }
- foo bar: { units: 5 }
- `,
- out: `<0>{foo: <1>{bar: <2>{name: "bar", units: 5}}}`,
- }, {
- desc: "top-level template",
- in: `
- <Name>: { name: Name }
- bar: { units: 5 }
- `,
- out: `<0>{bar: <1>{name: "bar", units: 5}}`,
- }, {
- desc: "with reference",
- in: `
- before: foo.bar
- foo <Name>: { name: Name }
- foo bar: { units: 5 }
- after: foo.bar
- `,
- out: `<0>{before: <1>{name: "bar", units: 5}, foo: <2>{bar: <3>{name: "bar", units: 5}}, after: <4>{name: "bar", units: 5}}`,
- }, {
- desc: "nested",
- in: `
- <X1> foo <X2> bar <X3>: { name: X1+X2+X3 }
- a foo b bar c: { morning: true }
- `,
- out: `<0>{a: <1>{foo: <2>{b: <3>{bar: <4>{c: <5>{name: "abc", morning: true}}}}}}`}}
- for _, tc := range testCases {
- t.Run("", func(t *testing.T) {
- ctx, obj := compileFile(t, tc.in)
-
- v := stripTemplates(ctx, obj)
-
- if got := debugStr(ctx, v); got != tc.out {
- t.Errorf("output differs:\ngot %s\nwant %s", got, tc.out)
- }
- })
- }
-}