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)))
+}