internal/core/export: handle alias and lets

This was partly unimplemented.

The new implementation is not optimal, but should at least
handle all cases off aliasing and let correctly.

Fixes #590

Change-Id: Ia0915d611b2182a54458ab30dc79bf7ce916797d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7701
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/go.mod b/go.mod
index a9f3c09..a597758 100644
--- a/go.mod
+++ b/go.mod
@@ -23,4 +23,4 @@
 	gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71
 )
 
-go 1.12
+go 1.13
diff --git a/internal/core/export/adt.go b/internal/core/export/adt.go
index 429d8e4..17d591e 100644
--- a/internal/core/export/adt.go
+++ b/internal/core/export/adt.go
@@ -17,7 +17,6 @@
 import (
 	"bytes"
 	"fmt"
-	"strconv"
 	"strings"
 
 	"cuelang.org/go/cue/ast"
@@ -63,15 +62,34 @@
 
 	case *adt.FieldReference:
 		f := e.frame(x.UpCount)
-		ident := e.ident(x.Label)
 		entry := f.fields[x.Label]
+
+		name := x.Label.IdentString(e.ctx)
+		switch {
+		case entry.alias != "":
+			name = entry.alias
+
+		case !ast.IsValidIdent(name):
+			name = "X"
+			if x.Src != nil {
+				name = x.Src.Name
+			}
+			name = e.uniqueAlias(name)
+			entry.alias = name
+		}
+
+		ident := ast.NewIdent(name)
 		entry.references = append(entry.references, ident)
+
+		if f.fields != nil {
+			f.fields[x.Label] = entry
+		}
+
 		return ident
 
 	case *adt.LabelReference:
 		// get potential label from Source. Otherwise use X.
 		f := e.frame(x.UpCount)
-		var ident *ast.Ident
 		if f.field == nil {
 			// This can happen when the LabelReference is evaluated outside of
 			// normal evaluation, that is, if a pattern constraint or
@@ -82,24 +100,35 @@
 		if !ok || len(list.Elts) != 1 {
 			panic("label reference to non-pattern constraint field or invalid list")
 		}
+		name := ""
 		if a, ok := list.Elts[0].(*ast.Alias); ok {
-			ident = ast.NewIdent(a.Ident.Name)
+			name = a.Ident.Name
 		} else {
-			ident = ast.NewIdent("X" + strconv.Itoa(e.unique))
-			e.unique++
+			if x.Src != nil {
+				name = x.Src.Name
+			}
+			name = e.uniqueAlias(name)
 			list.Elts[0] = &ast.Alias{
-				Ident: ast.NewIdent(ident.Name),
+				Ident: ast.NewIdent(name),
 				Expr:  list.Elts[0],
 			}
 		}
+		ident := ast.NewIdent(name)
 		ident.Scope = f.field
 		ident.Node = f.labelExpr
 		return ident
 
 	case *adt.DynamicReference:
 		// get potential label from Source. Otherwise use X.
-		ident := ast.NewIdent("X")
+		name := "X"
 		f := e.frame(x.UpCount)
+		if d := f.field; d != nil {
+			if x.Src != nil {
+				name = x.Src.Name
+			}
+			name = e.getFieldAlias(d, name)
+		}
+		ident := ast.NewIdent(name)
 		ident.Scope = f.field
 		ident.Node = f.field
 		return ident
@@ -121,14 +150,10 @@
 		return ident
 
 	case *adt.LetReference:
-		// TODO:
-		// - rename if necessary
-		// - look in to reusing the mechanism of the old evaluator
-		//
-		// Either way, we need a better mechanism. References may go out of
-		// scope. In case of aliases this means they may need to be reproduced
-		// locally. Most of these issues can be avoided by either fully
-		// expanding a configuration (export) or not at all (def).
+		// TODO: Handle references that went out of scope. In case of aliases
+		// this means they may need to be reproduced locally. Most of these
+		// issues can be avoided by either fully expanding a configuration
+		// (export) or not at all (def).
 		//
 		i := len(e.stack) - 1 - int(x.UpCount) - 1
 		if i < 0 {
@@ -140,14 +165,15 @@
 			if f.let == nil {
 				f.let = map[adt.Expr]*ast.LetClause{}
 			}
+			label := e.uniqueLetIdent(x.Label, x.X)
 			let = &ast.LetClause{
-				Ident: e.ident(x.Label),
+				Ident: e.ident(label),
 				Expr:  e.expr(x.X),
 			}
 			f.let[x.X] = let
 			f.scope.Elts = append(f.scope.Elts, let)
 		}
-		ident := e.ident(x.Label)
+		ident := ast.NewIdent(let.Ident.Name)
 		ident.Node = let
 		ident.Scope = f.scope
 		return ident
@@ -283,9 +309,16 @@
 		e.setDocs(x)
 		f := &ast.Field{
 			Label: e.stringLabel(x.Label),
-			Value: e.expr(x.Value),
 		}
-		e.addField(x.Label, f.Value)
+
+		frame := e.frame(0)
+		entry := frame.fields[x.Label]
+		entry.field = f
+		entry.node = f.Value
+		frame.fields[x.Label] = entry
+
+		f.Value = e.expr(x.Value)
+
 		// extractDocs(nil)
 		return f
 
@@ -294,9 +327,16 @@
 		f := &ast.Field{
 			Label:    e.stringLabel(x.Label),
 			Optional: token.NoSpace.Pos(),
-			Value:    e.expr(x.Value),
 		}
-		e.addField(x.Label, f.Value)
+
+		frame := e.frame(0)
+		entry := frame.fields[x.Label]
+		entry.field = f
+		entry.node = f.Value
+		frame.fields[x.Label] = entry
+
+		f.Value = e.expr(x.Value)
+
 		// extractDocs(nil)
 		return f
 
@@ -385,9 +425,9 @@
 			if x.Key != 0 {
 				key := e.ident(x.Key)
 				clause.Key = key
-				e.addField(x.Key, clause)
+				e.addField(x.Key, nil, clause)
 			}
-			e.addField(x.Value, clause)
+			e.addField(x.Value, nil, clause)
 
 			y = x.Dst
 
@@ -403,7 +443,7 @@
 			_, saved := e.pushFrame(nil)
 			defer e.popFrame(saved)
 
-			e.addField(x.Label, clause)
+			e.addField(x.Label, nil, clause)
 
 			y = x.Dst
 
diff --git a/internal/core/export/export.go b/internal/core/export/export.go
index da23162..5902187 100644
--- a/internal/core/export/export.go
+++ b/internal/core/export/export.go
@@ -15,12 +15,16 @@
 package export
 
 import (
+	"fmt"
+	"math/rand"
+
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/ast/astutil"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/internal"
 	"cuelang.org/go/internal/core/adt"
 	"cuelang.org/go/internal/core/eval"
+	"cuelang.org/go/internal/core/walk"
 )
 
 const debug = false
@@ -88,10 +92,14 @@
 // Def exports v as a definition.
 func (p *Profile) Def(r adt.Runtime, pkgID string, v *adt.Vertex) (*ast.File, errors.Error) {
 	e := newExporter(p, r, pkgID, v)
+	e.markUsedFeatures(v)
+
 	if v.Label.IsDef() {
 		e.inDefinition++
 	}
+
 	expr := e.expr(v)
+
 	if v.Label.IsDef() {
 		e.inDefinition--
 		if s, ok := expr.(*ast.StructLit); ok {
@@ -110,6 +118,8 @@
 
 func (p *Profile) Expr(r adt.Runtime, pkgID string, n adt.Expr) (ast.Expr, errors.Error) {
 	e := newExporter(p, r, pkgID, nil)
+	e.markUsedFeatures(n)
+
 	return e.expr(n), nil
 }
 
@@ -170,6 +180,7 @@
 		index: r,
 		pkgID: pkgID,
 	}
+	e.markUsedFeatures(n)
 	v := e.value(n, n.Conjuncts...)
 
 	return e.toFile(n, v)
@@ -187,6 +198,7 @@
 		index: r,
 		pkgID: pkgID,
 	}
+	e.markUsedFeatures(n)
 	v := e.value(n)
 	return v, e.errs
 }
@@ -198,19 +210,23 @@
 	ctx *adt.OpContext
 
 	index adt.StringIndexer
+	rand  *rand.Rand
 
 	// For resolving up references.
 	stack []frame
 
 	inDefinition int // for close() wrapping.
 
-	unique int
-
 	// hidden label handling
-	pkgID       string
-	hidden      map[string]adt.Feature // adt.InvalidFeatures means more than one.
-	usedFeature map[adt.Feature]string
-	usedHidden  map[string]bool
+	pkgID  string
+	hidden map[string]adt.Feature // adt.InvalidFeatures means more than one.
+
+	// If a used feature maps to an expression, it means it is assigned to a
+	// unique let expression.
+	usedFeature map[adt.Feature]adt.Expr
+	labelAlias  map[adt.Expr]adt.Feature
+
+	usedHidden map[string]bool
 }
 
 func newExporter(p *Profile, r adt.Runtime, pkgID string, v *adt.Vertex) *exporter {
@@ -222,6 +238,117 @@
 	}
 }
 
