cue: implementation of new comprehensions

- fieldComprehenion is really just a generated field
  name still need to be redone
- structComprehension now can have multiple fields

Change-Id: I81adfbdb79ffb1335e5be491422db20796ccab17
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3184
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast.go b/cue/ast.go
index c1dec8a..8f72cb5 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -314,71 +314,20 @@
 		return v.errf(n, "ellipsis (...) only allowed at end of list or struct")
 
 	case *ast.Comprehension:
-		st, ok := n.Value.(*ast.StructLit)
-		if !ok || len(st.Elts) != 1 {
-			return v.errf(n, "invalid comprehension: %v", n)
+		yielder := &yield{baseValue: newExpr(n.Value)}
+		sc := &structComprehension{
+			newNode(n),
+			wrapClauses(v, yielder, n.Clauses),
 		}
-		field := st.Elts[0].(*ast.Field)
-		yielder := &yield{
-			baseValue: newExpr(n.Value),
-			opt:       field.Optional != token.NoPos,
-			def:       field.Token == token.ISA,
-		}
-		fc := &fieldComprehension{
-			baseValue: newDecl(n),
-			clauses:   wrapClauses(v, yielder, n.Clauses),
-		}
-		switch x := field.Label.(type) {
-		case *ast.Interpolation:
-			v.sel = "?"
-			yielder.key = v.walk(x)
-			yielder.value = v.walk(field.Value)
-
-		case *ast.TemplateLabel:
-			v.sel = "*"
-			f := v.label(v.ident(x.Ident), true)
-
-			sig := &params{}
-			sig.add(f, &basicType{newNode(field.Label), stringKind})
-			template := &lambdaExpr{newExpr(field.Value), sig, nil}
-
-			v.setScope(field, template)
-			template.value = v.walk(field.Value)
-			yielder.value = template
-			fc.isTemplate = true
-
-		case *ast.BasicLit, *ast.Ident:
-			name, ok := internal.LabelName(x)
-			if !ok {
-				return v.errf(x, "invalid field name: %v", x)
-			}
-			// TODO: is this correct? Just for info, so not very important.
-			v.sel = name
-
-			// TODO: if the clauses do not contain a guard, we know that this
-			// field will always be added and we can move the comprehension one
-			// level down. This, in turn, has the advantage that it is more
-			// likely that the cross-reference limitation for field
-			// comprehensions is not violated. To ensure compatibility between
-			// implementations, though, we should relax the spec as well.
-			// The cross-reference rule is simple and this relaxation seems a
-			// bit more complex.
-
-			// TODO: for now we can also consider making this an error if
-			// the list of clauses does not contain if and make a suggestion
-			// to rewrite it.
-
-			if name != "" {
-				yielder.key = &stringLit{newNode(x), name, nil}
-				yielder.value = v.walk(field.Value)
-			}
-
+		// we don't support key for lists (yet?)
+		switch n.Value.(type) {
+		case *ast.StructLit:
 		default:
-			panic("cue: unknown label type")
+			// Caught by parser, usually.
+			v.errf(n, "comprehension must be struct")
 		}
-		// yielder.key = v.walk(n.Field.Label)
-		// yielder.value = v.walk(n.Field.Value)
-		v.object.comprehensions = append(v.object.comprehensions, fc)
+		yielder.value = v.walk(n.Value)
+		v.object.comprehensions = append(v.object.comprehensions, sc)
 
 	case *ast.Field:
 		opt := n.Optional != token.NoPos
@@ -391,18 +340,31 @@
 			ctx.inDefinition++
 			defer func() { ctx.inDefinition-- }()
 		}
+		attrs, err := createAttrs(v.ctx(), newNode(n), n.Attrs)
+		if err != nil {
+			return v.errf(n, err.format, err.args)
+		}
+		var leftOverDoc *docNode
+		for _, c := range n.Comments() {
+			if c.Position == 0 {
+				leftOverDoc = v.doc
+				v.doc = &docNode{n: n}
+				break
+			}
+		}
 		switch x := n.Label.(type) {
 		case *ast.Interpolation:
 			v.sel = "?"
-			yielder := &yield{baseValue: newNode(x)}
+			// Must be struct comprehension.
 			fc := &fieldComprehension{
 				baseValue: newDecl(n),
-				clauses:   yielder,
+				key:       v.walk(x),
+				val:       v.walk(n.Value),
+				opt:       opt,
+				def:       isDef,
+				doc:       leftOverDoc,
+				attrs:     attrs,
 			}
-			yielder.key = v.walk(x)
-			yielder.value = v.walk(n.Value)
-			yielder.opt = opt
-			yielder.def = isDef
 			v.object.comprehensions = append(v.object.comprehensions, fc)
 
 		case *ast.TemplateLabel:
@@ -431,23 +393,11 @@
 					v.sel = "*"
 				}
 			}
