cue: Reference handles concrete values for index operations

Also fixes
  - some caching and binding issues
    related to comprehensions.

- issues with comprehensions introduced with relaxed
closed struct semantics

Change-Id: I55277a6ee9c24b9c9641166bb79942762d44d06c
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4020
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast.go b/cue/ast.go
index 178d3be..4b9b193 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -328,7 +328,7 @@
 			v.errf(n, "comprehension must be struct")
 		}
 		yielder.value = v.walk(n.Value)
-		v.object.comprehensions = append(v.object.comprehensions, sc)
+		v.object.comprehensions = append(v.object.comprehensions, compValue{comp: sc})
 
 	case *ast.Field:
 		opt := n.Optional != token.NoPos
@@ -371,7 +371,7 @@
 				doc:       leftOverDoc,
 				attrs:     attrs,
 			}
-			v.object.comprehensions = append(v.object.comprehensions, fc)
+			v.object.comprehensions = append(v.object.comprehensions, compValue{comp: fc})
 
 		case *ast.ListLit:
 			if len(x.Elts) != 1 {
diff --git a/cue/binop.go b/cue/binop.go
index 9c7c1fe..b880159 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -646,12 +646,18 @@
 	// we need to apply the template to all elements.
 
 	sz := len(x.comprehensions) + len(y.comprehensions)
-	obj.comprehensions = make([]value, sz)
+	obj.comprehensions = make([]compValue, sz)
 	for i, c := range x.comprehensions {
-		obj.comprehensions[i] = ctx.copy(c)
+		obj.comprehensions[i] = compValue{
+			checked: c.checked || (!unchecked && y.isClosed()),
+			comp:    ctx.copy(c.comp),
+		}
 	}
 	for i, c := range y.comprehensions {
-		obj.comprehensions[i+len(x.comprehensions)] = ctx.copy(c)
+		obj.comprehensions[i+len(x.comprehensions)] = compValue{
+			checked: c.checked || (!unchecked && x.isClosed()),
+			comp:    ctx.copy(c.comp),
+		}
 	}
 
 	for _, a := range x.arcs {
@@ -666,6 +672,8 @@
 			if a.optional {
 				continue
 			}
+			// TODO: pass position of key, not value. Currently does not have
+			// a position.
 			return ctx.mkErr(a.v, a.v, "field %q not allowed in closed struct",
 				ctx.labelStr(a.feature))
 		}
@@ -709,6 +717,8 @@
 			if a.optional {
 				continue
 			}
+			// TODO: pass position of key, not value. Currently does not have a
+			// position.
 			return ctx.mkErr(a.v, x, "field %q not allowed in closed struct",
 				ctx.labelStr(a.feature))
 		}
diff --git a/cue/copy.go b/cue/copy.go
index fff4342..4698588 100644
--- a/cue/copy.go
+++ b/cue/copy.go
@@ -53,9 +53,9 @@
 			arcs[i] = a
 		}
 
-		comp := make([]value, len(x.comprehensions))
+		comp := make([]compValue, len(x.comprehensions))
 		for i, c := range x.comprehensions {
-			comp[i] = ctx.copy(c)
+			comp[i] = compValue{c.checked, ctx.copy(c.comp)}
 		}
 		obj.comprehensions = comp
 		return obj, false
diff --git a/cue/debug.go b/cue/debug.go
index e855f7f..17d8f2f 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -340,7 +340,10 @@
 		}
 		p.str(x.arcs)
 		for i, c := range x.comprehensions {
-			p.str(c)
+			if c.checked {
+				p.write("c:")
+			}
+			p.str(c.comp)
 			if i < len(x.comprehensions)-1 {
 				p.write(", ")
 			}
diff --git a/cue/eval.go b/cue/eval.go
index 01051f1..f90aa64 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -18,6 +18,22 @@
 	"bytes"
 )
 
