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.
//