-			attrs, err := createAttrs(v.ctx(), newNode(n), n.Attrs)
-			if err != nil {
-				return v.errf(n, err.format, err.args)
-			}
 			f, ok := v.nodeLabel(x)
 			if !ok {
 				return v.errf(n.Label, "invalid field name: %v", n.Label)
 			}
 			if f != 0 {
-				var leftOverDoc *docNode
-				for _, c := range n.Comments() {
-					if c.Position == 0 {
-						leftOverDoc = v.doc
-						v.doc = &docNode{n: n}
-						break
-					}
-				}
 				val := v.walk(n.Value)
 				v.object.insertValue(v.ctx(), f, opt, isDef, val, attrs, v.doc)
 				v.doc = leftOverDoc
diff --git a/cue/ast_test.go b/cue/ast_test.go
index 2663112..634cb05 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -31,7 +31,7 @@
 		in: `{
 		  foo: 1,
 		}`,
-		out: "<0>{}", // emitted value, but no top-level fields
+		out: "<0>{<1>{foo: 1}, }", // emitted value, but no top-level fields
 	}, {
 		in: `
 		foo: 1
@@ -224,19 +224,19 @@
 			c: 3
 		}
 		`,
-		out: `<0>{a: <1>{ <2>for k, v in <0>.b if (<0>.b.a < <2>.k) yield (""+<2>.k+""): <2>.v}, b: <3>{a: 1, b: 2, c: 3}}`,
+		out: `<0>{a: <1>{ <2>for k, v in <0>.b if (<0>.b.a < <2>.k) yield <3>{""+<2>.k+"": <2>.v}}, b: <4>{a: 1, b: 2, c: 3}}`,
 	}, {
 		in: `
 			a: { for k, v in b {"\(v)": v} }
 			b: { a: "aa", b: "bb", c: "cc" }
 			`,
-		out: `<0>{a: <1>{ <2>for k, v in <0>.b yield (""+<2>.v+""): <2>.v}, b: <3>{a: "aa", b: "bb", c: "cc"}}`,
+		out: `<0>{a: <1>{ <2>for k, v in <0>.b yield <3>{""+<2>.v+"": <2>.v}}, b: <4>{a: "aa", b: "bb", c: "cc"}}`,
 	}, {
 		in: `
 			a: [ v for _, v in b ]
 			b: { a: 1, b: 2, c: 3 }
 			`,
-		out: `<0>{a: [ <1>for _, v in <0>.b yield (*nil*): <1>.v ], b: <2>{a: 1, b: 2, c: 3}}`,
+		out: `<0>{a: [ <1>for _, v in <0>.b yield <1>.v ], b: <2>{a: 1, b: 2, c: 3}}`,
 	}, {
 		in: `
 			a: >=1 & <=2
diff --git a/cue/debug.go b/cue/debug.go
index 1db9eff..bb130bf 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -224,6 +224,12 @@
 		write(x.op)
 		p.str(x.x)
 	case *binaryExpr:
+		if x.op == opUnifyUnchecked {
+			p.str(x.left)
+			write(", ")
+			p.str(x.right)
+			break
+		}
 		write("(")
 		p.str(x.left)
 		writef(" %v ", x.op)
@@ -284,6 +290,10 @@
 			p.str(x.template)
 			write(", ")
 		}
+		if x.emit != nil {
+			p.str(x.emit)
+			write(", ")
+		}
 		p.str(x.arcs)
 		for i, c := range x.comprehensions {
 			p.str(c)
@@ -332,21 +342,20 @@
 		}
 
 	case *fieldComprehension:
-		p.str(x.clauses)
+		p.str(x.key)
+		writef(": ")
+		p.str(x.val)
 
 	case *listComprehension:
 		writef("[")
 		p.str(x.clauses)
 		write(" ]")
 
+	case *structComprehension:
+		p.str(x.clauses)
+
 	case *yield:
 		writef(" yield ")
-		writef("(")
-		p.str(x.key)
-		if x.opt {
-			writef("?")
-		}
-		writef("): ")
 		p.str(x.value)
 
 	case *feed:
diff --git a/cue/eval.go b/cue/eval.go
index bb4399f..ee66cba 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -258,10 +258,7 @@
 func (x *listComprehension) evalPartial(ctx *context) evaluated {
 	s := &structLit{baseValue: x.baseValue}
 	list := &list{baseValue: x.baseValue, elem: s}
-	err := x.clauses.yield(ctx, func(k, v evaluated, _, _ bool) *bottom {
-		if !k.kind().isAnyOf(intKind) {
-			return ctx.mkErr(k, "key must be of type int")
-		}
+	err := x.clauses.yield(ctx, func(v evaluated) *bottom {
 		list.elem.arcs = append(list.elem.arcs, arc{
 			feature: label(len(list.elem.arcs)),
 			v:       v.evalPartial(ctx),
@@ -275,11 +272,49 @@
 	return list
 }
 
+func (x *structComprehension) evalPartial(ctx *context) evaluated {
+	st := &structLit{baseValue: x.baseValue}
+	err := x.clauses.yield(ctx, func(v evaluated) *bottom {
+		embed := v.evalPartial(ctx).(*structLit)
+		embed, err := embed.expandFields(ctx)
+		if err != nil {
+			return err
+		}
+		res := binOp(ctx, x, opUnify, st, embed)
+		switch u := res.(type) {
+		case *bottom:
+			return u
+		case *structLit:
+			st = u
+		default:
+			panic("unreachable")
+		}
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+	return st
+}
+
 func (x *feed) evalPartial(ctx *context) evaluated  { return x }
 func (x *guard) evalPartial(ctx *context) evaluated { return x }
 func (x *yield) evalPartial(ctx *context) evaluated { return x }
 
-func (x *fieldComprehension) evalPartial(ctx *context) evaluated { return x }
+func (x *fieldComprehension) evalPartial(ctx *context) evaluated {
+	k := x.key.evalPartial(ctx)
+	v := x.val.evalPartial(ctx)
+	if err := firstBottom(k, v); err != nil {
+		return err
+	}
+	if !k.kind().isAnyOf(stringKind) {
+		return ctx.mkErr(k, "key must be of type string")
+	}
+	f := ctx.label(k.strValue(), true)
+	st := &structLit{baseValue: x.baseValue}
+	st.insertValue(ctx, f, x.opt, x.def, v, x.attrs, x.doc)
+	return st
+}
 
 func (x *structLit) evalPartial(ctx *context) (result evaluated) {
 	if ctx.trace {
@@ -290,6 +325,9 @@
 
 	// TODO: Handle cycle?
 
+	// TODO: would be great to be able to expand fields here. But would need
+	// some careful consideration regarding dereferencing.
+
 	return x
 }
 
diff --git a/cue/export.go b/cue/export.go
index ec454b5..8035c94 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -659,35 +659,33 @@
 	for _, v := range x.comprehensions {
 		switch c := v.(type) {
 		case *fieldComprehension:
+			l := p.expr(c.key)
+			label, _ := l.(ast.Label)
+			opt := token.NoPos
+			if c.opt {
+				opt = token.NoSpace.Pos() // anything but token.NoPos
+			}
+			tok := token.COLON
+			if c.def {
+				tok = token.ISA
+			}
+			f := &ast.Field{
+				Label:    label,
+				Optional: opt,
+				Token:    tok,
+				Value:    p.expr(c.val),
+			}
+			obj.Elts = append(obj.Elts, f)
+
+		case *structComprehension:
 			var clauses []ast.Clause
 			next := c.clauses
 			for {
 				if yield, ok := next.(*yield); ok {
-					l := p.expr(yield.key)
-					label, ok := l.(ast.Label)
-					if !ok {
-						// TODO: add an invalid field instead?
-						continue
-					}
-					opt := token.NoPos
-					if yield.opt {
-						opt = token.NoSpace.Pos() // anything but token.NoPos
-					}
-					f := &ast.Field{
-						Label:    label,
-						Optional: opt,
-						Value:    p.expr(yield.value),
-					}
-					var decl ast.Decl = f
-					if len(clauses) > 0 {
-						decl = &ast.Comprehension{
-							Clauses: clauses,
-							Value: &ast.StructLit{
-								Elts: []ast.Decl{f},
-							},
-						}
-					}
-					obj.Elts = append(obj.Elts, decl)
+					obj.Elts = append(obj.Elts, &ast.Comprehension{
+						Clauses: clauses,
+						Value:   p.expr(yield.value),
+					})
 					break
 				}
 
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 8a9b838..b1c776c 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -1324,10 +1324,11 @@
 		`,
 		out: `<0>{` +
 			`A :: <1>C{f1: int, f2: int}, ` +
-			`B :: <2>C{f1: int}, ` +
-			`C :: <3>C{f1: int}, ` +
-			`D :: <4>{f1: int, ...}, ` +
-			`a: <5>C{f1: int, f2: int, f3: int}}`,
+			`a: <2>C{f1: int, f2: int, f3: int}, ` +
+			`B :: <3>C{f1: int}, ` +
+			`C :: <4>C{f1: int}, ` +
+			`D :: <5>{f1: int, ...}` +
+			`}`,
 	}, {
 		desc: "reference to root",
 		in: `
@@ -1580,8 +1581,7 @@
 				}
 			}
 		`,
-		out: `<0>{obj: <1>{<>: <2>(Name: string)-><3>{a: (*"dummy" | string) if true yield ("sub"): <4>{as: <3>.a}}, ` +
-			`foo: <5>{a: "bar", sub: <6>{as: "bar"}}}}`,
+		out: `<0>{obj: <1>{<>: <2>(Name: string)-><3>{a: (*"dummy" | string) if true yield <4>{sub: <5>{as: <3>.a}}}, foo: <6>{a: "bar", sub: <7>{as: "bar"}}}}`,
 	}, {
 		desc: "builtins",
 		in: `
@@ -1911,7 +1911,7 @@
 				}
 			}
 		`,
-		out: `<0>{b: true, c: <1>{a: 3}, a: "foo", d: <2>{a: int if (<2>.a > 1) yield ("a"): 3}}`,
+		out: `<0>{b: true, a: "foo", c: <1>{a: 3}, d: <2>{a: int if (<2>.a > 1) yield <3>{a: 3}}}`,
 	}, {
 		desc: "referencing field in field comprehension",
 		in: `
@@ -2202,7 +2202,7 @@
 			`fibRec: <1>{` +
 			`nn: int, ` +
 			`out: (<2>.fib & <3>{n: <4>.nn}).out}, ` +
-			`fib: <5>{n: int if (<5>.n >= 2) yield ("out"): ((<2>.fibRec & <6>{nn: (<5>.n - 2)}).out + (<2>.fibRec & <7>{nn: (<5>.n - 1)}).out),  if (<5>.n < 2) yield ("out"): <5>.n}, ` +
+			`fib: <5>{n: int if (<5>.n >= 2) yield <6>{out: ((<2>.fibRec & <7>{nn: (<5>.n - 2)}).out + (<2>.fibRec & <8>{nn: (<5>.n - 1)}).out)},  if (<5>.n < 2) yield <9>{out: <5>.n}}, ` +
 			`fib2: 1, ` +
 			`fib7: 13, ` +
 			`fib12: 144}`,
@@ -2320,17 +2320,9 @@
 	// Don't remove. For debugging.
 	testCases := []testCase{{
 		in: `
