cue/format: replace bulk labels with ellipsis if possible

Change-Id: I1c46c97e82c993613f2b5731e956c1bf40b948a1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5080
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/export.go b/cue/export.go
index cc79bd5..9abf05c 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -27,6 +27,7 @@
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
 )
 
 func doEval(m options) bool {
@@ -218,6 +219,9 @@
 
 func hasTemplate(s *ast.StructLit) bool {
 	for _, e := range s.Elts {
+		if _, ok := e.(*ast.Ellipsis); ok {
+			return true
+		}
 		if f, ok := e.(*ast.Field); ok {
 			label := f.Label
 			if _, ok := label.(*ast.TemplateLabel); ok {
@@ -692,7 +696,7 @@
 	return st
 }
 
-func (p *exporter) optionals(st *ast.StructLit, x *optionals) {
+func (p *exporter) optionals(st *ast.StructLit, x *optionals) (skippedEllipsis bool) {
 	switch x.op {
 	default:
 		for _, t := range x.fields {
@@ -705,10 +709,15 @@
 			if c, ok := v.(*closeIfStruct); ok {
 				v = c.value
 			}
-			st.Elts = append(st.Elts, &ast.Field{
+			f := &ast.Field{
 				Label: p.mkTemplate(t.key, p.identifier(l.params.arcs[0].feature)),
 				Value: p.expr(l.value),
-			})
+			}
+			if internal.IsEllipsis(f) {
+				skippedEllipsis = true
+				continue
+			}
+			st.Elts = append(st.Elts, f)
 		}
 
 	case opUnify:
@@ -731,6 +740,7 @@
 		st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: left})
 		st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: right})
 	}
+	return skippedEllipsis
 }
 
 func (p *exporter) structure(x *structLit, addTempl bool) (ret *ast.StructLit, err *bottom) {
@@ -753,11 +763,12 @@
 	if x.emit != nil {
 		obj.Elts = append(obj.Elts, &ast.EmbedDecl{Expr: p.expr(x.emit)})
 	}
+	hasEllipsis := false
 	if p.showOptional() && x.optionals != nil &&
 		// Optional field constraints may be omitted if they were already
 		// applied and no more new fields may be added.
 		!(doEval(p.mode) && x.optionals.isEmpty() && p.isClosed(x)) {
-		p.optionals(obj, x.optionals)
+		hasEllipsis = p.optionals(obj, x.optionals)
 	}
 	for i, a := range x.arcs {
 		f := &ast.Field{
@@ -839,6 +850,10 @@
 			}
 		}
 	}
+
+	if hasEllipsis {
+		obj.Elts = append(obj.Elts, &ast.Ellipsis{})
+	}
 	return obj, nil
 }
 
diff --git a/cue/export_test.go b/cue/export_test.go
index 4771064..51f5202 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -300,15 +300,15 @@
 		eval:  true,
 		noOpt: true,
 		in: `{
-			job <Name>: {
+			job: [Name=_]: {
 				name:     Name
 				replicas: uint | *1 @protobuf(10)
 				command:  string
 			}
-			
-			job list command: "ls"
 
-			job nginx: {
+			job: list: command: "ls"
+
+			job: nginx: {
 				command:  "nginx"
 				replicas: 2
 			}
@@ -555,6 +555,16 @@
 			A: [>=0]
 			B: [10] | [192]
 		}`),
+	}, {
+		in: `{
+			[string]: _
+			foo: 3
+		}`,
+		out: unindent(`
+		{
+			foo: 3
+			...
+		}`),
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
diff --git a/cue/format/node.go b/cue/format/node.go
index 3972a49..b6fdda5 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -23,6 +23,7 @@
 	"cuelang.org/go/cue/literal"
 	"cuelang.org/go/cue/scanner"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
 )
 
 func printNode(node interface{}, f *printer) error {
@@ -103,6 +104,7 @@
 func (f *formatter) walkDeclList(list []ast.Decl) {
 	f.before(nil)
 	d := 0
+	hasEllipsis := false
 	for i, x := range list {
 		if i > 0 {
 			f.print(declcomma)
@@ -125,6 +127,10 @@
 				}
 			}
 		}
+		if f.printer.cfg.simplify && internal.IsEllipsis(x) {
+			hasEllipsis = true
+			continue
+		}
 		f.decl(x)
 		d = 0
 		if f, ok := x.(*ast.Field); ok {
@@ -150,6 +156,10 @@
 		}
 		f.print(f.current.parentSep)
 	}
+	if hasEllipsis {
+		f.decl(&ast.Ellipsis{})
+		f.print(f.current.parentSep)
+	}
 	f.after(nil)
 }
 
diff --git a/cue/format/testdata/simplify.golden b/cue/format/testdata/simplify.golden
index 84506b9..17c024e 100644
--- a/cue/format/testdata/simplify.golden
+++ b/cue/format/testdata/simplify.golden
@@ -30,6 +30,11 @@
 // Issue #294
 "\("x")": "x"
 
+a :: {
+	foo: 2
+	...
+}
+
 x: {
 	@tag0(foo)
 	r1: baz1
diff --git a/cue/format/testdata/simplify.input b/cue/format/testdata/simplify.input
index 271c6a0..8b14541 100644
--- a/cue/format/testdata/simplify.input
+++ b/cue/format/testdata/simplify.input
@@ -32,6 +32,11 @@
 // Issue #294
 "\("x")": "x"
 
+a :: {
+    [string]: _
+    foo: 2
+}
+
 x: {
 @tag0(foo)
     r1: baz1
diff --git a/internal/internal.go b/internal/internal.go
index 67a4e71..16f1e44 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -126,3 +126,30 @@
 	}
 	return cg
 }
+
+// IsEllipsis reports whether the declaration can be represented as an ellipsis.
+func IsEllipsis(x ast.Decl) bool {
+	// ...
+	if _, ok := x.(*ast.Ellipsis); ok {
+		return true
+	}
+
+	// [string]: _ or [_]: _
+	f, ok := x.(*ast.Field)
+	if !ok {
+		return false
+	}
+	v, ok := f.Value.(*ast.Ident)
+	if !ok || v.Name != "_" {
+		return false
+	}
+	l, ok := f.Label.(*ast.ListLit)
+	if !ok || len(l.Elts) != 1 {
+		return false
+	}
+	i, ok := l.Elts[0].(*ast.Ident)
+	if !ok {
+		return false
+	}
+	return i.Name == "string" || i.Name == "_"
+}