cue: add Dereference and fix value paths
Ensure Value's path reflects the actual values when
using the various functions.
Also introduces Dereference to follow a reference
to a newly rooted Value.
Change-Id: Ie555322a81a82d662bf7ef4a9b6ed072c62b0ade
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5404
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt b/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt
index 101c0d9..71d9d02 100644
--- a/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_baddisplay.txt
@@ -3,7 +3,7 @@
cmp stderr cmd_baddisplay.out
-- cmd_baddisplay.out --
-text: conflicting values 42 and string (mismatched types int and string):
+command.baddisplay.display.text: conflicting values 42 and string (mismatched types int and string):
./task_tool.cue:6:9
tool/cli:4:9
-- task.cue --
diff --git a/cue/op.go b/cue/op.go
index f465692..a1d7812 100644
--- a/cue/op.go
+++ b/cue/op.go
@@ -64,7 +64,10 @@
)
var opToOp = map[op]Op{
- opUnify: AndOp,
+ opUnify: AndOp,
+ // TODO(eval): opUnifyUnchecked is not the same as opUnify and should have its own
+ // category, if needed. More likely opUnifyUnchecked, should be
+ // represented as a separate embedding method.
opUnifyUnchecked: AndOp,
opDisjunction: OrOp,
opLand: BooleanAndOp,
diff --git a/cue/types.go b/cue/types.go
index 7481905..9b71831 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -648,6 +648,82 @@
return Value{obj.ctx.index, &valueData{obj.path, uint32(i), a}}
}
+// Dereference reports to the value v refers to if v is a reference or v itself
+// otherwise.
+func Dereference(v Value) Value {
+ if v.path == nil {
+ return v
+ }
+
+ ctx := v.ctx()
+ a, n := appendPath(ctx, make([]label, 0, 3), v.path.v)
+
+ if n == nil {
+ return v
+
+ }
+
+ p := locateNode(v.path, n)
+
+ if p == nil {
+
+ imp := ctx.getImportFromNode(n.node)
+ if imp == nil {
+ // TODO(eval): embedded structs are currently represented at the
+ // same level as the enclosing struct. This means that the parent
+ // of an embedded struct skips the struct in which it is embedded.
+ // Treat embedded structs as "anonymous" fields.
+ // See TestPathCorrection.
+ return v
+ }
+ p = &valueData{arc: arc{v: imp.rootValue, cache: imp.rootStruct}}
+ }
+
+ cached := p.cache
+ if cached != nil {
+ cached = p.v.evalPartial(ctx)
+ }
+ s := cached.(*structLit)
+ for _, f := range a {
+ a := s.lookup(ctx, f)
+ p = &valueData{parent: p, arc: a} // index
+ s, _ = a.cache.(*structLit)
+ }
+
+ v = Value{v.idx, p}
+ return v
+}
+
+func appendPath(ctx *context, a []label, v value) (path []label, n *nodeRef) {
+ switch x := v.(type) {
+ case *selectorExpr:
+ a, n = appendPath(ctx, a, x.x)
+ if n == nil {
+ return nil, nil
+ }
+
+ a = append(a, x.feature)
+
+ case *indexExpr:
+ e := x.index.evalPartial(ctx)
+ s, ok := e.(*stringLit)
+ if !ok {
+ return nil, nil
+ }
+
+ a, n = appendPath(ctx, a, x.x)
+ if n == nil {
+ return nil, nil
+ }
+
+ a = append(a, ctx.label(s.str, false))
+
+ case *nodeRef:
+ n = x
+ }
+ return a, n
+}
+
func remakeValue(base Value, v value) Value {
p := base.path
if n, ok := v.(*nodeRef); ok {
@@ -1100,7 +1176,7 @@
// Elem returns the value of undefined element types of lists and structs.
func (v Value) Elem() (Value, bool) {
ctx := v.ctx()
- switch x := v.path.v.(type) {
+ switch x := v.path.cache.(type) {
case *structLit:
t, _ := x.optionals.constraint(ctx, nil)
if t == nil {
@@ -1491,11 +1567,11 @@
return nil
}
- return func(label string) (v Value) {
+ return func(label string) Value {
arg := &stringLit{x.baseValue, label, nil}
- if v, _ := x.optionals.constraint(ctx, arg); v != nil {
- return newValueRoot(ctx, v)
+ if val, _ := x.optionals.constraint(ctx, arg); val != nil {
+ return remakeValue(v, val)
}
return v
}
@@ -1563,7 +1639,7 @@
b := w.path.v
src := binSrc(token.NoPos, opUnify, a, b)
val := mkBin(ctx, src.Pos(), opUnify, a, b)
- u := newValueRoot(ctx, val)
+ u := remakeValue(v, val)
if err := u.Validate(); err != nil {
u = newValueRoot(ctx, ctx.mkErr(src, err))
}
@@ -1588,7 +1664,14 @@
fmt.Fprint(state, "<nil>")
return
}
- _, _ = io.WriteString(state, ctx.str(v.path.cache))
+ switch {
+ case state.Flag('#'):
+ _, _ = io.WriteString(state, ctx.str(v.path.v))
+ case state.Flag('+'):
+ _, _ = io.WriteString(state, debugStr(ctx, v.path.v))
+ default:
+ _, _ = io.WriteString(state, ctx.str(v.path.cache))
+ }
}
func (v Value) instance() *Instance {
@@ -1652,9 +1735,18 @@
case *nodeRef:
// the parent must exist.
- up = locateNode(up, x)
var v value
- v, a = mkFromRoot(c, up, d+2)
+ if p := locateNode(up, x); p != nil {
+ v, a = mkFromRoot(c, p, d+2)
+ } else {
+ // Either this references another parent, or it is an embedding.
+ imp = c.getImportFromNode(x.node)
+ if imp != nil {
+ break
+ }
+ // This must be an embedding, go one up.
+ v, a = mkFromRoot(c, up.parent, d+2)
+ }
if v == nil {
v = x.node
}
diff --git a/cue/types_test.go b/cue/types_test.go
index a4c7b0b..0160f4f 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -2267,25 +2267,194 @@
}
func TestPathCorrection(t *testing.T) {
- var r Runtime
- inst, err := r.Compile("in", `
- a: b: {
- c: d: b
- }
- `)
- if err != nil {
- t.Fatal(err)
- }
- _, a := inst.Lookup("a", "b", "c", "d").Expr()
- v := a[0].Lookup("b", "c", "d")
- gotInst, ref := v.Reference()
- if gotInst != inst {
- t.Error("reference not in original instance")
- }
- gotPath := strings.Join(ref, ".")
- wantPath := "a.b"
- if gotPath != wantPath {
- t.Errorf("got path %s; want %s", gotPath, wantPath)
+ testCases := []struct {
+ input string
+ lookup func(i *Instance) Value
+ want string
+ skip bool
+ }{{
+ input: `
+ a: b: {
+ c: d: b
+ }
+ `,
+ lookup: func(i *Instance) Value {
+ _, a := i.Lookup("a", "b", "c", "d").Expr()
+ return a[0].Lookup("b", "c", "d")
+ },
+ want: "a.b",
+ }, {
+ input: `
+ a: {
+ c: 3
+ {x: c}
+ }
+ `,
+ lookup: func(i *Instance) Value {
+ _, a := i.Lookup("a").Expr()
+ return a[1].Lookup("x")
+ },
+ want: "a.c",
+ }, {
+ input: `
+ a: b: [...T]
+ a: b: [...T]
+ T: 1
+ `,
+ lookup: func(i *Instance) Value {
+ v, _ := i.Lookup("a", "b").Elem()
+ _, a := v.Expr()
+ return a[0]
+ },
+ want: "T",
+ }, {
+ input: `
+ a :: {
+ T :: {b: 3}
+ close({}) | close({c: T}) | close({d: string})
+ }
+ `,
+ lookup: func(i *Instance) Value {
+ f, _ := i.LookupField("a")
+ _, a := f.Value.Expr() // &
+ _, a = a[1].Expr() // |
+ return a[1].Lookup("c")
+ },
+ want: "a.T",
+ }, {
+ input: `
+ package foo
+
+ Struct :: {
+ T :: int
+
+ {b?: T}
+ }`,
+ want: "Struct.T",
+ lookup: func(inst *Instance) Value {
+ // Locate Struct
+ i, _ := inst.Value().Fields(Definitions(true))
+ if !i.Next() {
+ t.Fatal("no fields")
+ }
+ // Locate b
+ i, _ = i.Value().Fields(Definitions(true), Optional(true))
+ if !(i.Next() && i.Next()) {
+ t.Fatal("no fields")
+ }
+ v := i.Value()
+ return v
+ },
+ }, {
+ input: `
+ package foo
+
+ A :: B :: T
+
+ T :: {
+ a: S.U
+ S :: U:: {}
+ }
+ `,
+ want: "T.S.U",
+ lookup: func(inst *Instance) Value {
+ f, _ := inst.Value().LookupField("A")
+ f, _ = f.Value.LookupField("B")
+ v := f.Value
+ v = Dereference(v)
+ v = v.Lookup("a")
+ return v
+ },
+ }, {
+ input: `
+ package foo
+
+ A :: B :: T
+
+ T :: {
+ a: [...S]
+ S :: {}
+ }
+ `,
+ want: "T.S",
+ lookup: func(inst *Instance) Value {
+ f, _ := inst.Value().LookupField("A")
+ f, _ = f.Value.LookupField("B")
+ v := f.Value
+ v = Dereference(v)
+ v, _ = v.Lookup("a").Elem()
+ return v
+ },
+ }, {
+ input: `
+ A :: {
+ b: T
+ }
+
+ T :: {
+ a: S
+ S :: {}
+ }
+ `,
+ want: "T.S",
+ lookup: func(inst *Instance) Value {
+ f, _ := inst.Value().LookupField("A")
+ v := f.Value.Lookup("b")
+ v = Dereference(v)
+ v = v.Lookup("a")
+ return v
+ },
+ }, {
+ // TODO(eval): embedded structs are currently represented at the same
+ // level as the enclosing struct. This means that the parent of an
+ // embedded struct skips the struct in which it is embedded. Treat
+ // embedded structs as "anonymous" fields.
+ // This could perhaps be made fixed with dereferencing as well.
+ skip: true,
+ input: `
+ Tracing :: {
+ T :: { address?: string }
+ S :: { ip?: string }
+
+ close({}) | close({
+ t: T
+ }) | close({
+ s: S
+ })
+ }
+ X :: {}
+ X // Disconnect top-level struct from the one visible by close.
+ `,
+ want: "",
+ lookup: func(inst *Instance) Value {
+ f, _ := inst.Value().LookupField("Tracing")
+ v := f.Value.Eval()
+ _, args := v.Expr()
+ v = args[1].Lookup("t")
+ v = Dereference(v)
+ return v
+ },
+ }}
+ for _, tc := range testCases {
+ if tc.skip {
+ continue
+ }
+ t.Run("", func(t *testing.T) {
+ var r Runtime
+ inst, err := r.Compile("in", tc.input)
+ if err != nil {
+ t.Fatal(err)
+ }
+ v := tc.lookup(inst)
+ gotInst, ref := v.Reference()
+ if gotInst != inst {
+ t.Error("reference not in original instance")
+ }
+ gotPath := strings.Join(ref, ".")
+ if gotPath != tc.want {
+ t.Errorf("got path %s; want %s", gotPath, tc.want)
+ }
+ })
}
}
diff --git a/internal/filetypes/filetypes_test.go b/internal/filetypes/filetypes_test.go
index 4a38607..29bc9fc 100644
--- a/internal/filetypes/filetypes_test.go
+++ b/internal/filetypes/filetypes_test.go
@@ -42,7 +42,7 @@
}{{
name: "must specify encoding",
in: build.File{},
- out: `encoding: non-concrete value string`,
+ out: `FileInfo.encoding: non-concrete value string`,
}, {
// Default without any
name: "cue",