-		fnRec: {nn: [...int], out: (fn & {arg: nn}).out}
-		fn: {
-			arg: [...int]
-
-			out: arg[0] + (fnRec & {nn: arg[1:]}).out if len(arg) > 0
-			out: 0 if len(arg) == 0
-		}
-		fn7: (fn & {arg: [1, 2, 3]}).out
-
-
-		`,
+		g1: 1
+		"g\(1)"?: 1
+	`,
 	}}
 	rewriteHelper(t, testCases, evalFull)
 }
diff --git a/cue/rewrite.go b/cue/rewrite.go
index ff4e7ba..6d13eaa 100644
--- a/cue/rewrite.go
+++ b/cue/rewrite.go
@@ -225,24 +225,29 @@
 	return &listComprehension{x.baseValue, clauses}
 }
 
-func (x *fieldComprehension) rewrite(ctx *context, fn rewriteFunc) value {
+func (x *structComprehension) rewrite(ctx *context, fn rewriteFunc) value {
 	clauses := rewrite(ctx, x.clauses, fn).(yielder)
 	if clauses == x.clauses {
 		return x
 	}
-	return &fieldComprehension{x.baseValue, clauses, x.isTemplate}
+	return &structComprehension{x.baseValue, clauses}
+}
+
+func (x *fieldComprehension) rewrite(ctx *context, fn rewriteFunc) value {
+	key := rewrite(ctx, x.key, fn)
+	val := rewrite(ctx, x.val, fn)
+	if key == x.key && val == x.val {
+		return x
+	}
+	return &fieldComprehension{x.baseValue, key, val, x.opt, x.def, x.doc, x.attrs}
 }
 
 func (x *yield) rewrite(ctx *context, fn rewriteFunc) value {
-	key := x.key
-	if key != nil {
-		key = rewrite(ctx, x.key, fn)
-	}
 	value := rewrite(ctx, x.value, fn)
-	if key == x.key && value == x.value {
+	if value == x.value {
 		return x
 	}
-	return &yield{x.baseValue, x.opt, x.def, key, value}
+	return &yield{x.baseValue, value}
 }
 
 func (x *guard) rewrite(ctx *context, fn rewriteFunc) value {
diff --git a/cue/subsume.go b/cue/subsume.go
index 0440e54..cacc6fa 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -90,6 +90,11 @@
 		if len(x.comprehensions) > 0 { //|| x.template != nil {
 			return false
 		}
+		if x.emit != nil {
+			if o.emit == nil || !subsumes(ctx, x.emit, o.emit, mode) {
+				return false
+			}
+		}
 
 		// all arcs in n must exist in v and its values must subsume.
 		for _, a := range x.arcs {
@@ -471,9 +476,20 @@
 }
 
 // structural equivalence
+func (x *structComprehension) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
+	if b, ok := v.(*structComprehension); ok {
+		return subsumes(ctx, x.clauses, b.clauses, 0)
+	}
+	return isBottom(v)
+}
+
+// structural equivalence
 func (x *fieldComprehension) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
 	if b, ok := v.(*fieldComprehension); ok {
-		return subsumes(ctx, x.clauses, b.clauses, 0)
+		return subsumes(ctx, x.key, b.key, 0) &&
+			subsumes(ctx, x.val, b.val, 0) &&
+			!x.opt && b.opt &&
+			x.def == b.def
 	}
 	return isBottom(v)
 }
@@ -481,8 +497,7 @@
 // structural equivalence
 func (x *yield) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
 	if b, ok := v.(*yield); ok {
-		return subsumes(ctx, x.key, b.key, 0) &&
-			subsumes(ctx, x.value, b.value, 0)
+		return subsumes(ctx, x.value, b.value, 0)
 	}
 	return isBottom(v)
 }
