internal/core/export: initial commit

Change-Id: Ib388135cc653bdb21d8bc72bea0983df429ec9c2
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6504
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/core/export/bounds.go b/internal/core/export/bounds.go
new file mode 100644
index 0000000..71788a9
--- /dev/null
+++ b/internal/core/export/bounds.go
@@ -0,0 +1,213 @@
+// 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.
+
+package export
+
+import (
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/internal/core/adt"
+	"github.com/cockroachdb/apd/v2"
+)
+
+// boundSimplifier simplifies bound values into predeclared identifiers, if
+// possible.
+type boundSimplifier struct {
+	e *exporter
+
+	isInt  bool
+	min    *adt.BoundValue
+	minNum *adt.Num
+	max    *adt.BoundValue
+	maxNum *adt.Num
+}
+
+func (s *boundSimplifier) add(v adt.Value) (used bool) {
+	switch x := v.(type) {
+	case *adt.BasicType:
+		switch x.K & adt.ScalarKinds {
+		case adt.IntKind:
+			s.isInt = true
+			return true
+		}
+
+	case *adt.BoundValue:
+		if adt.IsConcrete(x.Value) && x.Kind() == adt.IntKind {
+			s.isInt = true
+		}
+		switch x.Op {
+		case adt.GreaterThanOp:
+			if n, ok := x.Value.(*adt.Num); ok {
+				if s.min == nil || s.minNum.X.Cmp(&n.X) != 1 {
+					s.min = x
+					s.minNum = n
+				}
+				return true
+			}
+
+		case adt.GreaterEqualOp:
+			if n, ok := x.Value.(*adt.Num); ok {
+				if s.min == nil || s.minNum.X.Cmp(&n.X) == -1 {
+					s.min = x
+					s.minNum = n
+				}
+				return true
+			}
+
+		case adt.LessThanOp:
+			if n, ok := x.Value.(*adt.Num); ok {
+				if s.max == nil || s.maxNum.X.Cmp(&n.X) != -1 {
+					s.max = x
+					s.maxNum = n
+				}
+				return true
+			}
+
+		case adt.LessEqualOp:
+			if n, ok := x.Value.(*adt.Num); ok {
+				if s.max == nil || s.maxNum.X.Cmp(&n.X) == 1 {
+					s.max = x
+					s.maxNum = n
+				}
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+type builtinRange struct {
+	typ string
+	lo  *apd.Decimal
+	hi  *apd.Decimal
+}
+
+func makeDec(s string) *apd.Decimal {
+	d, _, err := apd.NewFromString(s)
+	if err != nil {
+		panic(err)
+	}
+	return d
+}
+
+func (s *boundSimplifier) expr(ctx *adt.OpContext) (e ast.Expr) {
+	if s.min == nil || s.max == nil {
+		return nil
+	}
+	switch {
+	case s.isInt:
+		t := s.matchRange(intRanges)
+		if t != "" {
+			e = ast.NewIdent(t)
+			break
+		}
+		if sign := s.minNum.X.Sign(); sign == -1 {
+			e = ast.NewIdent("int")
+
+		} else {
+			e = ast.NewIdent("uint")
+			if sign == 0 && s.min.Op == adt.GreaterEqualOp {
+				s.min = nil
+				break
+			}
+		}
+		fallthrough
+	default:
+		t := s.matchRange(floatRanges)
+		if t != "" {
+			e = wrapBin(e, ast.NewIdent(t), adt.AndOp)
+		}
+	}
+
+	if s.min != nil {
+		e = wrapBin(e, s.e.expr(s.min), adt.AndOp)
+	}
+	if s.max != nil {
+		e = wrapBin(e, s.e.expr(s.max), adt.AndOp)
+	}
+	return e
+}
+
+func (s *boundSimplifier) matchRange(ranges []builtinRange) (t string) {
+	for _, r := range ranges {
+		if !s.minNum.X.IsZero() && s.min.Op == adt.GreaterEqualOp && s.minNum.X.Cmp(r.lo) == 0 {
+			switch s.maxNum.X.Cmp(r.hi) {
+			case 0:
+				if s.max.Op == adt.LessEqualOp {
+					s.max = nil
+				}
+				s.min = nil
+				return r.typ
+			case -1:
+				if !s.minNum.X.IsZero() {
+					s.min = nil
+					return r.typ
+				}
+			case 1:
+			}
+		} else if s.max.Op == adt.LessEqualOp && s.maxNum.X.Cmp(r.hi) == 0 {
+			switch s.minNum.X.Cmp(r.lo) {
+			case -1:
+			case 0:
+				if s.min.Op == adt.GreaterEqualOp {
+					s.min = nil
+				}
+				fallthrough
+			case 1:
+				s.max = nil
+				return r.typ
+			}
+		}
+	}
+	return ""
+}
+
+var intRanges = []builtinRange{
+	{"int8", makeDec("-128"), makeDec("127")},
+	{"int16", makeDec("-32768"), makeDec("32767")},
+	{"int32", makeDec("-2147483648"), makeDec("2147483647")},
+	{"int64", makeDec("-9223372036854775808"), makeDec("9223372036854775807")},
+	{"int128", makeDec("-170141183460469231731687303715884105728"),
+		makeDec("170141183460469231731687303715884105727")},
+
+	{"uint8", makeDec("0"), makeDec("255")},
+	{"uint16", makeDec("0"), makeDec("65535")},
+	{"uint32", makeDec("0"), makeDec("4294967295")},
+	{"uint64", makeDec("0"), makeDec("18446744073709551615")},
+	{"uint128", makeDec("0"), makeDec("340282366920938463463374607431768211455")},
+
+	// {"rune", makeDec("0"), makeDec(strconv.Itoa(0x10FFFF))},
+}
+
+var floatRanges = []builtinRange{
+	// 2**127 * (2**24 - 1) / 2**23
+	{"float32",
+		makeDec("-3.40282346638528859811704183484516925440e+38"),
+		makeDec("3.40282346638528859811704183484516925440e+38")},
+
+	// 2**1023 * (2**53 - 1) / 2**52
+	{"float64",
+		makeDec("-1.797693134862315708145274237317043567981e+308"),
+		makeDec("1.797693134862315708145274237317043567981e+308")},
+}
+
+func wrapBin(a, b ast.Expr, op adt.Op) ast.Expr {
+	if a == nil {
+		return b
+	}
+	if b == nil {
+		return a
+	}
+	return ast.NewBinExpr(op.Token(), a, b)
+}
diff --git a/internal/core/export/export.go b/internal/core/export/export.go
new file mode 100644
index 0000000..76bbe8a
--- /dev/null
+++ b/internal/core/export/export.go
@@ -0,0 +1,184 @@
+// 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.
+
+package export
+
+import (
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/ast/astutil"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/internal/core/adt"
+	"cuelang.org/go/internal/core/eval"
+)
+
+type Profile struct {
+	Simplify bool
+
+	// TODO:
+	// IncludeDocs
+}
+
+var Simplified = &Profile{
+	Simplify: true,
+}
+
+var Raw = &Profile{}
+
+// Concrete
+
+// Def exports v as a definition.
+func Def(r adt.Runtime, v *adt.Vertex) (*ast.File, errors.Error) {
+	p := Profile{}
+	return p.Def(r, v)
+}
+
+// Def exports v as a definition.
+func (p *Profile) Def(r adt.Runtime, v *adt.Vertex) (*ast.File, errors.Error) {
+	e := newExporter(p, r, v)
+	expr := e.expr(v)
+	return e.toFile(expr)
+}
+
+// // TODO: remove: must be able to fall back to arcs if there are no
+// // conjuncts.
+// func Conjuncts(conjuncts ...adt.Conjunct) (*ast.File, errors.Error) {
+// 	var e Exporter
+// 	// for now just collect and turn into an big conjunction.
+// 	var a []ast.Expr
+// 	for _, c := range conjuncts {
+// 		a = append(a, e.expr(c.Expr()))
+// 	}
+// 	return e.toFile(ast.NewBinExpr(token.AND, a...))
+// }
+
+func Expr(r adt.Runtime, n adt.Expr) (ast.Expr, errors.Error) {
+	return Simplified.Expr(r, n)
+}
+
+func (p *Profile) Expr(r adt.Runtime, n adt.Expr) (ast.Expr, errors.Error) {
+	e := newExporter(p, r, nil)
+	return e.expr(n), nil
+}
+
+func (e *exporter) toFile(x ast.Expr) (*ast.File, errors.Error) {
+	f := &ast.File{}
+
+	switch st := x.(type) {
+	case nil:
+		panic("null input")
+
+	case *ast.StructLit:
+		f.Decls = st.Elts
+
+	default:
+		f.Decls = append(f.Decls, &ast.EmbedDecl{Expr: x})
+	}
+
+	if err := astutil.Sanitize(f); err != nil {
+		err := errors.Promote(err, "export")
+		return f, errors.Append(e.errs, err)
+	}
+
+	return f, nil
+}
+
+// File
+
+func Vertex(r adt.Runtime, n *adt.Vertex) (*ast.File, errors.Error) {
+	return Simplified.Vertex(r, n)
+}
+
+func (p *Profile) Vertex(r adt.Runtime, n *adt.Vertex) (*ast.File, errors.Error) {
+	e := exporter{
+		cfg:   p,
+		index: r,
+	}
+	v := e.value(n, n.Conjuncts...)
+
+	return e.toFile(v)
+}
+
+func Value(r adt.Runtime, n adt.Value) (ast.Expr, errors.Error) {
+	return Simplified.Value(r, n)
+}
+
+func (p *Profile) Value(r adt.Runtime, n adt.Value) (ast.Expr, errors.Error) {
+	e := exporter{
+		cfg:   p,
+		index: r,
+	}
+	v := e.value(n)
+	return v, e.errs
+}
+
+type exporter struct {
+	cfg      *Profile
+	errs     errors.Error
+	concrete bool
+
+	ctx *adt.OpContext
+
+	index adt.StringIndexer
+
+	// For resolving up references.
+	stack []frame
+}
+
+func newExporter(p *Profile, r adt.Runtime, v *adt.Vertex) *exporter {
+	return &exporter{
+		cfg:   p,
+		ctx:   eval.NewContext(r, v),
+		index: r,
+	}
+}
+
+type completeFunc func(scope *ast.StructLit, m adt.Node)
+
+type frame struct {
+	scope *ast.StructLit
+	todo  []completeFunc
+
+	// field to new field
+	mapped map[adt.Node]ast.Node
+}
+
+// func (e *Exporter) pushFrame(d *adt.StructLit, s *ast.StructLit) (saved []frame) {
+// 	saved := e.stack
+// 	e.stack = append(e.stack, frame{scope: s, mapped: map[adt.Node]ast.Node{}})
+// 	return saved
+// }
+
+// func (e *Exporter) popFrame(saved []frame) {
+// 	f := e.stack[len(e.stack)-1]
+
+// 	for _, f
+
+// 	e.stack = saved
+// }
+
+// func (e *Exporter) promise(upCount int32, f completeFunc) {
+// 	e.todo = append(e.todo, f)
+// }
+
+func (e *exporter) errf(format string, args ...interface{}) *ast.BottomLit {
+	err := &exporterError{}
+	e.errs = errors.Append(e.errs, err)
+	return &ast.BottomLit{}
+}
+
+type errTODO errors.Error
+
+type exporterError struct {
+	errTODO
+}
diff --git a/internal/core/export/export_test.go b/internal/core/export/export_test.go
new file mode 100644
index 0000000..8c7bc83
--- /dev/null
+++ b/internal/core/export/export_test.go
@@ -0,0 +1,94 @@
+// 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.
+
+package export
+
+import (
+	"flag"
+	"testing"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/format"
+	"cuelang.org/go/internal/core/compile"
+	"cuelang.org/go/internal/core/runtime"
+	"cuelang.org/go/internal/cuetxtar"
+	"github.com/rogpeppe/go-internal/txtar"
+)
+
+var update = flag.Bool("update", false, "update the test files")
+
+func TestDefinition(t *testing.T) {
+	test := cuetxtar.TxTarTest{
+		Root:   "./testdata",
+		Name:   "definition",
+		Update: *update,
+	}
+
+	r := runtime.New()
+
+	test.Run(t, func(t *cuetxtar.Test) {
+		a := t.ValidInstances()
+
+		v, errs := compile.Files(nil, r, a[0].Files...)
+		if errs != nil {
+			t.Fatal(errs)
+		}
+
+		file, errs := Def(r, v)
+		errors.Print(t, errs, nil)
+		_, _ = t.Write(formatNode(t.T, file))
+	})
+}
+
+func formatNode(t *testing.T, n ast.Node) []byte {
+	t.Helper()
+
+	b, err := format.Node(n)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return b
+}
+
+// For debugging purposes. Do not delete.
+func TestX(t *testing.T) {
+	t.Skip()
+
+	in := `
+-- in.cue --
+package test
+
+import pkg2 "example.com/foo/pkg1"
+#pkg1: pkg2.Object
+
+"Hello \(#pkg1)!"
+	`
+
+	archive := txtar.Parse([]byte(in))
+	a := cuetxtar.Load(archive, "/tmp/test")
+
+	r := runtime.New()
+	v, errs := compile.Files(nil, r, a[0].Files...)
+	if errs != nil {
+		t.Fatal(errs)
+	}
+
+	file, errs := Def(r, v)
+	if errs != nil {
+		t.Fatal(errs)
+	}
+
+	t.Error(string(formatNode(t, file)))
+}
diff --git a/internal/core/export/expr.go b/internal/core/export/expr.go
new file mode 100644
index 0000000..c73e556
--- /dev/null
+++ b/internal/core/export/expr.go
@@ -0,0 +1,269 @@
+// 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.
+
+package export
+
+import (
+	"sort"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/core/adt"
+)
+
+// Modes:
+//   raw: as is
+//   def: merge structs, print reset as is.
+//
+// Possible simplifications in def mode:
+//    - merge contents of multiple _literal_ structs.
+//      - this is not possible if some of the elements are bulk optional
+//        (or is it?).
+//    - still do not ever resolve references.
+//    - to do this, fields must be pre-linked to their destinations.
+//    - use astutil.Sanitize to resolve shadowing and imports.
+//
+//
+// Categories of printing:
+//   - concrete
+//   - optionals
+//   - references
+//   - constraints
+//
+// Mixed mode is also not supported in the old implementation (at least not
+// correctly). It requires references to resolve properly, backtracking to
+// a common root and prefixing that to the reference. This is now possible
+// with the Environment construct and could be done later.
+
+func (e *exporter) expr(v adt.Expr) (result ast.Expr) {
+	switch x := v.(type) {
+	case nil:
+		return nil
+
+	case *adt.Vertex:
+		if len(x.Conjuncts) == 0 {
+			// Treat as literal value.
+			return e.value(x)
+		}
+		return e.mergeValues(x.Conjuncts...)
+
+	case *adt.StructLit:
+		return e.mergeValues(adt.MakeConjunct(nil, x))
+
+	case adt.Value:
+		e.value(x)
+
+	default:
+		if f, ok := x.Source().(*ast.File); ok {
+			return &ast.StructLit{Elts: f.Decls}
+		}
+
+		return v.Source().(ast.Expr)
+	}
+	return nil
+}
+
+// Piece out values:
+
+// For a struct, piece out conjuncts that are already values. Those can be
+// unified. All other conjuncts are added verbatim.
+
+func (x *exporter) mergeValues(a ...adt.Conjunct) ast.Expr {
+	e := conjuncts{
+		exporter: x,
+		values:   &adt.Vertex{},
+		fields:   map[adt.Feature][]adt.Conjunct{},
+	}
+
+	for _, c := range a {
+		e.addExpr(c.Env, c.Expr())
+	}
+
+	// Unify values only for one level.
+	if len(e.values.Conjuncts) > 0 {
+		e.values.Finalize(e.ctx)
+		e.exprs = append(e.exprs, e.value(e.values, e.values.Conjuncts...))
+	}
+
+	// Collect and order set of fields.
+	fields := []adt.Feature{}
+	for f := range e.fields {
+		fields = append(fields, f)
+	}
+	m := sortArcs(e.exporter.extractFeatures(e.structs))
+	sort.SliceStable(fields, func(i, j int) bool {
+		if m[fields[i]] == 0 {
+			return m[fields[j]] != 0
+		}
+		return m[fields[i]] > m[fields[j]]
+	})
+
+	if len(e.fields) == 0 && !e.hasEllipsis {
+		switch len(e.exprs) {
+		case 0:
+			return ast.NewIdent("_")
+		case 1:
+			return e.exprs[0]
+		case 2:
+			// Simplify.
+			return ast.NewBinExpr(token.AND, e.exprs...)
+		}
+	}
+
+	s := &ast.StructLit{}
+	for _, x := range e.exprs {
+		s.Elts = append(s.Elts, &ast.EmbedDecl{Expr: x})
+	}
+
+	for _, f := range fields {
+		c := e.fields[f]
+		merged := e.mergeValues(c...)
+		label := e.stringLabel(f)
+		d := &ast.Field{Label: label, Value: merged}
+		if isOptional(c) {
+			d.Optional = token.Blank.Pos()
+		}
+		s.Elts = append(s.Elts, d)
+	}
+	if e.hasEllipsis {
+		s.Elts = append(s.Elts, &ast.Ellipsis{})
+	}
+
+	return s
+}
+
+// A conjuncts collects values of a single vertex.
+type conjuncts struct {
+	*exporter
+	// Values is used to collect non-struct values.
+	values      *adt.Vertex
+	exprs       []ast.Expr
+	structs     []*adt.StructLit
+	fields      map[adt.Feature][]adt.Conjunct
+	hasEllipsis bool
+}
+
+func (e *conjuncts) addExpr(env *adt.Environment, x adt.Expr) {
+	switch x := x.(type) {
+	case *adt.StructLit:
+		// Only add if it only has no bulk fields or elipsis.
+		if isComplexStruct(x) {
+			switch src := x.Src.(type) {
+			case nil:
+				panic("now allowed")
+			case *ast.StructLit:
+				e.exprs = append(e.exprs, src)
+			case *ast.File:
+				e.exprs = append(e.exprs, &ast.StructLit{Elts: src.Decls})
+			}
+			return
+		}
+		// Used for sorting.
+		e.structs = append(e.structs, x)
+
+		for _, d := range x.Decls {
+			var label adt.Feature
+			switch f := d.(type) {
+			case *adt.Field:
+				label = f.Label
+			case *adt.OptionalField:
+				label = f.Label
+			case *adt.Ellipsis:
+				e.hasEllipsis = true
+			case adt.Expr:
+				e.addExpr(env, f)
+				continue
+
+				// TODO: also handle dynamic fields
+			default:
+				panic("unreachable")
+			}
+			c := adt.MakeConjunct(env, d)
+			e.fields[label] = append(e.fields[label], c)
+		}
+
+	case adt.Value: // other values.
+		if v, ok := x.(*adt.Vertex); ok {
+			// if !v.IsList() {
+			// 	panic("what to do?")
+			// }
+			// generated, only consider arcs.
+			e.exprs = append(e.exprs, e.value(v, v.Conjuncts...))
+			return
+		}
+
+		e.values.AddConjunct(adt.MakeConjunct(env, x))
+
+	case *adt.BinaryExpr:
+		switch {
+		case x.Op == adt.AndOp:
+			e.addExpr(env, x.X)
+			e.addExpr(env, x.Y)
+		case isSelfContained(x):
+			e.values.AddConjunct(adt.MakeConjunct(env, x))
+		default:
+			e.exprs = append(e.exprs, e.expr(x))
+		}
+
+	default:
+		if isSelfContained(x) {
+			e.values.AddConjunct(adt.MakeConjunct(env, x))
+		} else {
+			e.exprs = append(e.exprs, e.expr(x))
+		}
+	}
+}
+
+func isOptional(a []adt.Conjunct) bool {
+	for _, c := range a {
+		switch f := c.Source().(type) {
+		case *ast.Field:
+			if f.Optional == token.NoPos {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+func isComplexStruct(s *adt.StructLit) bool {
+	for _, e := range s.Decls {
+		switch x := e.(type) {
+		case *adt.Field, *adt.OptionalField, adt.Expr:
+
+		case *adt.Ellipsis:
+			if x.Value != nil {
+				return true
+			}
+
+		default:
+			return true
+		}
+	}
+	return false
+}
+
+func isSelfContained(expr adt.Expr) bool {
+	switch x := expr.(type) {
+	case *adt.BinaryExpr:
+		return isSelfContained(x.X) && isSelfContained(x.Y)
+	case *adt.UnaryExpr:
+		return isSelfContained(x.X)
+	case *adt.BoundExpr:
+		return isSelfContained(x.Expr)
+	case adt.Value:
+		return true
+	}
+	return false
+}
diff --git a/internal/core/export/extract.go b/internal/core/export/extract.go
new file mode 100644
index 0000000..c3b11ef
--- /dev/null
+++ b/internal/core/export/extract.go
@@ -0,0 +1,149 @@
+// 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.
+
+package export
+
+import (
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/core/adt"
+)
+
+// ExtractDoc collects documentation strings for a field.
+//
+// Comments are attached to a field with a field shorthand belong to the
+// child node. So in the following the comment is attached to field bar.
+//
+//     // comment
+//     foo: bar: 2
+//
+func ExtractDoc(v *adt.Vertex) (docs []*ast.CommentGroup) {
+	fields := []*ast.Field{}
+
+	// Collect docs directly related to this Vertex.
+	for _, x := range v.Conjuncts {
+		f, ok := x.Source().(*ast.Field)
+		if !ok || hasShorthandValue(f) {
+			continue
+		}
+
+		fields = append(fields, f)
+		for _, cg := range f.Comments() {
+			if !containsDoc(docs, cg) && cg.Doc {
+				docs = append(docs, cg)
+			}
+		}
+	}
+
+	// Collect docs from parent scopes in collapsed fields.
+	for p := v.Parent; p != nil; p = p.Parent {
+
+		newFields := []*ast.Field{}
+
+		for _, x := range p.Conjuncts {
+			f, ok := x.Source().(*ast.Field)
+			if !ok || !hasShorthandValue(f) {
+				continue
+			}
+
+			nested := nestedField(f)
+			for _, child := range fields {
+				if nested == child {
+					newFields = append(newFields, f)
+					for _, cg := range f.Comments() {
+						if !containsDoc(docs, cg) && cg.Doc {
+							docs = append(docs, cg)
+						}
+					}
+				}
+			}
+		}
+
+		fields = newFields
+	}
+	return docs
+}
+
+// hasShorthandValue reports whether this field has a struct value that will
+// be rendered as a shorthand, for instance:
+//
+//     f: g: 2
+//
+func hasShorthandValue(f *ast.Field) bool {
+	if f = nestedField(f); f == nil {
+		return false
+	}
+
+	// Not a regular field, but shorthand field.
+	// TODO: Should we return here? For now mimic old implementation.
+	if _, _, err := ast.LabelName(f.Label); err != nil {
+		return false
+	}
+
+	return true
+}
+
+// nestedField returns the child field of a field shorthand.
+func nestedField(f *ast.Field) *ast.Field {
+	s, _ := f.Value.(*ast.StructLit)
+	if s == nil ||
+		len(s.Elts) != 1 ||
+		s.Lbrace != token.NoPos ||
+		s.Rbrace != token.NoPos {
+		return nil
+	}
+
+	f, _ = s.Elts[0].(*ast.Field)
+	return f
+}
+
+func containsDoc(a []*ast.CommentGroup, cg *ast.CommentGroup) bool {
+	for _, c := range a {
+		if c == cg {
+			return true
+		}
+	}
+
+	for _, c := range a {
+		if c.Text() == cg.Text() {
+			return true
+		}
+	}
+
+	return false
+}
+
+func ExtractFieldAttrs(a []adt.Conjunct) (attrs []*ast.Attribute) {
+	for _, x := range a {
+		f, ok := x.Source().(*ast.Field)
+		if !ok {
+			continue
+		}
+		for _, a := range f.Attrs {
+			if !containsAttr(attrs, a) {
+				attrs = append(attrs, a)
+			}
+		}
+	}
+	return attrs
+}
+
+func containsAttr(a []*ast.Attribute, x *ast.Attribute) bool {
+	for _, e := range a {
+		if e.Text == x.Text {
+			return true
+		}
+	}
+	return false
+}
diff --git a/internal/core/export/extract_test.go b/internal/core/export/extract_test.go
new file mode 100644
index 0000000..354dae7
--- /dev/null
+++ b/internal/core/export/extract_test.go
@@ -0,0 +1,61 @@
+// 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.
+
+package export
+
+import (
+	"fmt"
+	"testing"
+
+	"cuelang.org/go/internal/core/adt"
+	"cuelang.org/go/internal/core/compile"
+	"cuelang.org/go/internal/core/eval"
+	"cuelang.org/go/internal/core/runtime"
+	"cuelang.org/go/internal/cuetxtar"
+)
+
+func TestExtract(t *testing.T) {
+	test := cuetxtar.TxTarTest{
+		Root:   "./testdata",
+		Name:   "doc",
+		Update: *update,
+	}
+
+	r := runtime.New()
+
+	test.Run(t, func(t *cuetxtar.Test) {
+		a := t.ValidInstances()
+
+		v, err := compile.Files(nil, r, a[0].Files...)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		ctx := eval.NewContext(r, v)
+		v.Finalize(ctx)
+
+		writeDocs(t, r, v, nil)
+	})
+}
+
+func writeDocs(t *cuetxtar.Test, r adt.Runtime, v *adt.Vertex, path []string) {
+	fmt.Fprintln(t, path)
+	for _, c := range ExtractDoc(v) {
+		fmt.Fprintln(t, "-", c.Text())
+	}
+
+	for _, a := range v.Arcs {
+		writeDocs(t, r, a, append(path, a.Label.SelectorString(r)))
+	}
+}
diff --git a/internal/core/export/label.go b/internal/core/export/label.go
new file mode 100644
index 0000000..0ece0a3
--- /dev/null
+++ b/internal/core/export/label.go
@@ -0,0 +1,34 @@
+// 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.
+
+package export
+
+import (
+	"strconv"
+	"strings"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/core/adt"
+)
+
+func (e *exporter) stringLabel(f adt.Feature) ast.Label {
+	str := f.SelectorString(e.index)
+	if strings.HasPrefix(str, "#") && !f.IsDef() ||
+		strings.HasPrefix(str, "_") && !f.IsHidden() ||
+		!ast.IsValidIdent(str) {
+		return ast.NewLit(token.STRING, strconv.Quote(str))
+	}
+	return &ast.Ident{Name: str}
+}
diff --git a/internal/core/export/quote.go b/internal/core/export/quote.go
new file mode 100644
index 0000000..05e95c5
--- /dev/null
+++ b/internal/core/export/quote.go
@@ -0,0 +1,122 @@
+// 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.
+
+package export
+
+import (
+	"strconv"
+	"strings"
+	"unicode/utf8"
+)
+
+// quote quotes the given string.
+func quote(str string, quote byte) string {
+	if strings.IndexByte(str, '\n') < 0 {
+		buf := []byte{quote}
+		buf = appendEscaped(buf, str, quote, true)
+		buf = append(buf, quote)
+		return string(buf)
+	}
+	buf := []byte{quote, quote, quote}
+	buf = append(buf, multiSep...)
+	buf = appendEscapeMulti(buf, str, quote)
+	buf = append(buf, quote, quote, quote)
+	return string(buf)
+}
+
+// TODO: consider the best indent strategy.
+const multiSep = "\n        "
+
+func appendEscapeMulti(buf []byte, str string, quote byte) []byte {
+	// TODO(perf)
+	a := strings.Split(str, "\n")
+	for _, s := range a {
+		buf = appendEscaped(buf, s, quote, true)
+		buf = append(buf, multiSep...)
+	}
+	return buf
+}
+
+const lowerhex = "0123456789abcdef"
+
+func appendEscaped(buf []byte, s string, quote byte, graphicOnly bool) []byte {
+	for width := 0; len(s) > 0; s = s[width:] {
+		r := rune(s[0])
+		width = 1
+		if r >= utf8.RuneSelf {
+			r, width = utf8.DecodeRuneInString(s)
+		}
+		if width == 1 && r == utf8.RuneError {
+			buf = append(buf, `\x`...)
+			buf = append(buf, lowerhex[s[0]>>4])
+			buf = append(buf, lowerhex[s[0]&0xF])
+			continue
+		}
+		buf = appendEscapedRune(buf, r, quote, graphicOnly)
+	}
+	return buf
+}
+
+func appendEscapedRune(buf []byte, r rune, quote byte, graphicOnly bool) []byte {
+	var runeTmp [utf8.UTFMax]byte
+	if r == rune(quote) || r == '\\' { // always backslashed
+		buf = append(buf, '\\')
+		buf = append(buf, byte(r))
+		return buf
+	}
+	// TODO(perf): IsGraphic calls IsPrint.
+	if strconv.IsPrint(r) || graphicOnly && strconv.IsGraphic(r) {
+		n := utf8.EncodeRune(runeTmp[:], r)
+		buf = append(buf, runeTmp[:n]...)
+		return buf
+	}
+	switch r {
+	case '\a':
+		buf = append(buf, `\a`...)
+	case '\b':
+		buf = append(buf, `\b`...)
+	case '\f':
+		buf = append(buf, `\f`...)
+	case '\n':
+		buf = append(buf, `\n`...)
+	case '\r':
+		buf = append(buf, `\r`...)
+	case '\t':
+		buf = append(buf, `\t`...)
+	case '\v':
+		buf = append(buf, `\v`...)
+	default:
+		switch {
+		case r < ' ':
+			// Invalid for strings, only bytes.
+			buf = append(buf, `\x`...)
+			buf = append(buf, lowerhex[byte(r)>>4])
+			buf = append(buf, lowerhex[byte(r)&0xF])
+		case r > utf8.MaxRune:
+			r = 0xFFFD
+			fallthrough
+		case r < 0x10000:
+			buf = append(buf, `\u`...)
+			for s := 12; s >= 0; s -= 4 {
+				buf = append(buf, lowerhex[r>>uint(s)&0xF])
+			}
+		default:
+			buf = append(buf, `\U`...)
+			for s := 28; s >= 0; s -= 4 {
+				buf = append(buf, lowerhex[r>>uint(s)&0xF])
+			}
+		}
+	}
+	return buf
+}
diff --git a/internal/core/export/testdata/docs.txtar b/internal/core/export/testdata/docs.txtar
new file mode 100644
index 0000000..63fd83a
--- /dev/null
+++ b/internal/core/export/testdata/docs.txtar
@@ -0,0 +1,208 @@
+
+-- in.cue --
+// foobar defines at least foo.
+package foobar
+
+// A Foo fooses stuff.
+Foo: {
+    // field1 is an int.
+    field1: int
+
+    field2: int
+
+    // duplicate field comment
+    dup3: int
+}
+
+// foos are instances of Foo.
+foos: [string]: Foo
+
+// My first little foo.
+foos: MyFoo: {
+    // local field comment.
+    field1: 0
+
+    // Dangling comment.
+
+    // other field comment.
+    field2: 1
+
+    // duplicate field comment
+    dup3: int
+}
+
+bar: {
+    // comment from bar on field 1
+    field1: int
+    // comment from bar on field 2
+    field2: int // don't include this
+}
+
+baz: bar & {
+    // comment from baz on field 1
+    field1: int
+    field2: int
+}
+
+-- out.txt --
+-- out/doc --
+[]
+[Foo]
+- A Foo fooses stuff.
+
+[Foo field1]
+- field1 is an int.
+
+[Foo field2]
+[Foo dup3]
+- duplicate field comment
+
+[foos]
+- foos are instances of Foo.
+
+[foos MyFoo]
+- My first little foo.
+
+[foos MyFoo field1]
+- local field comment.
+
+- field1 is an int.
+
+[foos MyFoo field2]
+- other field comment.
+
+[foos MyFoo dup3]
+- duplicate field comment
+
+[bar]
+[bar field1]
+- comment from bar on field 1
+
+[bar field2]
+- comment from bar on field 2
+
+[baz]
+[baz field1]
+- comment from bar on field 1
+
+- comment from baz on field 1
+
+[baz field2]
+- comment from bar on field 2
+
+-- out/definition --
+Foo: {
+	field1: int
+	field2: int
+	dup3:   int
+}
+foos: {
+	{
+		[string]: Foo_1
+	}
+	MyFoo: {
+		field1: 0
+		field2: 1
+		dup3:   int
+	}
+}
+bar: {
+	field1: int
+	field2: int
+}
+baz: {
+	bar_5
+	field1: int
+	field2: int
+}
+
+let Foo_1 = Foo
+
+let bar_5 = bar
+-- out/value --
+== Simplified
+{
+	// A Foo fooses stuff.
+	Foo: {
+		// field1 is an int.
+		field1: int
+		field2: int
+
+		// duplicate field comment
+		dup3: int
+	}
+
+	// foos are instances of Foo.
+	foos: {
+		// My first little foo.
+		MyFoo: {
+			// local field comment.
+
+			// field1 is an int.
+			field1: 0
+
+			// other field comment.
+			field2: 1
+
+			// duplicate field comment
+			dup3: int
+		}
+	}
+	bar: {
+		// comment from bar on field 1
+		field1: int
+		// comment from bar on field 2
+		field2: int
+	}
+	baz: {
+		// comment from bar on field 1
+
+		// comment from baz on field 1
+		field1: int
+		// comment from bar on field 2
+		field2: int
+	}
+}
+== Raw
+{
+	// A Foo fooses stuff.
+	Foo: {
+		// field1 is an int.
+		field1: int
+		field2: int
+
+		// duplicate field comment
+		dup3: int
+	}
+
+	// foos are instances of Foo.
+	foos: {
+		// My first little foo.
+		MyFoo: {
+			// local field comment.
+
+			// field1 is an int.
+			field1: 0
+
+			// other field comment.
+			field2: 1
+
+			// duplicate field comment
+			dup3: int
+		}
+	}
+	bar: {
+		// comment from bar on field 1
+		field1: int
+		// comment from bar on field 2
+		field2: int
+	}
+	baz: {
+		// comment from bar on field 1
+
+		// comment from baz on field 1
+		field1: int
+		// comment from bar on field 2
+		field2: int
+	}
+}
diff --git a/internal/core/export/testdata/scalardef.txtar b/internal/core/export/testdata/scalardef.txtar
new file mode 100644
index 0000000..3e5b5b3
--- /dev/null
+++ b/internal/core/export/testdata/scalardef.txtar
@@ -0,0 +1,28 @@
+cue eval ./pkg:foo
+
+-- cue.mod/module.cue --
+module: "example.com"
+
+-- in.cue --
+package test
+
+import pkg2 "example.com/foo/pkg1"
+#pkg1: pkg2.Object
+
+"Hello \(#pkg1)!"
+
+-- foo/pkg1/file.cue --
+package pkg1
+
+Object: "World"
+
+-- out/eval --
+(string){ "Hello World!" }
+-- out/doc --
+[]
+[#pkg1]
+-- out/definition --
+import pkg2 "example.com/foo/pkg1"
+
+"Hello \(#pkg1)!"
+#pkg1: pkg2.Object
diff --git a/internal/core/export/testdata/simplify.txtar b/internal/core/export/testdata/simplify.txtar
new file mode 100644
index 0000000..c8d27e8
--- /dev/null
+++ b/internal/core/export/testdata/simplify.txtar
@@ -0,0 +1,30 @@
+-- in.cue --
+
+x: [string]: int64
+x: {
+    y: int
+}
+-- out/definition --
+x: {
+	{
+		[string]: int64
+	}
+	y: int
+}
+-- out/doc --
+[]
+[x]
+[x y]
+-- out/value --
+== Simplified
+{
+	x: {
+		y: int64
+	}
+}
+== Raw
+{
+	x: {
+		y: >=-9223372036854775808 & <=9223372036854775807 & int
+	}
+}
diff --git a/internal/core/export/testdata/topo.txtar b/internal/core/export/testdata/topo.txtar
new file mode 100644
index 0000000..2d582aa
--- /dev/null
+++ b/internal/core/export/testdata/topo.txtar
@@ -0,0 +1,49 @@
+TODO: right now precedence goes to the first list. Perhaps we should give
+precedence to the longest list. In that case, `b` would be sorted before `a`.
+
+-- a.cue --
+a: 1
+c: 2
+e: 3
+g: 4
+
+-- b.cue --
+b: 1 // unanchored
+c: 2
+e: 3
+f: 4
+g: 4
+-- out/definition --
+a: 1
+b: 1
+c: 2
+e: 3
+f: 4
+g: 4
+-- out/doc --
+[]
+[a]
+[c]
+[e]
+[g]
+[b]
+[f]
+-- out/value --
+== Simplified
+{
+	a: 1
+	b: 1
+	c: 2
+	e: 3
+	f: 4
+	g: 4
+}
+== Raw
+{
+	a: 1
+	b: 1
+	c: 2
+	e: 3
+	f: 4
+	g: 4
+}
diff --git a/internal/core/export/toposort.go b/internal/core/export/toposort.go
new file mode 100644
index 0000000..436a521
--- /dev/null
+++ b/internal/core/export/toposort.go
@@ -0,0 +1,182 @@
+// 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.
+
+package export
+
+import (
+	"sort"
+
+	"cuelang.org/go/internal/core/adt"
+)
+
+// TODO: topological sort should go arguably in a more fundamental place as it
+// may be needed to sort inputs for comprehensions.
+
+// VertexFeatures returns the feature list of v. The list may include more
+// features than for which there are arcs and also includes features for
+// optional fields. It assumes the Structs fields is properly initialized.
+func VertexFeatures(v *adt.Vertex) []adt.Feature {
+	sets := extractFeatures(v.Structs)
+	m := sortArcs(sets) // TODO: use for convenience.
+
+	// Add features that are not in m. This may happen when fields were
+	// dynamically created.
+	var a []adt.Feature
+	for _, arc := range v.Arcs {
+		if _, ok := m[arc.Label]; !ok {
+			a = append(a, arc.Label)
+		}
+	}
+
+	sets = extractFeatures(v.Structs)
+	if len(a) > 0 {
+		sets = append(sets, a)
+	}
+
+	return sortedArcs(sets)
+}
+
+// func structFeatures(a []*adt.StructLit) []adt.Feature {
+// 	sets := extractFeatures(a)
+// 	return sortedArcs(sets)
+// }
+
+func (e *exporter) sortedArcs(v *adt.Vertex) (sorted []*adt.Vertex) {
+	a := extractFeatures(v.Structs)
+	if len(a) == 0 {
+		return v.Arcs
+	}
+
+	sorted = make([]*adt.Vertex, len(v.Arcs))
+	copy(sorted, v.Arcs)
+
+	m := sortArcs(a)
+	sort.SliceStable(sorted, func(i, j int) bool {
+		if m[sorted[i].Label] == 0 {
+			return m[sorted[j].Label] != 0
+		}
+		return m[sorted[i].Label] > m[sorted[j].Label]
+	})
+
+	return sorted
+}
+
+// TODO: remove
+func (e *exporter) extractFeatures(in []*adt.StructLit) (a [][]adt.Feature) {
+	return extractFeatures(in)
+}
+
+func extractFeatures(in []*adt.StructLit) (a [][]adt.Feature) {
+	for _, s := range in {
+		sorted := []adt.Feature{}
+		for _, e := range s.Decls {
+			switch x := e.(type) {
+			case *adt.Field:
+				sorted = append(sorted, x.Label)
+
+			case *adt.OptionalField:
+				sorted = append(sorted, x.Label)
+			}
+		}
+
+		// Lists with a single element may still be useful to distinguish
+		// between known and unknown fields: unknown fields are sorted last.
+		if len(sorted) > 0 {
+			a = append(a, sorted)
+		}
+	}
+	return a
+}
+
+// sortedArcs is like sortArcs, but returns a the features of optional and
+// required fields in an sorted slice. Ultimately, the implementation should
+// use merge sort everywhere, and this will be the preferred method. Also,
+// when querying optional fields as well, this helps identifying the optional
+// fields.
+func sortedArcs(fronts [][]adt.Feature) []adt.Feature {
+	m := sortArcs(fronts)
+	return sortedArcsFromMap(m)
+}
+
+func sortedArcsFromMap(m map[adt.Feature]int) []adt.Feature {
+	a := make([]adt.Feature, 0, len(m))
+
+	for k := range m {
+		a = append(a, k)
+	}
+
+	sort.Slice(a, func(i, j int) bool { return m[a[i]] > m[a[j]] })
+
+	return a
+}
+
+// sortArcs does a topological sort of arcs based on a variant of Kahn's
+// algorithm. See
+// https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/
+//
+// It returns a map from feature to int where the feature with the highest
+// number should be sorted first.
+func sortArcs(fronts [][]adt.Feature) map[adt.Feature]int {
+	counts := map[adt.Feature]int{}
+	for _, a := range fronts {
+		if len(a) <= 1 {
+			continue // no dependencies
+		}
+		for _, f := range a[1:] {
+			counts[f]++
+		}
+	}
+
+	// We could use a Heap instead of simple linear search here if we are
+	// concerned about the time complexity.
+
+	index := -1
+outer:
+	for {
+	lists:
+		for i, a := range fronts {
+			for len(a) > 0 {
+				f := a[0]
+				n := counts[f]
+				if n > 0 {
+					continue lists
+				}
+
+				// advance list and decrease dependency.
+				a = a[1:]
+				fronts[i] = a
+				if len(a) > 1 && counts[a[0]] > 0 {
+					counts[a[0]]--
+				}
+
+				if n == 0 { // may be head of other lists as well
+					counts[f] = index
+					index--
+				}
+				continue outer // progress
+			}
+		}
+
+		for _, a := range fronts {
+			if len(a) > 0 {
+				// Detected a cycle. Fire at will to make progress.
+				counts[a[0]] = 0
+				continue outer
+			}
+		}
+		break
+	}
+
+	return counts
+}
diff --git a/internal/core/export/toposort_test.go b/internal/core/export/toposort_test.go
new file mode 100644
index 0000000..adeb43a
--- /dev/null
+++ b/internal/core/export/toposort_test.go
@@ -0,0 +1,103 @@
+// 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.
+
+package export
+
+import (
+	"strings"
+	"testing"
+
+	"cuelang.org/go/internal/core/adt"
+	"cuelang.org/go/internal/core/runtime"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestSortArcs(t *testing.T) {
+	testCases := []struct {
+		desc string
+		in   string
+		out  string
+	}{{
+		desc: "empty",
+		in:   ``,
+		out:  ``,
+	}, {
+		desc: "multiple empty",
+		in:   `||`,
+		out:  ``,
+	}, {
+		desc: "single list",
+		in:   `a b c`,
+		out:  `a b c`,
+	}, {
+		desc: "several one-elem lists",
+		in:   `a|b|c`,
+		out:  `a b c`,
+	}, {
+		desc: "glue1",
+		in:   `a b c | g h i | c d e g`,
+		out:  `a b c d e g h i`,
+	}, {
+		desc: "glue2",
+		in:   `c d e g | a b c | g h i|`,
+		out:  `a b c d e g h i`,
+	}, {
+		desc: "interleaved, prefer first",
+		in:   `a b d h k | c d h i k l m`,
+		out:  `a b c d h i k l m`,
+	}, {
+		desc: "subsumed",
+		in:   `a b c d e f g h i j k | c e f | i j k`,
+		out:  `a b c d e f g h i j k`,
+	}, {
+		desc: "cycle, single list",
+		in:   `a b a`,
+		out:  `a b`,
+	}, {
+		desc: "cycle, across lists",
+		in:   `a b | b c | c a`,
+		out:  `a b c`,
+	}}
+
+	r := runtime.New()
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			fa := parseFeatures(r, tc.in)
+
+			keys := sortedArcs(fa)
+
+			want := parseFeatures(r, tc.out)[0]
+
+			if !cmp.Equal(keys, want) {
+				got := ""
+				for _, f := range keys {
+					got += " " + f.SelectorString(r)
+				}
+				t.Errorf("got: %s\nwant: %s", got, tc.out)
+			}
+		})
+	}
+}
+
+func parseFeatures(r adt.Runtime, s string) (res [][]adt.Feature) {
+	for _, v := range strings.Split(s, "|") {
+		a := []adt.Feature{}
+		for _, w := range strings.Fields(v) {
+			a = append(a, adt.MakeStringLabel(r, w))
+		}
+		res = append(res, a)
+	}
+	return res
+}
diff --git a/internal/core/export/value.go b/internal/core/export/value.go
new file mode 100644
index 0000000..bf9500a
--- /dev/null
+++ b/internal/core/export/value.go
@@ -0,0 +1,263 @@
+// 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.
+
+package export
+
+import (
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/ast/astutil"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/core/adt"
+)
+
+func (e *exporter) bareValue(v adt.Value) ast.Expr {
+	// TODO: allow a Value context wrapper.
+	a := &adt.Vertex{Value: v}
+	return e.vertex(a)
+}
+
+// TODO: if the original value was a single reference, we could replace the
+// value with a reference in graph mode.
+
+func (e *exporter) vertex(n *adt.Vertex) (result ast.Expr) {
+	switch n.Value.(type) {
+	case nil:
+		// bare
+	case *adt.StructMarker:
+		result = e.structComposite(n)
+
+	case *adt.ListMarker:
+		result = e.listComposite(n)
+
+	default:
+		result = e.value(n.Value, n.Conjuncts...)
+	}
+	return result
+}
+
+func (e *exporter) value(n adt.Value, a ...adt.Conjunct) (result ast.Expr) {
+	// Evaluate arc if needed?
+
+	// if e.concrete && !adt.IsConcrete(n.Value) {
+	// 	return e.errf("non-concrete value: %v", e.bareValue(n.Value))
+	// }
+
+	switch x := n.(type) {
+	case *adt.Bottom:
+		result = e.bottom(x)
+
+	case *adt.Null:
+		result = e.null(x)
+
+	case *adt.Bool:
+		result = e.bool(x)
+
+	case *adt.Num:
+		result = e.num(x, a)
+
+	case *adt.String:
+		result = e.string(x, a)
+
+	case *adt.Bytes:
+		result = e.bytes(x, a)
+
+	case *adt.BasicType:
+		result = e.basicType(x)
+
+	case *adt.Top:
+		result = ast.NewIdent("_")
+
+	case *adt.BoundValue:
+		result = e.boundValue(x)
+
+	case *adt.BuiltinValidator:
+		result = e.builtinValidator(x)
+
+	case *adt.Vertex:
+		result = e.vertex(x)
+
+	case *adt.Conjunction:
+		if len(x.Values) == 0 {
+			result = ast.NewIdent("_")
+			break
+		}
+
+		a := []ast.Expr{}
+		b := boundSimplifier{e: e}
+		for _, v := range x.Values {
+			if !e.cfg.Simplify || !b.add(v) {
+				a = append(a, e.bareValue(v))
+			}
+		}
+
+		if !e.cfg.Simplify {
+			return ast.NewBinExpr(token.AND, a...)
+		}
+
+		result = b.expr(e.ctx) // e.bareValue(x.Values[0])
+		for _, c := range a {
+			result = &ast.BinaryExpr{X: result, Op: token.AND, Y: c}
+		}
+	}
+
+	// TODO: Add comments from original.
+
+	return result
+}
+
+func (e *exporter) bottom(n *adt.Bottom) *ast.BottomLit {
+	err := &ast.BottomLit{}
+	if x := n.Err; x != nil {
+		msg := x.Error()
+		// if len(x.sub) > 0 {
+		// 	buf := strings.Builder{}
+		// 	for i, b := range x.sub {
+		// 		if i > 0 {
+		// 			buf.WriteString("; ")
+		// 			buf.WriteString(b.msg())
+		// 		}
+		// 	}
+		// 	msg = buf.String()
+		// }
+		comment := &ast.Comment{Text: "// " + msg}
+		err.AddComment(&ast.CommentGroup{
+			Line:     true,
+			Position: 2,
+			List:     []*ast.Comment{comment},
+		})
+	}
+	return err
+}
+
+func (e *exporter) null(n *adt.Null) *ast.BasicLit {
+	return &ast.BasicLit{Kind: token.NULL, Value: "null"}
+}
+
+func (e *exporter) bool(n *adt.Bool) (b *ast.BasicLit) {
+	return ast.NewBool(n.B)
+}
+
+func extractBasic(a []adt.Conjunct) *ast.BasicLit {
+	for _, v := range a {
+		if b, ok := v.Source().(*ast.BasicLit); ok {
+			return &ast.BasicLit{Kind: b.Kind, Value: b.Value}
+		}
+	}
+	return nil
+}
+
+func (e *exporter) num(n *adt.Num, orig []adt.Conjunct) *ast.BasicLit {
+	// TODO: take original formatting into account.
+	if b := extractBasic(orig); b != nil {
+		return b
+	}
+	kind := token.FLOAT
+	if n.K&adt.IntKind != 0 {
+		kind = token.INT
+	}
+	return &ast.BasicLit{Kind: kind, Value: n.X.String()}
+
+}
+
+func (e *exporter) string(n *adt.String, orig []adt.Conjunct) *ast.BasicLit {
+	// TODO: take original formatting into account.
+	if b := extractBasic(orig); b != nil {
+		return b
+	}
+	return &ast.BasicLit{
+		Kind:  token.STRING,
+		Value: quote(n.Str, '"'),
+	}
+}
+
+func (e *exporter) bytes(n *adt.Bytes, orig []adt.Conjunct) *ast.BasicLit {
+	// TODO: take original formatting into account.
+	if b := extractBasic(orig); b != nil {
+		return b
+	}
+	return &ast.BasicLit{
+		Kind:  token.STRING,
+		Value: quote(string(n.B), '\''),
+	}
+}
+
+func (e *exporter) basicType(n *adt.BasicType) ast.Expr {
+	// TODO: allow multi-bit types?
+	return ast.NewIdent(n.K.String())
+}
+
+func (e *exporter) boundValue(n *adt.BoundValue) ast.Expr {
+	return &ast.UnaryExpr{Op: n.Op.Token(), X: e.value(n.Value)}
+}
+
+func (e *exporter) builtin(x *adt.Builtin) ast.Expr {
+	if x.Package == 0 {
+		return ast.NewIdent(x.Name)
+	}
+	spec := ast.NewImport(nil, x.Package.StringValue(e.index))
+	info, _ := astutil.ParseImportSpec(spec)
+	ident := ast.NewIdent(info.Ident)
+	ident.Node = spec
+	return ast.NewSel(ident, x.Name)
+}
+
+func (e *exporter) builtinValidator(n *adt.BuiltinValidator) ast.Expr {
+	call := ast.NewCall(e.builtin(n.Builtin))
+	for _, a := range n.Args {
+		call.Args = append(call.Args, e.value(a))
+	}
+	return call
+}
+
+func (e *exporter) listComposite(v *adt.Vertex) ast.Expr {
+	l := &ast.ListLit{}
+	for _, a := range v.Arcs {
+		if !a.Label.IsInt() {
+			continue
+		}
+		elem := e.vertex(a)
+
+		docs := ExtractDoc(a)
+		ast.SetComments(elem, docs)
+
+		l.Elts = append(l.Elts, elem)
+	}
+	return l
+}
+
+func (e *exporter) structComposite(v *adt.Vertex) ast.Expr {
+	s := &ast.StructLit{}
+	saved := e.stack
+	e.stack = append(e.stack, frame{scope: s})
+	defer func() { e.stack = saved }()
+
+	for _, a := range e.sortedArcs(v) {
+		if a.Label.IsDef() || a.Label.IsHidden() {
+			continue
+		}
+
+		f := &ast.Field{
+			Label: e.stringLabel(a.Label),
+			Value: e.vertex(a),
+			Attrs: ExtractFieldAttrs(a.Conjuncts),
+		}
+
+		docs := ExtractDoc(a)
+		ast.SetComments(f, docs)
+
+		s.Elts = append(s.Elts, f)
+	}
+
+	return s
+}
diff --git a/internal/core/export/value_test.go b/internal/core/export/value_test.go
new file mode 100644
index 0000000..6b89513
--- /dev/null
+++ b/internal/core/export/value_test.go
@@ -0,0 +1,102 @@
+// 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.
+
+package export
+
+import (
+	"fmt"
+	"testing"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/internal/core/adt"
+	"cuelang.org/go/internal/core/compile"
+	"cuelang.org/go/internal/core/eval"
+	"cuelang.org/go/internal/core/runtime"
+	"cuelang.org/go/internal/cuetxtar"
+	"github.com/rogpeppe/go-internal/txtar"
+)
+
+var exclude = map[string]string{
+	"scalardef": "incomplete",
+}
+
+func TestValue(t *testing.T) {
+	test := cuetxtar.TxTarTest{
+		Root:   "./testdata",
+		Name:   "value",
+		Update: *update,
+		Skip:   exclude,
+	}
+
+	r := runtime.New()
+
+	test.Run(t, func(t *cuetxtar.Test) {
+		a := t.ValidInstances()
+
+		v, errs := compile.Files(nil, r, a[0].Files...)
+		if errs != nil {
+			t.Fatal(errs)
+		}
+
+		ctx := eval.NewContext(r, v)
+		v.Finalize(ctx)
+
+		for _, tc := range []struct {
+			name string
+			fn   func(r adt.Runtime, v adt.Value) (ast.Expr, errors.Error)
+		}{
+			{"Simplified", Simplified.Value},
+			{"Raw", Raw.Value},
+		} {
+			fmt.Fprintln(t, "==", tc.name)
+			x, errs := tc.fn(r, v)
+			errors.Print(t, errs, nil)
+			_, _ = t.Write(formatNode(t.T, x))
+			fmt.Fprintln(t)
+		}
+	})
+}
+
+// For debugging purposes. Do not delete.
+func TestValueX(t *testing.T) {
+	t.Skip()
+
+	in := `
+-- in.cue --
+x: [string]: int64
+x: {
+    y: int
+}
+	`
+
+	archive := txtar.Parse([]byte(in))
+	a := cuetxtar.Load(archive, "/tmp/test")
+
+	r := runtime.New()
+	v, errs := compile.Files(nil, r, a[0].Files...)
+	if errs != nil {
+		t.Fatal(errs)
+	}
+
+	ctx := eval.NewContext(r, v)
+	v.Finalize(ctx)
+
+	x, errs := Value(r, v)
+	if errs != nil {
+		t.Fatal(errs)
+	}
+
+	t.Error(string(formatNode(t, x)))
+}