cue: support for #-style definitions

The old style definitions are still supported.
No rewriting is done as of yet.

Semantics is mostly the same, except that
bulk optional fields are not applied to #-style
definitions.

Issue #339

Change-Id: I28f219bea5a9dca7f2cc38726a2ac0dcd24a6580
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5762
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast.go b/cue/ast.go
index bc5b61c..9fd68e7 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -195,6 +195,21 @@
 	return str
 }
 
+func isDef(f *ast.Field) bool {
+	if f.Token == token.ISA {
+		return true
+	}
+	switch x := f.Label.(type) {
+	case *ast.Alias:
+		if ident, ok := x.Expr.(*ast.Ident); ok {
+			return strings.HasPrefix(ident.Name, "#")
+		}
+	case *ast.Ident:
+		return strings.HasPrefix(x.Name, "#")
+	}
+	return false
+}
+
 // We probably don't need to call Walk.s
 func (v *astVisitor) walk(astNode ast.Node) (ret value) {
 	switch n := astNode.(type) {
@@ -376,7 +391,7 @@
 
 	case *ast.Field:
 		opt := n.Optional != token.NoPos
-		isDef := n.Token == token.ISA
+		isDef := isDef(n)
 		if isDef {
 			ctx := v.ctx()
 			ctx.inDefinition++
diff --git a/cue/ast/ident.go b/cue/ast/ident.go
index a37c572..24e5322 100644
--- a/cue/ast/ident.go
+++ b/cue/ast/ident.go
@@ -37,11 +37,17 @@
 	if ident == "" {
 		return false
 	}
-	for i, r := range ident {
-		if isLetter(r) || r == '_' || r == '$' {
-			continue
-		}
-		if i > 0 && isDigit(r) {
+
+	r, sz := utf8.DecodeRuneInString(ident)
+	switch {
+	case isDigit(r):
+		return false
+	case r == '#':
+		ident = ident[sz:]
+	}
+
+	for _, r := range ident {
+		if isLetter(r) || isDigit(r) || r == '_' || r == '$' {
 			continue
 		}
 		return false
@@ -72,15 +78,13 @@
 			continue
 		}
 		if r == '-' {
-			goto escape
+			return "`" + ident + "`", nil
 		}
 		return "", errors.Newf(token.NoPos, "invalid character '%s' in identifier", string(r))
 	}
 
-	return ident, nil
-
-escape:
-	return "`" + ident + "`", nil
+	_, err := parseIdent(token.NoPos, ident)
+	return ident, err
 }
 
 // ParseIdent unquotes a possibly quoted identifier and validates
@@ -88,28 +92,40 @@
 //
 // Deprecated: quoted identifiers are deprecated. Use aliases.
 func ParseIdent(n *Ident) (string, error) {
-	ident := n.Name
+	return parseIdent(n.NamePos, n.Name)
+}
+
+func parseIdent(pos token.Pos, ident string) (string, error) {
 	if ident == "" {
-		return "", errors.Newf(n.Pos(), "empty identifier")
+		return "", errors.Newf(pos, "empty identifier")
 	}
 	quoted := false
 	if ident[0] == '`' {
 		u, err := strconv.Unquote(ident)
 		if err != nil {
-			return "", errors.Newf(n.Pos(), "invalid quoted identifier")
+			return "", errors.Newf(pos, "invalid quoted identifier")
 		}
 		ident = u
 		quoted = true
 	}
 
-	for _, r := range ident {
+	r, sz := utf8.DecodeRuneInString(ident)
+	switch {
+	case isDigit(r):
+		return "", errors.Newf(pos, "invalid character '%s' in identifier", string(r))
+	case r == '#':
+	default:
+		sz = 0
+	}
+
+	for _, r := range ident[sz:] {
 		if isLetter(r) || isDigit(r) || r == '_' || r == '$' {
 			continue
 		}
 		if r == '-' && quoted {
 			continue
 		}
-		return "", errors.Newf(n.Pos(), "invalid character '%s' in identifier", string(r))
+		return "", errors.Newf(pos, "invalid character '%s' in identifier", string(r))
 	}
 
 	return ident, nil
diff --git a/cue/ast/ident_test.go b/cue/ast/ident_test.go
index b652d0a..eefd093 100644
--- a/cue/ast/ident_test.go
+++ b/cue/ast/ident_test.go
@@ -37,6 +37,10 @@
 		out:     "foo-bar",
 		isIdent: false,
 	}, {
+		in:      ast.NewString("8ball"),
+		out:     "8ball",
+		isIdent: false,
+	}, {
 		in:      ast.NewString("foo bar"),
 		out:     "foo bar",
 		isIdent: false,
@@ -45,6 +49,19 @@
 		out:     "foo",
 		isIdent: true,
 	}, {
+		in:      &ast.Ident{Name: "8ball"},
+		out:     "",
+		isIdent: false,
+		err:     true,
+	}, {
+		in:      &ast.Ident{Name: "_hidden"},
+		out:     "_hidden",
+		isIdent: true,
+	}, {
+		in:      &ast.Ident{Name: "#Def"},
+		out:     "#Def",
+		isIdent: true,
+	}, {
 		in:      &ast.Ident{Name: "`foo-bar`"},
 		out:     "foo-bar",
 		isIdent: true,
diff --git a/cue/ast_test.go b/cue/ast_test.go
index 4faceff..966135e3 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -225,7 +225,7 @@
 			C="\(a)": 5
 			c: C
 			`,
-		out: `<0>{[]: <1>(ID: string)-><2>{name: <1>.ID}, "foo=bar": 3, a: <0>.foo=bar, bb: 4, b1: (<0>.bb & <0>.bb), c: <0>[""+<0>.a+""]""+<0>.a+"": 5}`,
+		out: `<0>{[]: <1>(ID: string)-><2>{name: <1>.ID}, "foo=bar": 3, a: <0>."foo=bar", bb: 4, b1: (<0>.bb & <0>.bb), c: <0>[""+<0>.a+""]""+<0>.a+"": 5}`,
 	}, {
 		// optional fields with key filters
 		in: `
@@ -368,6 +368,27 @@
 			`base :: <8>C{info :: <9>{...}}}`,
 	}, {
 		in: `
+		a: d: {
+			#base
+			#info: {
+				...
+			}
+			Y: #info.X
+		}
+
+		#base: {
+			#info: {...}
+		}
+
+		a: [Name=string]: { #info: {
+			X: "foo"
+		}}
+		`,
+		out: `<0>{` +
+			`a: (<1>{d: <2>{#info: <3>{...}, Y: <2>.#info.X}, <0>.#base} & <4>{[]: <5>(Name: string)-><6>{#info: <7>C{X: "foo"}}, }), ` +
+			`#base: <8>C{#info: <9>{...}}}`,
+	}, {
+		in: `
 		def :: {
 			Type: string
 			Text: string
diff --git a/cue/build.go b/cue/build.go
index f7e17fa..27408fc 100644
--- a/cue/build.go
+++ b/cue/build.go
@@ -17,6 +17,7 @@
 import (
 	"path"
 	"strconv"
+	"strings"
 	"sync"
 
 	"cuelang.org/go/cue/ast"
@@ -295,15 +296,20 @@
 		idx.labelMap[s] = f
 		idx.labels = append(idx.labels, s)
 	}
-	f <<= 1
-	if isIdent && s != "" && s[0] == '_' {
-		f |= 1
+	f <<= labelShift
+	if isIdent {
+		if strings.HasPrefix(s, "#") {
+			f |= definition
+		}
+		if strings.HasPrefix(s, "_") || strings.HasPrefix(s, "#_") {
+			f |= hidden
+		}
 	}
 	return f
 }
 
 func (idx *index) labelStr(l label) string {
-	l >>= 1
+	l >>= labelShift
 	for ; l < idx.offset; idx = idx.parent {
 	}
 	return idx.labels[l-idx.offset]
diff --git a/cue/debug.go b/cue/debug.go
index c4b0cfc..630b4be 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -16,10 +16,11 @@
 
 import (
 	"bytes"
-	"cuelang.org/go/cue/ast"
 	"fmt"
 	"strconv"
 	"strings"
+
+	"cuelang.org/go/cue/ast"
 )
 
 func debugStr(ctx *context, v value) string {
@@ -140,7 +141,14 @@
 	if p.ctx == nil {
 		return strconv.Itoa(int(f))
 	}
-	return p.ctx.labelStr(f)
+
+	str := p.ctx.labelStr(f)
+	if strings.HasPrefix(str, "#") && f&definition == 0 ||
+		strings.HasPrefix(str, "_") && f&hidden == 0 ||
+		!ast.IsValidIdent(str) {
+		return strconv.Quote(str)
+	}
+	return str
 }
 
 func (p *printer) writef(format string, args ...interface{}) {
@@ -369,14 +377,11 @@
 	case arc:
 		n := x.v
 		str := p.label(x.feature)
-		if !ast.IsValidIdent(str) {
-			str = strconv.Quote(str)
-		}
 		p.writef(str)
 		if x.optional {
 			p.write("?")
 		}
-		if x.definition {
+		if x.definition && x.feature&definition == 0 {
 			p.write(" :: ")
 		} else {
 			p.write(": ")
diff --git a/cue/export.go b/cue/export.go
index 75f3690..d6d6072 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -20,7 +20,6 @@
 	"sort"
 	"strconv"
 	"strings"
-	"unicode"
 	"unicode/utf8"
 
 	"github.com/cockroachdb/apd/v2"
@@ -146,21 +145,13 @@
 }
 
 func (p *exporter) label(f label) ast.Label {
-	orig := p.ctx.labelStr(f)
-	str := strconv.Quote(orig)
-	if len(orig)+2 < len(str) || (strings.HasPrefix(orig, "_") && f&1 == 0) {
-		return ast.NewLit(token.STRING, str)
+	str := p.ctx.labelStr(f)
+	if strings.HasPrefix(str, "#") && f&definition == 0 ||
+		strings.HasPrefix(str, "_") && f&hidden == 0 ||
+		!ast.IsValidIdent(str) {
+		return ast.NewLit(token.STRING, strconv.Quote(str))
 	}
-	for i, r := range orig {
-		if unicode.IsLetter(r) || r == '_' || r == '$' {
-			continue
-		}
-		if i > 0 && unicode.IsDigit(r) {
-			continue
-		}
-		return ast.NewLit(token.STRING, str)
-	}
-	return &ast.Ident{Name: orig}
+	return &ast.Ident{Name: str}
 }
 
 func (p *exporter) identifier(f label) *ast.Ident {
@@ -874,7 +865,9 @@
 			if p.mode.omitDefinitions || p.mode.concrete {
 				continue
 			}
-			f.Token = token.ISA
+			if !isDef(f) {
+				f.Token = token.ISA
+			}
 		}
 		if a.feature&hidden != 0 && p.mode.concrete && p.mode.omitHidden {
 			continue
diff --git a/cue/export_test.go b/cue/export_test.go
index 7e7fb1c..dcad849 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -933,6 +933,43 @@
 		a: A`),
 	}, {
 		eval: true,
+		opts: []Option{Docs(true)},
+		// It is okay to allow bulk-optional fields along-side definitions.
+		in: `
+		"#A": {
+			[string]: int
+			"#B": 4
+		}
+		// Definition
+		#A: {
+			[string]: int
+			#B: 4
+		}
+		a: #A.#B
+		`,
+		out: unindent(`
+		"#A": {
+			[string]: int
+			"#B":     4
+		}
+		// Definition
+		#A: {
+			[string]: int
+			#B:       4
+		}
+		a: 4`),
+	}, {
+		in: `
+		#A: {
+			#B: 4
+		}
+		a: #A.#B
+		`,
+		out: unindent(`
+		#A: #B: 4
+		a: #A.#B`),
+	}, {
+		eval: true,
 		in: `
 		x: [string]: int
 		a: [P=string]: {
diff --git a/cue/format/simplify.go b/cue/format/simplify.go
index 2152ac4..5f9c664 100644
--- a/cue/format/simplify.go
+++ b/cue/format/simplify.go
@@ -87,7 +87,10 @@
 	switch x := n.(type) {
 	case *ast.BasicLit:
 		str, err := strconv.Unquote(x.Value)
-		if err != nil || !ast.IsValidIdent(str) || strings.HasPrefix(str, "_") {
+		if err != nil ||
+			!ast.IsValidIdent(str) ||
+			strings.HasPrefix(str, "_") ||
+			strings.HasPrefix(str, "#") {
 			return false
 		}
 		s.scope[str] = true
@@ -105,7 +108,10 @@
 	switch x := c.Node().(type) {
 	case *ast.BasicLit:
 		str, err := strconv.Unquote(x.Value)
-		if err == nil && s.scope[str] && !strings.HasPrefix(str, "_") {
+		if err == nil &&
+			s.scope[str] &&
+			!strings.HasPrefix(str, "_") &&
+			!strings.HasPrefix(str, "#") {
 			c.Replace(ast.NewIdent(str))
 		}
 	}
diff --git a/cue/format/testdata/simplify.golden b/cue/format/testdata/simplify.golden
index e89e4f6..461d0be 100644
--- a/cue/format/testdata/simplify.golden
+++ b/cue/format/testdata/simplify.golden
@@ -35,6 +35,8 @@
 	...
 }
 
+"#A": dontSimplify
+
 x: {
 	@tag0(foo)
 	r1: baz1
diff --git a/cue/format/testdata/simplify.input b/cue/format/testdata/simplify.input
index a87340d..056dc43 100644
--- a/cue/format/testdata/simplify.input
+++ b/cue/format/testdata/simplify.input
@@ -37,6 +37,8 @@
     foo: 2
 }
 
+"#A": dontSimplify
+
 x: {
 @tag0(foo)
     r1: baz1
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index 3bbf610..f033572 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -109,8 +109,9 @@
 
 			 embedding
 		}
+		#Def: {}
 		`,
-		`Def :: {b: "2", c: 3, embedding}`,
+		`Def :: {b: "2", c: 3, embedding}, #Def: {}`,
 	}, {
 		"one-line embedding",
 		`{ V1, V2 }`,
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index d8e95b9..7e7dbcb 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -1243,6 +1243,26 @@
 			`DC :: <10>C{a: int}` +
 			`}`,
 	}, {
+		desc: "new-style definitions",
+		in: `
+		#Foo: {
+			a: 1
+			b: int
+		}
+		"#Foo": #Foo & {b: 1}
+
+		bulk: {[string]: string} & {
+			#def: 4 // Different namespace, so bulk option does not apply.
+			_hid: 3
+			a: "foo"
+		}
+		`,
+		out: `<0>{` +
+			`"#Foo": <1>C{a: 1, b: 1}, ` +
+			`#Foo: <2>C{a: 1, b: int}, ` +
+			`bulk: <3>{[]: <4>(_: string)->string, a: "foo", #def: 4, _hid: 3}` +
+			`}`,
+	}, {
 		desc: "recursive closing starting at non-definition",
 		in: `
 			z: a: {
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index abd1461..0547952 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -266,6 +266,17 @@
 	return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
 }
 
+func (s *Scanner) scanFieldIdentifier() string {
+	offs := s.offset
+	if s.ch == '#' {
+		s.next()
+	}
+	for isLetter(s.ch) || isDigit(s.ch) || s.ch == '_' || s.ch == '$' {
+		s.next()
+	}
+	return string(s.src[offs:s.offset])
+}
+
 func (s *Scanner) scanIdentifier() string {
 	offs := s.offset
 	for isLetter(s.ch) || isDigit(s.ch) || s.ch == '_' || s.ch == '$' {
@@ -752,27 +763,28 @@
 
 	// determine token value
 	insertEOL := false
+	var quote quoteInfo
 	switch ch := s.ch; {
-	// case ch == '$':
-	// 	lit = string(rune(ch))
-	// 	s.next()
-	// 	fallthrough
-	case isLetter(ch), ch == '$':
-		lit = s.scanIdentifier()
+	case '0' <= ch && ch <= '9':
+		insertEOL = true
+		tok, lit = s.scanNumber(false)
+	case isLetter(ch), ch == '$', ch == '#':
+		lit = s.scanFieldIdentifier()
 		if len(lit) > 1 {
 			// keywords are longer than one letter - avoid lookup otherwise
 			tok = token.Lookup(lit)
 			insertEOL = true
-		} else {
+			break
+		} else if ch != '#' {
 			tok = token.IDENT
 			insertEOL = true
+			break
 		}
-	case '0' <= ch && ch <= '9':
-		insertEOL = true
-		tok, lit = s.scanNumber(false)
+		quote.numHash = 1
+		ch = s.ch
+		fallthrough
 	default:
 		s.next() // always make progress
-		var quote quoteInfo
 		switch ch {
 		case -1:
 			if s.insertEOL {
@@ -813,7 +825,7 @@
 			s.insertEOL = false // newline consumed
 			return s.file.Pos(offset, token.Elided), token.COMMA, "\n"
 		case '#':
-			for quote.numHash = 1; s.ch == '#'; quote.numHash++ {
+			for quote.numHash++; s.ch == '#'; quote.numHash++ {
 				s.next()
 			}
 			ch = s.ch
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index ee6d5e9..d40bbfb 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -76,6 +76,8 @@
 
 	{token.IDENT, "foobar", literal},
 	{token.IDENT, "$foobar", literal},
+	{token.IDENT, "#foobar", literal},
+	{token.IDENT, "_foobar", literal},
 	{token.IDENT, "`foobar`", literal},
 	{token.IDENT, "a۰۱۸", literal},
 	{token.IDENT, "foo६४", literal},
diff --git a/cue/value.go b/cue/value.go
index 8092014..8abaf0b 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -799,6 +799,10 @@
 		return false
 	}
 
+	if f&(hidden|definition) != 0 {
+		return false
+	}
+
 	str := ctx.labelStr(f)
 	arg := &stringLit{str: str}
 
@@ -1199,12 +1203,15 @@
 		return v, nil
 	}
 
-	name := ctx.labelStr(x.arcs[i].feature)
-	arg := &stringLit{x.baseValue, name, nil}
+	if x.arcs[i].feature&(hidden|definition) == 0 {
+		name := ctx.labelStr(x.arcs[i].feature)
+		arg := &stringLit{x.baseValue, name, nil}
 
-	val, doc := x.optionals.constraint(ctx, arg)
-	if val != nil {
-		v = binOp(ctx, x, opUnify, v, val.evalPartial(ctx))
+		var val value
+		val, doc = x.optionals.constraint(ctx, arg)
+		if val != nil {
+			v = binOp(ctx, x, opUnify, v, val.evalPartial(ctx))
+		}
 	}
 
 	if x.closeStatus != 0 {
@@ -1216,7 +1223,12 @@
 // A label is a canonicalized feature name.
 type label uint32
 
-const hidden label = 0x01 // only set iff identifier starting with _
+const (
+	hidden     label = 0x01 // only set iff identifier starting with _ or #_
+	definition label = 0x02 // only set iff identifier starting with #
+
+	labelShift = 2
+)
 
 // An arc holds the label-value pair.
 //