diff --git a/cue/value.go b/cue/value.go
index c24af45..6f5f659 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -803,71 +803,47 @@
 	x.expanded = x
 
 	comprehensions := x.comprehensions
-	emit := x.emit
-	template := x.template
-	newArcs := []arc{}
+
+	var n evaluated = &top{x.baseValue}
+	if x.emit != nil {
+		n = x.emit.evalPartial(ctx)
+	}
 
 	for _, x := range comprehensions {
-		switch c := x.(type) {
-		case *fieldComprehension:
-			err := c.clauses.yield(ctx, func(k, v evaluated, opt, def bool) *bottom {
-				if !k.kind().isAnyOf(stringKind) {
-					return ctx.mkErr(k, "key must be of type string")
-				}
+		v := x.evalPartial(ctx)
+		src := binSrc(x.Pos(), opUnify, x, v)
+		n = binOp(ctx, src, opUnifyUnchecked, n, v)
+	}
 
-				if c.isTemplate {
-					// TODO: disallow altogether or only when it refers to fields.
-					if template == nil {
-						template = v
-					} else {
-						template = mkBin(ctx, c.Pos(), opUnify, template, v)
-					}
-					return nil
-				}
-				// TODO(perf): improve big O
-				f := ctx.label(k.strValue(), true)
-				for i, a := range newArcs {
-					if a.feature == f {
-						newArcs[i].v = mkBin(ctx, x.Pos(), opUnify, a.v, v)
-						return nil
-					}
-				}
-				newArcs = append(newArcs, arc{
-					feature:    f,
-					optional:   opt,
-					definition: def,
-					v:          v,
-				})
-				return nil
-			})
-			if err != nil {
-				return nil, err
-			}
+	src := binSrc(x.Pos(), opUnify, x, n)
+	switch n.(type) {
+	case *bottom:
+	case *structLit:
+		orig := *x
+		orig.comprehensions = nil
+		orig.emit = nil
+		n = binOp(ctx, src, opUnifyUnchecked, &orig, n)
+
+	default:
+		if x.emit != nil {
+			v := x.emit.evalPartial(ctx)
+			n = binOp(ctx, src, opUnifyUnchecked, v, n)
 		}
+		return nil, ctx.mkErr(x, "invalid embedding")
 	}
 
-	// TODO: 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. Or should we relax
-	// the specs?
+	switch v := n.(type) {
+	case *bottom:
+		x.expanded = n
+		return nil, v
+	case *structLit:
+		x.expanded = n
+		return v, nil
 
-	orig := *x
-	orig.comprehensions = nil
-
-	res := orig.binOp(ctx, x, opUnifyUnchecked, &structLit{
-		x.baseValue, // baseValue
-		emit,        // emit
-		template,    // template
-		false,       // isClosed
-		nil,         // comprehensions
-		newArcs,     // arcs
-		nil,         // attributes
-	})
-
-	x.expanded = res
-	if isBottom(res) {
-		return nil, res.(*bottom)
+	default:
+		x.expanded = x
+		return x, nil
 	}
-	return x.expanded.(*structLit), nil
 }
 
 func (x *structLit) applyTemplate(ctx *context, i int, v evaluated) (evaluated, *ast.Field) {
@@ -1377,17 +1353,32 @@
 	return listKind | nonGround | referenceKind
 }
 
+type structComprehension struct {
+	baseValue
+	clauses yielder
+}
+
+func (x *structComprehension) kind() kind {
+	return structKind | nonGround | referenceKind
+}
+
+// TODO: rename to something better. No longer a comprehension.
+// Generated field, perhaps.
 type fieldComprehension struct {
 	baseValue
-	clauses    yielder
-	isTemplate bool
+	key   value
+	val   value
+	opt   bool
+	def   bool
+	doc   *docNode
+	attrs *attributes
 }
 
 func (x *fieldComprehension) kind() kind {
 	return structKind | nonGround
 }
 
-type yieldFunc func(k, v evaluated, optional, definition bool) *bottom
+type yieldFunc func(v evaluated) *bottom
 
 type yielder interface {
 	value
@@ -1396,29 +1387,17 @@
 
 type yield struct {
 	baseValue
-	opt   bool
-	def   bool
-	key   value
 	value value
 }
 
 func (x *yield) kind() kind { return topKind | referenceKind }
 
 func (x *yield) yield(ctx *context, fn yieldFunc) *bottom {
-	var k evaluated
-	if x.key != nil {
-		k = ctx.manifest(x.key)
-		if err, ok := k.(*bottom); ok {
-			return err
-		}
-	} else {
-		k = &top{}
-	}
 	v := x.value.evalPartial(ctx)
 	if err, ok := v.(*bottom); ok {
 		return err
 	}
-	if err := fn(k, v, x.opt, x.def); err != nil {
+	if err := fn(v); err != nil {
 		return err
 	}
 	return nil
diff --git a/doc/tutorial/kubernetes/testdata/manual.out b/doc/tutorial/kubernetes/testdata/manual.out
index 8e1b30f..6019a37 100644
--- a/doc/tutorial/kubernetes/testdata/manual.out
+++ b/doc/tutorial/kubernetes/testdata/manual.out
@@ -1559,11 +1559,11 @@
                                 name:          "peer"
                                 containerPort: 2380
                             }]
-                            command: ["/usr/local/bin/etcd"]
                             volumeMounts: [{
                                 name:      "etcd3"
                                 mountPath: "/data"
                             }]
+                            command: ["/usr/local/bin/etcd"]
                             livenessProbe: {
                                 httpGet: {
                                     path: "/health"
@@ -1650,11 +1650,11 @@
                 template: {
                     spec: {
                         containers: [{
-                            command: ["/usr/local/bin/etcd"]
                             volumeMounts: [{
                                 name:      "etcd3"
                                 mountPath: "/data"
                             }]
+                            command: ["/usr/local/bin/etcd"]
                             livenessProbe: {
                                 httpGet: {
                                     path: "/health"
@@ -1859,6 +1859,9 @@
                             }]
                             volumeMounts: [_|_ /* non-concrete value bool */]
                         }]
+                        volumes: [{
+                            name: "secret-volume"
+                        }]
                         affinity: {
                             podAntiAffinity: {
                                 requiredDuringSchedulingIgnoredDuringExecution: [{
@@ -1873,9 +1876,6 @@
                                 }]
                             }
                         }
-                        volumes: [{
-                            name: "secret-volume"
-                        }]
                     }
                     metadata: {
                         labels: {
@@ -4855,13 +4855,13 @@
                                 }
                             }
                         }]
-                        hostNetwork: true
-                        hostPID:     true
                         volumes: [{
                             name: "proc"
                         }, {
                             name: "sys"
                         }]
+                        hostNetwork: true
+                        hostPID:     true
                     }
                     metadata: {
                         labels: {