+type resolver interface {
+	reference(ctx *context) value
+}
+
+var _ resolver = &selectorExpr{}
+var _ resolver = &indexExpr{}
+
+func resolveReference(ctx *context, v value) evaluated {
+	if r, ok := v.(resolver); ok {
+		if st, ok := r.reference(ctx).(*structLit); ok {
+			return st
+		}
+	}
+	return v.evalPartial(ctx)
+}
+
 func eval(idx *index, v value) evaluated {
 	ctx := idx.newContext()
 	return v.evalPartial(ctx)
@@ -74,8 +90,42 @@
 			// TODO: mention x.x in error message?
 			return ctx.mkErr(x, "undefined field %q", field)
 		}
-		// TODO: do we need to evaluate here?
-		return n.cache.evalPartial(ctx)
+		return n.cache
+	}
+	return e.err(&selectorExpr{x.baseValue, v, x.feature})
+}
+
+func (x *selectorExpr) reference(ctx *context) (result value) {
+	if ctx.trace {
+		defer uni(indent(ctx, "selectorExpr", x))
+		defer func() { ctx.debugPrint("result:", result) }()
+	}
+
+	e := newEval(ctx, true)
+
+	const msgType = "invalid operation: %[5]s (type %[3]s does not support selection)"
+	v := e.eval(x.x, structKind|lambdaKind, msgType, x)
+
+	if e.is(v, structKind|lambdaKind, "") {
+		sc, ok := v.(scope)
+		if !ok {
+			return ctx.mkErr(x, "invalid subject to selector (found %v)", v.kind())
+		}
+		n := sc.lookup(ctx, x.feature)
+		if n.optional {
+			field := ctx.labelStr(x.feature)
+			return ctx.mkErr(x, codeIncomplete, "field %q is optional", field)
+		}
+		if n.val() == nil {
+			field := ctx.labelStr(x.feature)
+			if st, ok := sc.(*structLit); ok && !st.isClosed() {
+				return ctx.mkErr(x, codeIncomplete, "undefined field %q", field)
+			}
+			//	m.foo undefined (type map[string]bool has no field or method foo)
+			// TODO: mention x.x in error message?
+			return ctx.mkErr(x, "undefined field %q", field)
+		}
+		return n.v
 	}
 	return e.err(&selectorExpr{x.baseValue, v, x.feature})
 }
@@ -129,6 +179,65 @@
 	return e.err(&indexExpr{x.baseValue, val, index})
 }
 
