cue: improve support for exporting partial results
- add aliases if references become shadowed
- don’t return false for Err if a value is merely incomplete
- export struct templates
Also:
- remove Default from API
Change-Id: If3d30c25efc4bae77b3c10c018f64586668f298f
Reviewed-on: https://cue-review.googlesource.com/c/1530
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/testdata/hello/export.out b/cmd/cue/cmd/testdata/hello/export.out
new file mode 100644
index 0000000..ccd2471
--- /dev/null
+++ b/cmd/cue/cmd/testdata/hello/export.out
@@ -0,0 +1,4 @@
+{
+ "who": "World",
+ "message": "Hello World!"
+}
diff --git a/cmd/cue/cmd/testdata/partial/eval.out b/cmd/cue/cmd/testdata/partial/eval.out
new file mode 100644
index 0000000..0f97899
--- /dev/null
+++ b/cmd/cue/cmd/testdata/partial/eval.out
@@ -0,0 +1,20 @@
+// $CWD/testdata/partial
+{
+ def: 1
+ sum: 1 | 2
+ A = a
+ b: {
+ idx: A[str]
+ a b: 4
+ str: string
+ }
+ a: {
+ b: 3
+ c: 4
+ }
+ c: {
+ idx: 3
+ a b: 4
+ str: "b"
+ }
+}
diff --git a/cmd/cue/cmd/testdata/partial/partial.cue b/cmd/cue/cmd/testdata/partial/partial.cue
new file mode 100644
index 0000000..ce33304
--- /dev/null
+++ b/cmd/cue/cmd/testdata/partial/partial.cue
@@ -0,0 +1,15 @@
+package partial
+
+def: *1 | int
+sum: 1 | 2
+
+b: {
+ idx: a[str] // should resolve to top-level `a`
+ str: string
+}
+b a b: 4
+a: {
+ b: 3
+ c: 4
+}
+c: b & {str: "b"}
diff --git a/cue/export.go b/cue/export.go
index 4b18225..f42d36c 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -16,6 +16,7 @@
import (
"fmt"
+ "math/rand"
"strconv"
"strings"
"unicode"
@@ -28,18 +29,39 @@
type exportMode int
const (
- exportRaw exportMode = iota
- exportEval
+ exportEval exportMode = 1 << iota
+ exportRaw exportMode = 0
)
func export(ctx *context, v value, m exportMode) ast.Expr {
- e := exporter{ctx, m}
+ e := exporter{ctx, m, nil}
return e.expr(v)
}
type exporter struct {
- ctx *context
- mode exportMode
+ ctx *context
+ mode exportMode
+ stack []remap
+}
+
+type remap struct {
+ key scope // structLit or params
+ from label
+ to *ast.Ident
+ syn *ast.StructLit
+}
+
+func (p *exporter) unique(s string) string {
+ s = strings.ToUpper(s)
+ lab := s
+ for {
+ if _, ok := p.ctx.labelMap[lab]; !ok {
+ p.ctx.label(lab, true)
+ break
+ }
+ lab = s + fmt.Sprintf("%0.6x", rand.Intn(1<<24))
+ }
+ return lab
}
func (p *exporter) label(f label) ast.Label {
@@ -90,44 +112,92 @@
func (p *exporter) expr(v value) ast.Expr {
if p.mode == exportEval {
- v = v.evalPartial(p.ctx)
+ x := p.ctx.manifest(v)
+ if isIncomplete(x) {
+ p = &exporter{p.ctx, exportRaw, p.stack}
+ return p.expr(v)
+ }
+ v = x
}
+ old := p.stack
+ defer func() { p.stack = old }()
+
// TODO: also add position information.
switch x := v.(type) {
case *builtin:
return &ast.Ident{Name: x.Name}
+
case *nodeRef:
return nil
+
case *selectorExpr:
n := p.expr(x.x)
- if n == nil {
- return p.identifier(x.feature)
+ if n != nil {
+ return &ast.SelectorExpr{X: n, Sel: p.identifier(x.feature)}
}
- return &ast.SelectorExpr{X: n, Sel: p.identifier(x.feature)}
+ ident := p.identifier(x.feature)
+ node, ok := x.x.(*nodeRef)
+ if !ok {
+ // TODO: should not happen: report error
+ return ident
+ }
+ conflict := false
+ for i := len(p.stack) - 1; i >= 0; i-- {
+ e := &p.stack[i]
+ if e.from != x.feature {
+ continue
+ }
+ if e.key != node.node {
+ conflict = true
+ continue
+ }
+ if conflict {
+ ident = e.to
+ if e.to == nil {
+ name := p.unique(p.ctx.labelStr(x.feature))
+ e.syn.Elts = append(e.syn.Elts, &ast.Alias{
+ Ident: p.ident(name),
+ Expr: p.identifier(x.feature),
+ })
+ ident = p.ident(name)
+ e.to = ident
+ }
+ }
+ return ident
+ }
+ // TODO: should not happen: report error
+ return ident
+
case *indexExpr:
return &ast.IndexExpr{X: p.expr(x.x), Index: p.expr(x.index)}
+
case *sliceExpr:
return &ast.SliceExpr{
X: p.expr(x.x),
Low: p.expr(x.lo),
High: p.expr(x.hi),
}
+
case *callExpr:
call := &ast.CallExpr{Fun: p.expr(x.x)}
for _, a := range x.args {
call.Args = append(call.Args, p.expr(a))
}
return call
+
case *unaryExpr:
return &ast.UnaryExpr{Op: opMap[x.op], X: p.expr(x.x)}
+
case *binaryExpr:
return &ast.BinaryExpr{
X: p.expr(x.left),
Op: opMap[x.op], Y: p.expr(x.right),
}
+
case *bound:
return &ast.UnaryExpr{Op: opMap[x.op], X: p.expr(x.value)}
+
case *unification:
if len(x.values) == 1 {
return p.expr(x.values[0])
@@ -158,17 +228,37 @@
case *structLit:
obj := &ast.StructLit{}
if p.mode == exportEval {
+ for _, a := range x.arcs {
+ p.stack = append(p.stack, remap{
+ key: x,
+ from: a.feature,
+ to: nil,
+ syn: obj,
+ })
+ }
x = x.expandFields(p.ctx)
}
if x.emit != nil {
obj.Elts = append(obj.Elts, &ast.EmitDecl{Expr: p.expr(x.emit)})
}
+ if p.mode != exportEval && x.template != nil {
+ l, ok := x.template.evalPartial(p.ctx).(*lambdaExpr)
+ if ok {
+ obj.Elts = append(obj.Elts, &ast.Field{
+ Label: &ast.TemplateLabel{
+ Ident: p.identifier(l.params.arcs[0].feature),
+ },
+ Value: p.expr(l.value),
+ })
+ } // TODO: else record error
+ }
for _, a := range x.arcs {
obj.Elts = append(obj.Elts, &ast.Field{
Label: p.label(a.feature),
Value: p.expr(a.v),
})
}
+
for _, c := range x.comprehensions {
var clauses []ast.Clause
next := c.clauses
diff --git a/cue/export_test.go b/cue/export_test.go
index 7578f92..d47e61c 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -27,6 +27,7 @@
func TestExport(t *testing.T) {
testCases := []struct {
raw bool
+ mode exportMode
in, out string
}{{
in: `"hello"`,
@@ -85,14 +86,14 @@
f: [1, 2, ...]
}`,
out: unindent(`
- {
- a: 5*[int] & [1, 2, ...int]
- b: (>=2 & <=5)*[int] & [1, 2, ...int]
- c: (<=5 & >=3)*[int] & [1, 2, ...int]
- d: [1, 2, ...int]
- e: [1, 2, ...int]
- f: [1, 2, ...]
- }`),
+ {
+ a: 5*[int] & [1, 2, ...int]
+ b: (>=2 & <=5)*[int] & [1, 2, ...int]
+ c: (<=5 & >=3)*[int] & [1, 2, ...int]
+ d: [1, 2, ...int]
+ e: [1, 2, ...int]
+ f: [1, 2, ...]
+ }`),
}, {
in: `{
a: >=0*[int]
@@ -170,6 +171,49 @@
a: ""
b: len(a)
}`),
+ }, {
+ raw: true,
+ mode: exportEval,
+ in: `{
+ b: {
+ idx: a[str]
+ str: string
+ }
+ b a b: 4
+ a b: 3
+ }`,
+ // reference to a must be redirected to outer a through alias
+ out: unindent(`
+ {
+ A = a
+ b: {
+ idx: A[str]
+ a b: 4
+ str: string
+ }
+ a b: 3
+ }`),
+ }, {
+ raw: true,
+ mode: exportEval,
+ in: `{
+ b: [{
+ <X>: int
+ f: 4 if a > 4
+ }][a]
+ a: int
+ c: *1 | 2
+ }`,
+ // reference to a must be redirected to outer a through alias
+ out: unindent(`
+ {
+ b: [{
+ <X>: int
+ "f": 4 if a > 4
+ }][a]
+ a: int
+ c: 1
+ }`),
}}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
@@ -186,7 +230,7 @@
v := newValueRoot(ctx, n)
buf := &bytes.Buffer{}
- err := format.Node(buf, export(ctx, v.eval(ctx), exportRaw))
+ err := format.Node(buf, export(ctx, v.eval(ctx), tc.mode))
if err != nil {
log.Fatal(err)
}
diff --git a/cue/types.go b/cue/types.go
index d12f805..51f9ba7 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -478,11 +478,6 @@
return ctx.manifest(v.path.v)
}
-// Default returs v if v.Exists or a value converted from x otherwise.
-func (v Value) Default(x interface{}) Value {
- return v
-}
-
// Label reports he label used to obtain this value from the enclosing struct.
//
// TODO: get rid of this somehow. Maybe by passing it to walk
@@ -709,12 +704,14 @@
return b
}
got := x.kind()
- if got&want&concreteKind == bottomKind && want != bottomKind {
- return ctx.mkErr(x, "not of right kind (%v vs %v)", got, want)
- }
- if !got.isGround() {
- return ctx.mkErr(x, codeIncomplete,
- "non-concrete value %v", got)
+ if want != bottomKind {
+ if got&want&concreteKind == bottomKind {
+ return ctx.mkErr(x, "not of right kind (%v vs %v)", got, want)
+ }
+ if !got.isGround() {
+ return ctx.mkErr(x, codeIncomplete,
+ "non-concrete value %v", got)
+ }
}
return nil
}
@@ -985,13 +982,39 @@
return v, true
}
+type options struct {
+ concrete bool
+}
+
+// An Option defines modes of evaluation.
+type Option func(p *options)
+
+// Used in Validate, Subsume?, Fields()
+
+// TODO: could also be used for subsumption.
+
+// RequireConcrete verifies that all values in a tree are concrete.
+func RequireConcrete() Option {
+ return func(p *options) { p.concrete = true }
+}
+
+// VisitHidden(visit bool)
+//
+
// Validate reports any errors, recursively. The returned error may be an
// errors.List reporting multiple errors, where the total number of errors
// reported may be less than the actual number.
-func (v Value) Validate() error {
+func (v Value) Validate(opts ...Option) error {
+ var o options
+ for _, fn := range opts {
+ fn(&o)
+ }
list := errors.List{}
v.Walk(func(v Value) bool {
if err := v.Err(); err != nil {
+ if !o.concrete && isIncomplete(v.eval(v.ctx())) {
+ return false
+ }
list.Add(err)
if len(list) > 50 {
return false // mostly to avoid some hypothetical cycle issue
diff --git a/cue/types_test.go b/cue/types_test.go
index 4f43796..9ef0215 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -429,7 +429,7 @@
value: `"Hello world!"`,
}, {
value: `string`,
- err: "non-concrete value (string)*",
+ err: "",
}}
for _, tc := range testCases {
t.Run(tc.value, func(t *testing.T) {