+func (e *exporter) markUsedFeatures(x adt.Expr) {
+	e.usedFeature = make(map[adt.Feature]adt.Expr)
+
+	w := &walk.Visitor{}
+	w.Before = func(n adt.Node) bool {
+		switch x := n.(type) {
+		case *adt.Vertex:
+			if !x.IsData() {
+				for _, c := range x.Conjuncts {
+					w.Expr(c.Expr())
+				}
+			}
+
+		case *adt.DynamicReference:
+			if e.labelAlias == nil {
+				e.labelAlias = make(map[adt.Expr]adt.Feature)
+			}
+			// TODO: add preferred label.
+			e.labelAlias[x.Label] = adt.InvalidLabel
+
+		case *adt.LabelReference:
+		}
+		return true
+	}
+
+	w.Feature = func(f adt.Feature, src adt.Node) {
+		_, ok := e.usedFeature[f]
+
+		switch x := src.(type) {
+		case *adt.LetReference:
+			if !ok {
+				e.usedFeature[f] = x.X
+			}
+
+		default:
+			e.usedFeature[f] = nil
+		}
+	}
+
+	w.Expr(x)
+}
+
+func (e *exporter) getFieldAlias(f *ast.Field, name string) string {
+	a, ok := f.Label.(*ast.Alias)
+	if !ok {
+		a = &ast.Alias{
+			Ident: ast.NewIdent(e.uniqueAlias(name)),
+			Expr:  f.Label.(ast.Expr),
+		}
+		f.Label = a
+	}
+	return a.Ident.Name
+}
+
+func setFieldAlias(f *ast.Field, name string) {
+	if _, ok := f.Label.(*ast.Alias); !ok {
+		f.Label = &ast.Alias{
+			Ident: ast.NewIdent(name),
+			Expr:  f.Label.(ast.Expr),
+		}
+	}
+}
+
+// uniqueLetIdent returns a name for a let identifier that uniquely identifies
+// the given expression. If the preferred name is already taken, a new globally
+// unique name of the form base_X ... base_XXXXXXXXXXXXXX is generated.
+//
+// It prefers short extensions over large ones, while ensuring the likelihood of
+// fast termination is high. There are at least two digits to make it visually
+// clearer this concerns a generated number.
+//
+func (e exporter) uniqueLetIdent(f adt.Feature, x adt.Expr) adt.Feature {
+	if e.usedFeature[f] == x {
+		return f
+	}
+
+	f, _ = e.uniqueFeature(f.IdentString(e.ctx))
+	e.usedFeature[f] = x
+	return f
+}
+
+func (e exporter) uniqueAlias(name string) string {
+	f := adt.MakeIdentLabel(e.ctx, name, "")
+
+	if _, ok := e.usedFeature[f]; !ok {
+		e.usedFeature[f] = nil
+		return name
+	}
+
+	_, name = e.uniqueFeature(f.IdentString(e.ctx))
+	return name
+}
+
+func (e exporter) uniqueFeature(base string) (f adt.Feature, name string) {
+	if e.rand == nil {
+		e.rand = rand.New(rand.NewSource(808))
+	}
+
+	const mask = 0xff_ffff_ffff_ffff // max bits; stay clear of int64 overflow
+	const shift = 4                  // rate of growth
+	for n := int64(0x10); ; n = int64(mask&((n<<shift)-1)) + 1 {
+		num := e.rand.Intn(int(n))
+		name := fmt.Sprintf("%s_%01X", base, num)
+		f := adt.MakeIdentLabel(e.ctx, name, "")
+		if _, ok := e.usedFeature[f]; !ok {
+			e.usedFeature[f] = nil
+			return f, name
+		}
+	}
+}
+
 type completeFunc func(scope *ast.StructLit, m adt.Node)
 
 type frame struct {
@@ -244,14 +371,16 @@
 }
 
 type entry struct {
-	node ast.Node
-
+	alias      string
+	field      *ast.Field
+	node       ast.Node // How to reference. See astutil.Resolve
 	references []*ast.Ident
 }
 