+func (x *indexExpr) reference(ctx *context) (result value) {
+	if ctx.trace {
+		defer uni(indent(ctx, "indexExpr", x))
+		defer func() { ctx.debugPrint("result:", result) }()
+	}
+
+	e := newEval(ctx, true)
+
+	const msgType = "invalid operation: %[5]s (type %[3]s does not support indexing)"
+	const msgIndexType = "invalid %[5]s index %[1]s (type %[3]s)"
+
+	val := e.eval(x.x, listKind|structKind, msgType, x)
+	k := val.kind()
+	index := e.eval(x.index, stringKind|intKind, msgIndexType, k)
+
+	switch v := val.(type) {
+	case *structLit:
+		if e.is(index, stringKind, msgIndexType, k) {
+			s := index.strValue()
+			// TODO: must lookup
+			n := v.lookup(ctx, ctx.strLabel(s))
+			if n.definition {
+				return ctx.mkErr(x, index,
+					"field %q is a definition", s)
+			}
+			if n.optional {
+				return ctx.mkErr(x, index, codeIncomplete, "field %q is optional", s)
+			}
+			if n.val() == nil {
+				if !v.isClosed() {
+					return ctx.mkErr(x, index, codeIncomplete, "undefined field %q", s)
+				}
+				return ctx.mkErr(x, index, "undefined field %q", s)
+			}
+			return n.v
+		}
+	case *list:
+		if e.is(index, intKind, msgIndexType, k) {
+			i := index.(*numLit).intValue(ctx)
+			if i < 0 {
+				const msg = "invalid %[4]s index %[1]s (index must be non-negative)"
+				return e.mkErr(x.index, index, 0, k, msg)
+			}
+			return v.iterAt(ctx, i).v
+		}
+
+	case atter:
+		if e.is(index, intKind, msgIndexType, k) {
+			i := index.(*numLit).intValue(ctx)
+			if i < 0 {
+				const msg = "invalid %[4]s index %[1]s (index must be non-negative)"
+				return e.mkErr(x.index, index, 0, k, msg)
+			}
+			return v.at(ctx, i)
+		}
+	}
+	return e.err(&indexExpr{x.baseValue, val, index})
+}
+
 // Composit
 
 func (x *sliceExpr) evalPartial(ctx *context) (result evaluated) {
@@ -306,7 +415,7 @@
 
 func (x *fieldComprehension) evalPartial(ctx *context) evaluated {
 	k := x.key.evalPartial(ctx)
-	v := x.val.evalPartial(ctx)
+	v := x.val
 	if err := firstBottom(k, v); err != nil {
 		return err
 	}
@@ -321,7 +430,7 @@
 
 func (x *closeIfStruct) evalPartial(ctx *context) evaluated {
 	v := x.value.evalPartial(ctx)
-	updateCloseStatus(ctx, v)
+	v = updateCloseStatus(ctx, v)
 	return v
 }
 
@@ -450,8 +559,8 @@
 			}
 		}
 	} else {
-		left = x.left.evalPartial(ctx)
-		right = x.right.evalPartial(ctx)
+		left = resolveReference(ctx, x.left)
+		right = resolveReference(ctx, x.right)
 
 		if err := cycleError(left); err != nil && ctx.inSum == 0 && right.kind().isAtom() {
 			return ctx.delayConstraint(right, x)
diff --git a/cue/export.go b/cue/export.go
index c3d2c84..2e2ca40 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -809,7 +809,7 @@
 	}
 
 	for _, v := range x.comprehensions {
-		switch c := v.(type) {
+		switch c := v.comp.(type) {
 		case *fieldComprehension:
 			l := p.expr(c.key)
 			label, _ := l.(ast.Label)
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 2a1ece3..da385ee 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -1484,12 +1484,12 @@
 		}
 		`,
 		out: `<0>{` +
-			`E :: _|_(int:field "f3" not allowed in closed struct), ` +
-			`A :: <1>C{f1: int, f2: int}, ` +
-			`a: _|_(int:field "f3" not allowed in closed struct), ` +
-			`B :: <2>C{f1: int}, ` +
-			`C :: <3>C{f1: int}, ` +
-			`D :: <4>{f1: int, ...}` +
+			`E :: _|_(<1>.v:field "f3" not allowed in closed struct), ` +
+			`A :: <2>C{f1: int, f2: int}, ` +
+			`a: _|_(<3>.v:field "f3" not allowed in closed struct), ` +
+			`B :: <4>C{f1: int}, ` +
+			`C :: <5>C{f1: int}, ` +
+			`D :: <6>{f1: int, ...}` +
 			`}`,
 	}, {
 		desc: "incomplete comprehensions",
@@ -2603,7 +2603,7 @@
 			 }]
 		}
 		`,
-		out: `<0>{<1>{listOfCloseds: [_|_(2:field "b" not allowed in closed struct)]}, Foo: <2>{listOfCloseds: []}, Closed :: <3>C{a: 0}, Junk: <4>{b: 2}}`,
+		out: `<0>{<1>{listOfCloseds: [_|_(<2>.v:field "b" not allowed in closed struct)]}, Foo: <3>{listOfCloseds: []}, Closed :: <4>C{a: 0}, Junk: <5>{b: 2}}`,
 	}, {
 		desc: "label and field aliases",
 		in: `
@@ -2750,6 +2750,37 @@
 		foo: Task & {"op": "pull"}
 		`,
 		out: `<0>{Task :: (<1>C{op: "pull", tag: (*"latest" | string), refToTag: <1>.tag, tagExpr: (<1>.tag + "dd"), tagInString: ""+<1>.tag+""} | <2>C{op: "scratch"}), foo: <3>C{op: "pull", tag: "latest", refToTag: "latest", tagExpr: "latestdd", tagInString: "latest"}}`,
+	}, {
+		in: `
+		t: {
+			ok :: *true | bool
+			if ok {
+				x: int
+			}
+		}
+		s: t & {
+			ok :: false
+		}`,
+		out: `<0>{t: <1>{ok :: true, x: int}, s: <2>{ok :: false}}`,
+	}, {
+		// TODO(eval): fix: c should ultimately be allowed the struct. Current
+		// semantics require, however, that generated fields are not available
+		// for evaluation. This, however, does not have to hold, for closedness
+		// checks and allowing this would be more intuitive.
+		// Until that time, ensure that the behavior is at commutative.
+		in: `
+		a :: {
+			if b {
+				c: 4
+			}
+			b: bool
+		}
+		x: (a & { b: true}) & {c: 4 }
+		y: x
+		`,
+		// c should not be allowed, as it would break commutativiy.
+		// See comments above.
+		out: `<0>{a :: <1>C{b: bool if <2>.b yield <3>C{c: 4}}, x: _|_(4:field "c" not allowed in closed struct), y: _|_(4:field "c" not allowed in closed struct)}`,
 	}}
 	rewriteHelper(t, testCases, evalFull)
 }
diff --git a/cue/types.go b/cue/types.go
index bb2a2ee..ad16023 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -1349,27 +1349,56 @@
 
 // Reference returns the instance and path referred to by this value such that
 // inst.Lookup(path) resolves to the same value, or no path if this value is not
-// a reference,
+// a reference. If a reference contains index selection (foo[bar]), it will
+// only return a reference if the index resolves to a concrete value.
 func (v Value) Reference() (inst *Instance, path []string) {
 	// TODO: don't include references to hidden fields.
 	if v.path == nil {
 		return nil, nil
 	}
-	sel, ok := v.path.v.(*selectorExpr)
-	if !ok {
+	ctx := v.ctx()
+	var x value
+	var feature string
+	switch sel := v.path.v.(type) {
+	case *selectorExpr:
+		x = sel.x
+		feature = ctx.labelStr(sel.feature)
+
+	case *indexExpr:
+		e := sel.index.evalPartial(ctx)
+		s, ok := e.(*stringLit)
+		if !ok {
+			return nil, nil
+		}
+		x = sel.x
+		feature = s.str
+
+	default:
 		return nil, nil
 	}
-	imp, a := mkPath(v.ctx(), v.path, sel, 0)
+	imp, a := mkPath(ctx, v.path, x, feature, 0)
 	return imp, a
 }
 
-func mkPath(c *context, up *valueData, sel *selectorExpr, d int) (imp *Instance, a []string) {
-	switch x := sel.x.(type) {
+func mkPath(c *context, up *valueData, x value, feature string, d int) (imp *Instance, a []string) {
+	switch x := x.(type) {
 	case *selectorExpr:
-		imp, a = mkPath(c, up.parent, x, d+1)
+		imp, a = mkPath(c, up, x.x, c.labelStr(x.feature), d+1)
 		if imp == nil {
 			return nil, nil
 		}
+
+	case *indexExpr:
+		e := x.index.evalPartial(c)
+		s, ok := e.(*stringLit)
+		if !ok {
+			return nil, nil
+		}
+		imp, a = mkPath(c, up, x.x, s.str, d+1)
+		if imp == nil {
+			return nil, nil
+		}
+
 	case *nodeRef:
 		// the parent must exist.
 		for ; up != nil && up.cache != x.node.(value); up = up.parent {
@@ -1379,11 +1408,11 @@
 		if v == nil {
 			v = x.node
 		}
-		imp = c.getImportFromNode(x.node)
+		imp = c.getImportFromNode(v)
 	default:
 		return nil, nil
 	}
-	return imp, append(a, c.labelStr(sel.feature))
+	return imp, append(a, feature)
 }
 
 func mkFromRoot(c *context, up *valueData, d int) (root value, a []string) {
diff --git a/cue/types_test.go b/cue/types_test.go
index 31eb4a6..b088a7b 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1998,21 +1998,41 @@
 		input string
 		want  string
 	}{{
-		input: "v: _|_",
+		input: "v: w: x: _|_",
 		want:  "",
 	}, {
-		input: "v: 2",
+		input: "v: w: x: 2",
 		want:  "",
 	}, {
-		input: "v: a, a: 1",
+		input: "v: w: x: a, a: 1",
 		want:  "a",
 	}, {
-		input: "v: a.b.c, a b c: 1",
+		input: "v: w: x: a.b.c, a: b: c: 1",
 		want:  "a b c",
+	}, {
+		input: "v: w: x: w.a.b.c, v: w: a: b: c: 1",
+		want:  "v w a b c",
+	}, {
+		input: `v: w: x: w.a.b.c, v: w: a: b: c: 1, D :: 3, opt?: 3, "v\(D)": 3, X: {a: 3}, X`,
+		want:  "v w a b c",
+	}, {
+		input: `v: w: x: w.a[bb]["c"], v: w: a: b: c: 1, bb: "b"`,
+		want:  "v w a b c",
+	}, {
+		input: `v: {
+			for t in src {
+				w: "t\(t)": 1
+				w: "\(t)": w["t\(t)"]
+			}
+		},
+		src: ["x", "y"]`,
+		want: "v w tx",
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
-			v := getInstance(t, tc.input).Lookup("v")
+			var r Runtime
+			inst, _ := r.Compile("in", tc.input) // getInstance(t, tc.input)
+			v := inst.Lookup("v", "w", "x")
 			inst, a := v.Reference()
 			if got := strings.Join(a, " "); got != tc.want {
 				t.Errorf("\n got %v;\nwant %v", got, tc.want)
diff --git a/cue/value.go b/cue/value.go
index 7ad98e7..858fa44 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -642,13 +642,26 @@
 	optionals   *optionals
 	closeStatus closeMode
 
-	comprehensions []value
+	comprehensions []compValue
 
 	// TODO: consider hoisting the template arc to its own value.
 	arcs     []arc
 	expanded evaluated
 }
 
+// compValue is a temporary stop-gap until the correct unification algorithm is
+// implemented. This implementation is more strict than should be. When two
+// structs, of which at least one is closed, are unified, the fields resolving
+// later from unresolved comprehensions should match the closedness constraints.
+// To relax this constraint, unification could follow the lines of
+// traditional unification with bookkeeping of which fields are
+// allowed, to be applied as constraints after full unification.
+
+type compValue struct {
+	checked bool
+	comp    value
+}
+
 // optionals holds a set of key pattern-constraint pairs, where constraints are
 // to be applied to concrete fields of which the label matches the key pattern.
 //
@@ -1045,7 +1058,13 @@
 		// it is safe to cache the result.
 		ctx.cycleErr = false
 
-		updateCloseStatus(ctx, v)
+		v = updateCloseStatus(ctx, v)
+		if st, ok := v.(*structLit); ok {
+			v, err = st.expandFields(ctx)
+			if err != nil {
+				v = err
+			}
+		}
 		x.arcs[i].cache = v
 		if doc != nil {
 			x.arcs[i].docs = &docNode{left: doc, right: x.arcs[i].docs}
@@ -1083,15 +1102,17 @@
 
 	comprehensions := x.comprehensions
 
-	var incomplete []value
+	var incomplete []compValue
 
 	var n evaluated = &top{x.baseValue}
 	if x.emit != nil {
 		n = x.emit.evalPartial(ctx)
 	}
 
+	var checked evaluated = &top{x.baseValue}
+
 	for _, x := range comprehensions {
-		v := x.evalPartial(ctx)
+		v := x.comp.evalPartial(ctx)
 		if v, ok := v.(*bottom); ok {
 			if isIncomplete(v) {
 				incomplete = append(incomplete, x)
@@ -1100,27 +1121,41 @@
 
 			return nil, v
 		}
-		src := binSrc(x.Pos(), opUnify, x, v)
-		n = binOp(ctx, src, opUnifyUnchecked, n, v)
+		src := binSrc(x.comp.Pos(), opUnify, x.comp, v)
+		_ = checked
+		if x.checked {
+			checked = binOp(ctx, src, opUnifyUnchecked, checked, v)
+		} else {
+			n = binOp(ctx, src, opUnifyUnchecked, n, v)
+		}
+	}
+	if len(comprehensions) == len(incomplete) {
+		return x, nil
 	}
 
-	src := binSrc(x.Pos(), opUnify, x, n)
 	switch n.(type) {
-	case *bottom:
+	case *bottom, *top:
 	case *structLit:
-		orig := *x
-		orig.comprehensions = incomplete
-		orig.emit = nil
-		n = binOp(ctx, src, opUnify, &orig, n)
+		orig := x.comprehensions
+		x.comprehensions = incomplete
+		src := binSrc(x.Pos(), opUnify, x, n)
+		n = binOp(ctx, src, opUnifyUnchecked, x, n)
+		x.comprehensions = orig
 
 	default:
-		if len(comprehensions) == len(incomplete) {
-			return x, nil
-		}
-		if x.emit != nil {
-			v := x.emit.evalPartial(ctx)
-			n = binOp(ctx, src, opUnifyUnchecked, v, n)
-		}
+		return nil, ctx.mkErr(x, n, "invalid embedding")
+	}
+
+	switch checked.(type) {
+	case *bottom, *top:
+	case *structLit:
+		orig := x.comprehensions
+		x.comprehensions = incomplete
+		src := binSrc(x.Pos(), opUnify, n, checked)
+		n = binOp(ctx, src, opUnify, x, checked)
+		x.comprehensions = orig
+
+	default:
 		return nil, ctx.mkErr(x, n, "invalid embedding")
 	}
 
@@ -1152,7 +1187,7 @@
 	}
 
 	if x.closeStatus != 0 {
-		updateCloseStatus(ctx, v)
+		v = updateCloseStatus(ctx, v)
 	}
 	return v, doc
 }
@@ -1240,8 +1275,12 @@
 		switch x := v.(type) {
 		case *top:
 			return v
-		case *structLit, *list, *disjunction:
-			updateCloseStatus(ctx, v)
+		case *structLit:
+			v = updateCloseStatus(ctx, x)
+		case *list:
+			v = updateCloseStatus(ctx, x)
+		case *disjunction:
+			v = updateCloseStatus(ctx, x)
 		case *closeIfStruct:
 			return x
 		}
@@ -1250,18 +1289,15 @@
 	return v
 }
 
-func updateCloseStatus(ctx *context, v value) {
+func updateCloseStatus(ctx *context, v evaluated) evaluated {
 	switch x := v.(type) {
 	case *structLit:
-		y, err := x.expandFields(ctx)
-		if err == nil {
-			if x.closeStatus.shouldClose() {
-				x.closeStatus = isClosed
-				y.optionals = y.optionals.close()
-			}
-			x.closeStatus |= shouldFinalize
-			y.closeStatus = x.closeStatus
+		if x.closeStatus.shouldClose() {
+			x.closeStatus = isClosed
+			x.optionals = x.optionals.close()
 		}
+		x.closeStatus |= shouldFinalize
+		return x
 
 	case *disjunction:
 		for _, d := range x.values {
@@ -1274,6 +1310,7 @@
 			wrapFinalize(ctx, x.typ)
 		}
 	}
+	return v
 }
 
 // insertValue is used during initialization but never during evaluation.