-func (e *exporter) addField(label adt.Feature, n ast.Node) {
+func (e *exporter) addField(label adt.Feature, f *ast.Field, n ast.Node) {
 	frame := e.top()
 	entry := frame.fields[label]
+	entry.field = f
 	entry.node = n
 	frame.fields[label] = entry
 }
@@ -272,8 +401,13 @@
 	top := e.stack[len(e.stack)-1]
 
 	for _, f := range top.fields {
+		node := f.node
+		if f.alias != "" && f.field != nil {
+			setFieldAlias(f.field, f.alias)
+			node = f.field
+		}
 		for _, r := range f.references {
-			r.Node = f.node
+			r.Node = node
 		}
 	}
 
diff --git a/internal/core/export/export_test.go b/internal/core/export/export_test.go
index 7aa4652..52bce8a 100644
--- a/internal/core/export/export_test.go
+++ b/internal/core/export/export_test.go
@@ -204,13 +204,6 @@
 
 // // // My first little foo.
 // foos: MyFoo: {}
-
-bar: 3
-d2: C="foo\(bar)": {
-    name: "xx"
-    foo: C.name
-}
-
 	`
 
 	archive := txtar.Parse([]byte(in))
diff --git a/internal/core/export/expr.go b/internal/core/export/expr.go
index d58eff8..a89beac 100644
--- a/internal/core/export/expr.go
+++ b/internal/core/export/expr.go
@@ -15,6 +15,7 @@
 package export
 
 import (
+	"fmt"
 	"sort"
 
 	"cuelang.org/go/cue/ast"
@@ -162,13 +163,21 @@
 			a = append(a, cc.c)
 		}
 
-		merged := e.mergeValues(f, nil, c, a...)
+		d := &ast.Field{Label: label}
+
+		top := e.frame(0)
+		if fr, ok := top.fields[f]; ok && fr.alias != "" {
+			setFieldAlias(d, fr.alias)
+			fr.node = d
+			top.fields[f] = fr
+		}
+
+		d.Value = e.mergeValues(f, nil, c, a...)
 
 		if f.IsDef() {
 			x.inDefinition--
 		}
 
-		d := &ast.Field{Label: label, Value: merged}
 		if isOptional(a) {
 			d.Optional = token.Blank.Pos()
 		}
@@ -255,7 +264,7 @@
 
 				// TODO: also handle dynamic fields
 			default:
-				panic("unreachable")
+				panic(fmt.Sprintf("Unexpected type %T", d))
 			}
 			e.addConjunct(label, env, d)
 		}
@@ -336,9 +345,25 @@
 }
 
 func isComplexStruct(s *adt.StructLit) bool {
-	for _, e := range s.Decls {
-		switch x := e.(type) {
-		case *adt.Field, *adt.OptionalField, adt.Expr:
+	for _, d := range s.Decls {
+		switch x := d.(type) {
+		case *adt.Field:
+			// TODO: remove this and also handle field annotation in expr().
+			// This allows structs to be merged. Ditto below.
+			if x.Src != nil {
+				if _, ok := x.Src.Label.(*ast.Alias); ok {
+					return ok
+				}
+			}
+
+		case *adt.OptionalField:
+			if x.Src != nil {
+				if _, ok := x.Src.Label.(*ast.Alias); ok {
+					return ok
+				}
+			}
+
+		case adt.Expr:
 
 		case *adt.Ellipsis:
 			if x.Value != nil {
diff --git a/internal/core/export/testdata/adt.txtar b/internal/core/export/testdata/adt.txtar
index c407f8e..e26c8c8 100644
--- a/internal/core/export/testdata/adt.txtar
+++ b/internal/core/export/testdata/adt.txtar
@@ -90,9 +90,9 @@
 }
 bar: "bar"
 d2: {
-	"foo\(bar)": {
+	C="foo\(bar)": {
 		name: "xx"
-		foo:  X.name
+		foo:  C.name
 	}
 }
 c1: mystrings.Contains("aa", "a")
diff --git a/internal/core/export/testdata/alias.txtar b/internal/core/export/testdata/alias.txtar
new file mode 100644
index 0000000..ce85066
--- /dev/null
+++ b/internal/core/export/testdata/alias.txtar
@@ -0,0 +1,85 @@
+// TODO: merge the resulting files. This can only be done if off-by-one handling
+// is moved to expr handling too.
+//
+// For now this is better than panicking.
+
+-- x.cue --
+X="a-b": 4
+
+foo: X
+bar?: Y
+
+Y="a-c": 5
+
+-- y.cue --
+baz: 3
+X="d-2": E=[D="cue"]: C="foo\(baz)": {
+    name: "xx"
+	foo: C.name
+	bar: X
+	baz: D
+	qux: E
+}
+-- out/definition --
+
+{
+	X="a-b": 4
+	foo:     X
+	bar?:    Y
+	Y="a-c": 5
+} & {
+	baz: 3
+	X_1="d-2": {
+		E=[D="cue"]: {
+			C="foo\(baz)": {
+				name: "xx"
+				foo:  C.name
+				bar:  X_1
+				baz:  D
+				qux:  E
+			}
+		}
+	}
+}
+-- out/doc --
+[]
+["a-b"]
+[foo]
+["a-c"]
+[baz]
+["d-2"]
+-- out/value --
+== Simplified
+{
+	"a-b": 4
+	foo:   4
+	baz:   3
+	"a-c": 5
+	"d-2": {}
+}
+== Raw
+{
+	"a-b":   4
+	foo:     4
+	bar?:    Y
+	baz:     3
+	Y="a-c": 5
+	"d-2": {}
+}
+== Final
+{
+	"a-b": 4
+	foo:   4
+	baz:   3
+	"a-c": 5
+	"d-2": {}
+}
+== All
+{
+	"a-b":   4
+	foo:     4
+	bar?:    Y
+	baz:     3
+	Y="a-c": 5
+	"d-2": {}
+}
diff --git a/internal/core/export/testdata/let.txtar b/internal/core/export/testdata/let.txtar
index 2d0b306..ced992d 100644
--- a/internal/core/export/testdata/let.txtar
+++ b/internal/core/export/testdata/let.txtar
@@ -1,23 +1,54 @@
+# Issue #590
+
 -- in.cue --
 let X = 1 + 1
 #Foo: X
+-- x.cue --
+x: string
+
+let Y = x
+y: Y
+-- y.cue --
+x: "foo"
+
+// Note: although syntactically the same, this is a different let clause than
+// the one from before and thus will be renamed.
+// Issue #590
+let Y = x
+y: Y
 -- out/definition --
 
 let X = 1 + 1
 #Foo: X
+x:    "foo"
+let Y = x
+let Y_1 = x
+y: Y & Y_1
 -- out/doc --
 []
 [#Foo]
+[x]
+[y]
 -- out/value --
 == Simplified
-{}
+{
+	x: "foo"
+	y: "foo"
+}
 == Raw
 {
 	#Foo: 2
+	x:    "foo"
+	y:    "foo"
 }
 == Final
-{}
+{
+	x: "foo"
+	y: "foo"
+}
 == All
 {
 	#Foo: 2
+	x:    "foo"
+	y:    "foo"
 }
diff --git a/internal/core/export/value.go b/internal/core/export/value.go
index eef9b21..7bf8f11 100644
--- a/internal/core/export/value.go
+++ b/internal/core/export/value.go
@@ -321,6 +321,8 @@
 
 		f := &ast.Field{Label: e.stringLabel(label)}
 
+		e.addField(label, f, f.Value)
+
 		if label.IsDef() {
 			e.inDefinition++
 		}
diff --git a/internal/core/walk/walk.go b/internal/core/walk/walk.go
new file mode 100644
index 0000000..a0ef5e6
--- /dev/null
+++ b/internal/core/walk/walk.go
@@ -0,0 +1,185 @@
+// Copyright 2020 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.
+
+// walk provides functions for visiting the nodes of an ADT tree.
+package walk
+
+import (
+	"fmt"
+
+	"cuelang.org/go/internal/core/adt"
+)
+
+// Features calls f for all features used in x and indicates whether the
+// feature is used as a reference or not.
+func Features(x adt.Expr, f func(label adt.Feature, src adt.Node)) {
+	w := Visitor{
+		Feature: f,
+	}
+	w.Expr(x)
+}
+
+type Visitor struct {
+	// TODO: lets really should be special fields
+	letDone map[adt.Expr]bool
+
+	Feature func(f adt.Feature, src adt.Node)
+	Before  func(adt.Node) bool
+}
+
+func (w *Visitor) init() {
+	if w.letDone == nil {
+		w.letDone = map[adt.Expr]bool{}
+	}
+}
+
+func (w *Visitor) Expr(x adt.Expr) {
+	w.init()
+	w.node(x)
+}
+
+func (w *Visitor) feature(x adt.Feature, src adt.Node) {
+	if w.Feature != nil {
+		w.Feature(x, src)
+	}
+}
+
+func (w *Visitor) node(n adt.Node) {
+	if w.Before != nil && !w.Before(n) {
+		return
+	}
+
+	switch x := n.(type) {
+	case nil:
+
+	// TODO: special-case Vertex?
+	case adt.Value:
+
+	case *adt.ListLit:
+		for _, x := range x.Elems {
+			w.node(x)
+		}
+
+	case *adt.StructLit:
+		for _, x := range x.Decls {
+			w.node(x)
+		}
+
+	case *adt.FieldReference:
+		w.feature(x.Label, x)
+
+	case *adt.LabelReference:
+
+	case *adt.DynamicReference:
+
+	case *adt.ImportReference:
+		w.feature(x.ImportPath, x)
+		w.feature(x.Label, x)
+
+	case *adt.LetReference:
+		w.feature(x.Label, x)
+		if w.letDone == nil {
+			w.letDone = map[adt.Expr]bool{}
+		}
+		if !w.letDone[x.X] {
+			w.letDone[x.X] = true
+			w.node(x.X)
+		}
+
+	case *adt.SelectorExpr:
+		w.node(x.X)
+		w.feature(x.Sel, x)
+
+	case *adt.IndexExpr:
+		w.node(x.X)
+		w.node(x.Index)
+
+	case *adt.SliceExpr:
+		w.node(x.X)
+		w.node(x.Lo)
+		w.node(x.Hi)
+		w.node(x.Stride)
+
+	case *adt.Interpolation:
+		for _, x := range x.Parts {
+			w.node(x)
+		}
+
+	case *adt.BoundExpr:
+		w.node(x.Expr)
+
+	case *adt.UnaryExpr:
+		w.node(x.X)
+
+	case *adt.BinaryExpr:
+		w.node(x.X)
+		w.node(x.Y)
+
+	case *adt.CallExpr:
+		w.node(x.Fun)
+		for _, arg := range x.Args {
+			w.node(arg)
+		}
+
+	case *adt.DisjunctionExpr:
+		for _, d := range x.Values {
+			w.node(d.Val)
+		}
+
+	// Fields
+
+	case *adt.Ellipsis:
+		if x.Value != nil {
+			w.node(x.Value)
+		}
+
+	case *adt.Field:
+		w.feature(x.Label, x)
+		w.node(x.Value)
+
+	case *adt.OptionalField:
+		w.feature(x.Label, x)
+		w.node(x.Value)
+
+	case *adt.BulkOptionalField:
+		w.node(x.Filter)
+		w.node(x.Value)
+
+	case *adt.DynamicField:
+		w.node(x.Key)
+		w.node(x.Value)
+
+	// Yielders
+
+	case *adt.ForClause:
+		w.feature(x.Key, x)
+		w.feature(x.Value, x)
+		w.node(x.Dst)
+
+	case *adt.IfClause:
+		w.node(x.Condition)
+		w.node(x.Dst)
+
+	case *adt.LetClause:
+		w.feature(x.Label, x)
+		w.node(x.Expr)
+		w.node(x.Dst)
+
+	case *adt.ValueClause:
+		w.node(x.StructLit)
+
+	default:
+		panic(fmt.Sprintf("unknown field %T", x))
+	}
+}
diff --git a/internal/filetypes/types.go b/internal/filetypes/types.go
index 33efc31..7dd0d80 100644
--- a/internal/filetypes/types.go
+++ b/internal/filetypes/types.go
@@ -41,5 +41,5 @@
 	return v
 }
 
-// Data size: 1573 bytes.
-var cuegenInstanceData = []byte("\x01\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xd4\x17[o\xdcD\xd7N\xf3I\x9fG\xfd\xbe\x17xE:u%T\xa2\xe2<\"\xad\x14U\x88\xb4(/\x14\xa1\xf2TU\u046c}\xbc;\u051e13\xe36Q\xb3\x0f@)\xfcU\xfeD\x17\x9d\x99\xf1u\xb7%\x91\x8a\x04y\xc9\xfa\xdc\xe6\xdc/\xff\xdb\xfev\x10\x1fl\u007f\x8f\xe2\xed\xcfQ\xf4\xc5\xf6\xa7[q|[Hc\xb9\xcc\xf1\x94[N\xf0\xf8V|\xf8\x9dR6>\x88\xe2\xc3o\xb9]\u01f7\xa3\xf8?\x8fD\x85&\u07be\x89\xa2\xe8\x93\xed\xaf\aq\xfc\xff\xa7\xcf\xf2\x16\xb3RT\x81\xf3M\x14o_G\u047d\xed/\xb7\xe2\xf8\xbf\x03\xfcu\x14\x1f\u0107\xdf\xf0\x1aI\u0421\x03\xb2(\x8a\xde~\xec4\x89\xe3\x838N\xece\x83&\xcb[\x8c\xdf~\xf4G\xc3\xf3\xe7|\x85\xb0lEU0v|\f_\x02\xbd\x0f\xb9\xd2\x1aM\xa3da\xc0*\xe0\xf0\xb5\xf2D\x19\xa13v\x97\xfe-\xe0\x15K\xe8y\xc9k\\@\xf83V\v\xb9b\t\xca\\\x15B\xaez\xc4\u0747\x01\xc2\x12!-\xeaF\xa3\xe5V(\xf9`\x01w\xcf&\x10\x96\x94J\xd7\x0fzV\xe2~\xa4t\xcd\x12\xcbW\xe6\x81{8y\xea_z\xb6\xe8\x9f\u0730\x8d3\xe2\x14K\xdeV\x16\x84\x01\xbbF \x15\xa15X@\xa94\x18[\b\t\\\x16\xf4K\xb56\x83'k\x04\x83\xd6\n\xb92P`\x83\xb2 )J\x0e\u0735*\xc8\xea \u063d\xef\\0\xb6\xff(\xfd<\x85\xabN\x99\xcd\u021dg\xb2TP`)$\x1aX\xab\x97\xc0\xbdTa\xc0y\t\v\xa7O\xef\x15,\x82\x87\x89q\xfcX\xc1-\x1f\x9crdu\x8bp\x05%\xaf\f\xb2Dc\x89\x1ae\x8ef\xb1\x8b\xcc/\xf3\xca#\xf6p:\xd5\x049\x9e(\x96JU,Q\r}\xf3\u02b3xX\xae\xa4\xb1\x9a\vi\a\xba\xe7\x88Mp\x8bY\x04\x98\x90\xb9\xaa\x9b\n\xad\u02ca\x00\xab\x1b\xa5m\xa7\x81\x87\x19\xab\x91\u05ddR\x1eV\xa8\xdc\f&z\x18\xb7V\x8bek\xbd\x01\x0e\xe6\xddKa1\x14;\x8a\x9b\xd7\xc1\u0178\x10\xa5\xf3\x85\x05\u0560\xe6\xde\x12O\x9d\xb1\xe3cb}\xb2F\x83`\xb1n*n\xd1\x00\xd7\xe8\x02 )\x1aV\xc1\x12\xa1\x95\xa2\x14Hq\x01n].h\xa5,\xa8\x12\xecZ\x18\x12\x92+Y\x8aU\xeb_\u0218{\xc0\xc5K\u0226\xf5i\x92L\x92&\x19\x95\xc5Q\x9a\xb7H\x19sN\xf0,\xcbX\x92lX\x92Th\xe1\x02N<\xf9\xd8\x1d\xb3\xa8%\x13\xbf\u0311$i\x94C\x17lx\xda\x04U\U00096c96*\xcdd&_c\u03432\u010b\x17\x16\xa5\xf1)\xe1\xa8\xd3\xec\a\xa3d\x1a\xbef%L\xd6\xf0\u05aa\u079c\x8dg\xb9\xe4uuS\x96\x9bql\xa8\xec\x13\xbc\xa0\xec\xfaK\x87;\vn\xe0\xf1\xe0\u04e3\xbd\x1e\x9f#o\xe8q\xaa\xe5\xc1\xdf\x1b\x96\xa8\xb6\xb1]\xd2\xfc34\xc2\x17T\xff\x1f6\x89\xffN\x03JAe>\xb6\xa0\xc0\xf2\xdf\\\x85\xdd@{\xd8\x15#\u053c1~x\f\x05J\xed*\xb4?\x8fj4\xb5=+\xa8\xdb\xcd\xea\xf8|\xe8\xad\xe7,Ii\x11H\x1d\x88\x06+}\xb1\xa1\xd0\x03\x90\xbe:(\x95\xe6\x00\xad\b\\\x15\x81|\n\x96\xfb\xc1\xa1!\x04!\xf4\xc5\xfa\x9a\x9fC\xed\x85\x1dA-^X\x82\xae\x94\az\xe8J\x11\xac\xd1\u02aa^5\xf7\xc5\x12j\xf2\x8fO\x1f/\x80\x1e7\xf8\xe3}\aJ\x9d\xa0\x8e\xa1\x97\xdc,\x03\xb6Y\xa6\xbd\x8b\x1cv)d\xb3\xec\xc7y\xb7\u00c0\x90\x85\xc8\xfd\xe4\xf0N\xa7\br\xeb\u018f\xc6F\xa3AI\x1b\x05p\n\xc7J\xf3:c\xfd\x06\xb4\x80;'i\xeaEJ\x98\xee>P\xa0E]\x8fv\x85\x1c\xb5\xe5Bvr\xc0\xacU[\x154\xa1&\x1b\xc3\xf11<R\x1a\xba-\xf3>\xb8z\xae\xf9\xe5\x8c\x128MK\x93k\xb1\xf4\xfa\xf9\xac\xbb\x0f/\xd7\"_\x83\xb0\x06\xab\xd2M7.\x895W\xf2\x05j\xeb\xc7\"\x87\xaf\xbe\u007f\x18826[\xdb\xfaM\xcc-k\xe3\xed-\xc0\xcbnk\xeckb\xbc@\xa5\xa5R>\xf7\xfc\xfe\xe7\x05\xa4\xfe\xb14\x84\x80b\xe2\xab WuMkS%$z\xb0U\xbb\xf9O\b\xf7\xa4\x17\xe3\x8b\xceK\xef%S\xa9\xad4o\xd6\x13\xac\x83xd\xc1W\x13T\xc1W\x1d\xc2\xf2\x19\xc6\x06\x81\xae\xae_\xb1q\xafq\xad\xc6!\xc9\xca\x1dl0=\xa0\xab\xbd\xf8\xca\x13P\x89\xec\xe0]m9\xb4K\xef\x1d\xbcOzG@\xa9\xee\xd3>]@\u07d1\x86\xd2\xf0\x14.\xf5\xa9\x1c\x06\n\x02y\x02\xa2\xddy\x82\x80i\xef\r\x17\xbe\xc1#\xab\x1d\x95\xfc_J+0q\xcd\xc7}J\xc0>\x80IRq\xf7\u020a\x8c\b\r\x9eX?\x88\xd4\xee\x86\bri\xcf\xf0\xf8\x1dv\xb7\x82\xecyp\xb2^\x84 \x8e\x93nG\xd0@p\x1dq\xaaA\xc9\x1b\xf1\x0eY\x01{\rA\xbe\x8c\u073c\xe9o\x920w\xa8w\xf1\xaa\xf2\xc8\f\xce,\x14\n\rHeA\u023cj\v\xf4\x17\x91\xd25\x9c\x9df\xcc\xd19\x85\xdc=F\x97\xe7I\u007f\x94\x8dJ|\xe3\xe6\xce\xf9\xbe\x1a\xeco\x99\xae\x18\xe1\nR7\xc7\u076f\xae\x06g\xa7\xc2|O\x98\x1e\x1c\xf3!<=o\xe6\xd8\xe9\xa1so\x82\xfe\f>\x9dCX2;\x83\xe6\xf2\xa6\a\xd1\x1c;=\x83f\xd8\ruC\xd9\xed\\\xe3\x95`\xc7_\xc1G;\xef\xed\xb7j\x90\xbf\xd3\xe6\x86\x00x_\x93\u05e9\xbd\xf9\xff\xaetgg'\xe9\xbc\xe3\xf3\xfd\xbe~\xaf63?\xee\xf7\xdf~\xbf\r\xf6L:\xb3\u025c\r#\xdb\xee\x9c\f)\u051d\xc0c\xe6q\xf7\xa6\x05x5\xf7\u02dd\x93\xd0\xec\xa7\xdavjMn\xee\u07ae\xf1\xad\xbd\u05c0\xbd~\xe9\xf5\u06b0\xe9\x8e\xd8O\x92\xae\b\x06\v\x8692\xac\xf0\xb3j\xf1E\x02W]\xdc\u01bbl\xa7\xc7x\x85\x1d\x84\x0fCf\xea\u0709\x1aT\x86^\xf2tn\xed\u0567'\x1c\x86\xc7^\xbaA\a\xab\xea\xf7\t\x1c\bG#oV8\xfb'\xe008f\xe4;\xa27l\xdam\xaf\xdf\xf1\xfc\x92\xedG\xc9\xf4\x95\xf9lx\xa7\xca\xef\x9d\x02\xd7\xe5\u06b0(\xfa3\x00\x00\xff\xff\xf3\xef>\x1d\x92\x14\x00\x00")
+// Data size: 1613 bytes.
+var cuegenInstanceData = []byte("\x01\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xacW\xcbo\xd5F\x17\xb7\x93|\xd2g\x8bv\u0172\x95\x0eF\xaahD\x1d\xb1@\xa0+E\b\bT\u0654\xaa\xa2+\x84\xa2\xb9\xf6\xb1\xef\x14{\xc6\xf5\x8c!\x11\xb9\x8b\xb6\x94\xf6\xaf\xe6Vgf\xfc\xbc\x06\x926\xd9\xe4\xfa\xbc\xe6\x9c\u07dc\xd7|\xb1\xf9k\xc7\xdf\xd9\xfc\xed\xf9\x9b\xdf=\xef\xde\xe6\xb7]\u07ff\u0185\xd2L$x\xc44#\xba\xbf\xeb\xef\xfd$\xa5\xf6w<\u007f\xefG\xa6W\xfe5\xcf\xff\xdfS^\xa0\xf27\xef=\xcf\xfbz\xf3\xe7\x8e\xef\u007f\xf9\xe2e\xd2`\x9c\xf1\xc2i\xbe\xf7\xfc\xcd;\u03fb\xb5\xf9c\xd7\xf7\xff\xdf\xd3\xdfy\xfe\x8e\xbf\xf7\x03+\x91\f\xed\x19b\xe8y\u0787\xeb_\x91'\xbe\xbf\xe3\xfb\x81>\xabP\xc5I\x83\xfe\x87\xeb\xbb\x15K^\xb1\x1ca\xd9\xf0\"\r\u00c3\x03x\bt>$\xb2\xaeQUR\xa4\n\xb4\x04\x06\xdfK+\x14\x13;\x0eo\u04bf\x05\xbc\r\x03:^\xb0\x12\x17\xe0\xfe\x94\xae\xb9\xc8\xc3\x00E\"S.\xf2\x8eq\U000c98c4\x01\x17\x1a\xeb\xaaF\xcd4\x97\xe2\xc1\x02n\x1e\x8f(a\x90\u027a|\u0429\x92\xf6SY\x97a\xa0Y\xae\x1e\x98\x83\x83\x17\xf6\xa4\x97\x8b\xee\xc8u\xb86A\x1ca\u019aB\x03W\xa0W\b\xe4\"4\nS\xc8d\rJ\xa7\\\x00\x13)\xfd\x92\x8d\x8e\xe1\xf9\nA\xa1\xd6\\\xe4\nR\xacP\xa4dE\x8a^\xbb\x94)E\xed\f\x9b\xf3\r\x04\xc3\xf8\xf7\xa3\xef\"8o\x9dY\x0f\xe0<\x16\x99\x84\x143.P\xc1J\xbe\x01f\xadr\x05\x06%L\x8d?\x1d*\x98:\x84IqxX\xca4\xebA\xd9\xd7u\x83p\x0e\x19+\x14\x86A\x8d\x19\xd6(\x12T\x8bmfr\x96\x14\x961\xa3i\\\xe3\x04<I,\xa5,\xc2@V\xf4\xcd\n\xabbi\x89\x14J\u05cc\v\xdd\u02fdB\xac\x1c,j\xe1h\\$\xb2\xac\n\xd4&+\x1c\xad\xacd\xad[\x0f,M\xe9\x1aY\xd9:ei\xa9LT\x1f\xa2\xa51\xadk\xbel\xb4\r\xc0\xd0,\xbct-\x8a\xee\x8e\xee\xcd\xfa`\xee8\xe5\x99\xc1B\x83\xac\xb0f6\x12+\x1d\x87\a\a\xa4\xfa|\x85\nAcY\x15L\xa3\x02V\xa3\xb9\x00A\xb7\xa1%,\x11\x1a\xc13\x8et/\xc0\xb4\u0245ZJ\r2\x03\xbd\u228c$Rd<o\xec\tqh\x0e0\xf7\xc5E\xd5\xd84\tFI\x13\f\xcab?J\x1a\xa4\x8c9!z\x1c\xc7a\x10\xac\xc3 (P\xc3)\x1cZ\xf1!\x1c\x93[\vF\xb8L\x99di\x90C\xa7a\u007f\xb4r\xae$\re-U\x9a\x8aU\xb2\u00929gH\x17O5\neS\xc2HG\xf1/J\x8a\xc8}MJ\x98\xa2a\x8d\x96]8k\xabr\xc6\xca\xe2\xb2*\x97\xd3XS\xd9\axJ\xd9\xf5Y\xc0M\x04\x1fA\xfc\xe4\xce\x1c\xe6\x0e\xd5\xfdY\u0327\xcc)\xe6'w>\x83:\xd5s\x8f\xf9:\fdS\xe96q\xacW\xf7\xef^\xbd[\xf7\xef^\xd6/|M\x9d\xe0\u07e7\xf3\u0263\x87W\x1f\u01a3\x87\x9f\t#\xe3T\xf6\xc38R\xcc\xfeS\x18w\xef=\xbe\u007f\xe5\xa5i\xac^\xb2>\xdbQ\xf7\xa4-S(Y\xa5\xecX\xe9K\x97\x1a\x99k\x8c\x96U\xd5\xd4\x105\xa7>8\xa9\xf0\x93\xbe\ub784AD+BdH4r\xe9+\xec[\x80#\xd2WK\xa5\xa2\xed\xa9\x05\x91\x8b\u0509\x8f\xc9b\x9e\xecZ\x853B_a\xd7\r\xa6T}\xaa\aT\x8d\xa7\x9a\xa8\xb9\xb4DK\xcd%\u046aZj\u0679f\xbe\u0080\xda\xff\xb3\xa3g\v\xa0\xc3\x15\xfez\u06d0\"c\xa8U\xe8,WK\u01ed\x96Q\a\x91\xe1.\xb9\xa8\x96\u0760o\xb7\x1b\xe0\"\u525d)\x16t\xbaA\xa6\xcd`\xaa\xb1\xaaQ\xa1\xa0]\x03\x18]G^\xb32\x0e\xbb\xddh\x017\x0e\xa3\u021a\x140\u078a E\x8du9\xd8\"\x12\xac5\u38b5\x03j%\x9b\"\xa5\xd95\xda%\x0e\x0e\u0a6c\xa1\xdd?o\x83\xa9\uf49dM$\x81\xd1\x1cUI\u0357\xd6?\x9bu\xb7\xe1\u034a'+\xe0Za\x91\x99\xb9\xc7\x04\xa9&R\xbc\xc6Z\u06c1\xc9\xe0\xf1\xcfO\x9cF\x1cN\x16\xbanG3k\xdcp\xafs\xf4\xac\xdd'\xbb\xc2\x18\xaeVQ&\xa5\xcd=\xbb\x19Z\x03\x91=,rW@wb\xab \x91eI\vU\xc1\x05Z\xb2\x96\xdb\xf9O\fs\xa45c\x8b\xceZ\xef,S\xa9\xe55\xabV#\xae\xa1Xf\xca\xf2\x11+ey\xcb\xd0l\xc2\xd1\u03a0\xa9\xeb\xb7\xe1\xb0\ub626c\x98\x14\xe5\x16\u05c5\xee\xd8\xc5,\xbf\xb0\x02T\"[|S[\x86m\xd2{\x8bo\x93\xde\bP\xaa\u06f4\x8f\x16\xd0u\xa4\xbe4\xac\x84I}*\x87^\x82HV\x80d\xb7\x8e b\u0521a\xae\xafG$\xdfr\xc9\xfeE\xb4\x1c\x93\xd6t\x11\x88\x88\xd8]`\x10\x14\xcc\x1c\x92S\x10\xae\u0553\xea\x95Xm_\x17\xce.m \x96\xbf\xa5n\x96\x93\x99\x03G\x8b\x87\xbb\xc4a\xd2m\x19\xea\x05.bNV(X\xc5?b\xcbq/`\u0216\x91\x997\xddk\xc5\xcd\x1d\xea]\xac(,3\x86c\r\xa9D\x05Bj\xe0\")\x9a\x14\xed[I\xd6%\x1c\x1f\u0161\x913\x0e\x99\x97\x1a\xbdI\x0f\xbb\xe7\u06a0\xc4\xd7f\xee\x9c\xcc\xd5`\xf7\xcai\x8b\x11\xce!2\x13\xdd\xfcjkp\xf2\x88\x98.\r\xe3\xa7\xc8t\x12\x8f\x1f>S\xee\xf8\ttk\xc4\xfe\x16\xbe\x99R\xc2`\xf2@\x9a\xda\x1b?\x95\xa6\xdc\xf1\x03i\xc2]S7\x14\xed\x0e6\\\t\xb6\xf0r\x18m\x9d7\x1fUo\u007f\xab\xcd\xf5\x17`\xb1&\u0529\xbd\xd9\xff\xa6t'\x0fR\xf2y\v\xf3y\xac?\xe9\xcd\x04\xc7y\xfc\xe6q\xeb\xe3\x19uf\x15\x9b\x18\x06\xb1\xdd8\xecS\xa8}\x1c\x0f\x95\x87\u075b\x16\xe2|\x8a\u02cdC\xd7\xec\xc7\u07b6n\x8d^\xe3]\\\xc3W\xf8l\x00\xb3\xb8t~\xad\xc3\xf1\x8e\xd8M\x92\xb6\b\xfa\b\xfa9\u04af\xf4\x93j\xb1E\x02\xe7\xed\xbd\r\x17\xda\u058f\xe1\x1e\xdb\x1b\xef\x87\xcc\x18\u0711\x1bT\x86\xd6\xf2xn\xcd\xfa\xd3\t\xf6\xc3cV\xae\xf7A\xcb\xf2S\x06{\xc1\xc1\u021b\x14\xce\xfc\x04\xec\a\xc7D|\xcb\xf4:\x1cw\u06cbw<\xbbd\xdbQ2>e:\x1b>\xea\xf2'\xa7\xc0E\xb5\u05a1\xe7\xfd\x13\x00\x00\xff\xffC\xa9X9\xac\x14\x00\x00")