cue: added Eval, Default, IsConcrete, Expr
These API changes allow for analyzing
values at the expression level.
Change-Id: I44d495aef2794bab2a4172c93d8b4dae133d0689
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1965
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/op.go b/cue/op.go
index 6385903..214e3c8 100644
--- a/cue/op.go
+++ b/cue/op.go
@@ -16,6 +16,74 @@
import "cuelang.org/go/cue/token"
+// Op indicates the operation at the top of an expression tree of the expression
+// use to evaluate a value.
+type Op int
+
+// Values of Op.
+const (
+ NoOp Op = iota
+
+ AndOp
+ OrOp
+
+ SelectorOp
+ IndexOp
+ SliceOp
+ CallOp
+
+ BooleanAndOp
+ BooleanOrOp
+
+ EqualOp
+ NotOp
+ NotEqualOp
+ LessThanOp
+ LessThanEqualOp
+ GreaterThanOp
+ GreaterThanEqualOp
+
+ RegexMatchOp
+ NotRegexMatchOp
+
+ AddOp
+ SubtractOp
+ MultiplyOp
+ FloatQuotientOp
+ FloatRemainOp
+ IntQuotientOp
+ IntRemainderOp
+ IntDivideOp
+ IntModuloOp
+
+ InterpolationOp
+)
+
+var opToOp = map[op]Op{
+ opUnify: AndOp,
+ opDisjunction: OrOp,
+ opLand: BooleanAndOp,
+ opLor: BooleanOrOp,
+ opEql: EqualOp,
+ opNot: NotOp,
+ opNeq: NotEqualOp,
+ opLss: LessThanOp,
+ opLeq: LessThanEqualOp,
+ opGtr: GreaterThanOp,
+ opGeq: GreaterThanEqualOp,
+ opMat: RegexMatchOp,
+ opNMat: NotRegexMatchOp,
+ opAdd: AddOp,
+ opSub: SubtractOp,
+ opMul: MultiplyOp,
+ opQuo: FloatQuotientOp,
+ opRem: FloatRemainOp,
+ opIQuo: IntQuotientOp,
+ opIRem: IntRemainderOp,
+ opIDiv: IntDivideOp,
+ opIMod: IntModuloOp,
+}
+
func opIn(op op, anyOf ...op) bool {
for _, o := range anyOf {
if o == op {
diff --git a/cue/types.go b/cue/types.go
index 22bd98a..f9aa791 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -460,6 +460,13 @@
return Value{obj.ctx.index, &valueData{obj.path, uint32(i), a}}
}
+func remakeValue(base Value, v value) Value {
+ path := *base.path
+ path.v = v
+ path.cache = v.evalPartial(base.ctx())
+ return Value{base.idx, &path}
+}
+
func (v Value) ctx() *context {
return v.idx.newContext()
}
@@ -476,6 +483,28 @@
return ctx.manifest(v.path.cache)
}
+// Eval resolves the references of a value and returns the result.
+// This method is not necessary to obtain concrete values.
+func (v Value) Eval() Value {
+ if v.path == nil {
+ return v
+ }
+ return remakeValue(v, v.path.v.evalPartial(v.ctx()))
+}
+
+// Default reports the default value and whether it existed. It returns the
+// normal value if there is no default.
+func (v Value) Default() (Value, bool) {
+ if v.path == nil {
+ return v, false
+ }
+ x := v.ctx().manifest(v.path.v)
+ if x != v.path.v {
+ return remakeValue(v, x), true
+ }
+ return v, false
+}
+
// Label reports he label used to obtain this value from the enclosing struct.
//
// TODO: get rid of this somehow. Probably by including a FieldInfo struct
@@ -595,7 +624,11 @@
return nil
}
ctx := v.ctx()
- return export(ctx, v.eval(ctx), getOptions(opts))
+ o := getOptions(opts)
+ if o.raw {
+ return export(ctx, v.path.v, o)
+ }
+ return export(ctx, v.path.cache, o)
}
// Decode initializes x with Value v. If x is a struct, it will validate the
@@ -634,7 +667,7 @@
func separate(v value) (a []value) {
c := v.computed()
- if c == nil {
+ if c == nil || c.op != opUnify {
return []value{v}
}
if c.x != nil {
@@ -674,9 +707,30 @@
return v.idx.fset.Position(pos)
}
-// IsIncomplete indicates that the value cannot be fully evaluated due to
+// IsConcrete reports whether the current value is a concrete scalar value,
+// not relying on default values, a terminal error, a list, or a struct.
+// It does not verify that values of lists or structs are concrete themselves.
+// To check whether there is a concrete default, use v.Default().IsConcrete().
+func (v Value) IsConcrete() bool {
+ if v.path == nil {
+ return false // any is neither concrete, not a list or struct.
+ }
+ x := v.path.v.evalPartial(v.ctx())
+
+ // Errors marked as incomplete are treated as not complete.
+ if isIncomplete(x) {
+ return false
+ }
+ // Other errors are considered ground.
+ return x.kind().isGround()
+}
+
+// IsIncomplete is deprecated.
+//
+// It indicates that the value cannot be fully evaluated due to
// insufficient information.
func (v Value) IsIncomplete() bool {
+ // TODO: remove
x := v.eval(v.ctx())
if x.kind().hasReferences() || !x.kind().isGround() {
return true
@@ -724,6 +778,53 @@
return nil
}
+func makeInt(v Value, x int64) Value {
+ n := &numLit{numBase: numBase{baseValue: v.path.v.base()}}
+ n.v.SetInt64(x)
+ return remakeValue(v, n)
+}
+
+// Len returns the number of items of the underlying value.
+// For lists it reports the capacity of the list. For structs it indicates the
+// number of fields, for bytes the number of bytes.
+func (v Value) Len() Value {
+ if v.path != nil {
+ switch x := v.path.v.evalPartial(v.ctx()).(type) {
+ case *list:
+ return remakeValue(v, x.len.evalPartial(v.ctx()))
+ case *bytesLit:
+ return makeInt(v, int64(x.len()))
+ case *stringLit:
+ return makeInt(v, int64(x.len()))
+ }
+ }
+ const msg = "len not supported for type %v"
+ return remakeValue(v, v.ctx().mkErr(v.path.v, msg, v.Kind()))
+}
+
+// Elem returns the value of undefined element types of lists and structs.
+func (v Value) Elem() (Value, bool) {
+ ctx := v.ctx()
+ switch x := v.path.cache.(type) {
+ case *structLit:
+ if x.template == nil {
+ break
+ }
+ fn, ok := ctx.manifest(x.template).(*lambdaExpr)
+ if !ok {
+ // TODO: return an error instead.
+ break
+ }
+ // Note, this template does not maintain the relation between the
+ // the attribute value and the instance.
+ y := fn.call(ctx, x, &basicType{x.baseValue, stringKind})
+ return newValueRoot(ctx, y), true
+ case *list:
+ return newValueRoot(ctx, x.typ), true
+ }
+ return Value{}, false
+}
+
// List creates an iterator over the values of a list or reports an error if
// v is not a list.
func (v Value) List() (Iterator, error) {
@@ -942,8 +1043,47 @@
io.WriteString(state, debugStr(ctx, v.path.cache))
}
+// Reference returns path from the root of the file referred to by this value
+// or no path if this value is not a reference.
+func (v Value) Reference() []string {
+ // TODO: don't include references to hidden fields.
+ if v.path == nil {
+ return nil
+ }
+ sel, ok := v.path.v.(*selectorExpr)
+ if !ok {
+ return nil
+ }
+ return mkPath(v.ctx(), v.path, sel, 0)
+}
+
+func mkPath(c *context, up *valueData, sel *selectorExpr, d int) (a []string) {
+ switch x := sel.x.(type) {
+ case *selectorExpr:
+ a = mkPath(c, up.parent, x, d+1)
+ case *nodeRef:
+ // the parent must exist.
+ for ; up != nil && up.cache != x.node.(value); up = up.parent {
+ }
+ a = mkFromRoot(c, up, d+1)
+ default:
+ panic("should not happend")
+ }
+ return append(a, c.labelStr(sel.feature))
+}
+
+func mkFromRoot(c *context, up *valueData, d int) []string {
+ if up == nil || up.parent == nil {
+ return make([]string, 0, d)
+ }
+ a := mkFromRoot(c, up.parent, d+1)
+ return append(a, c.labelStr(up.feature))
+}
+
// References reports all references used to evaluate this value. It does not
// report references for sub fields if v is a struct.
+//
+// Deprecated: can be implemented in terms of Reference and Expr.
func (v Value) References() [][]string {
ctx := v.ctx()
pf := pathFinder{up: v.path}
@@ -1245,3 +1385,103 @@
}
return "", false, nil
}
+
+// Expr reports the operation of the underlying expression and the values it
+// operates on. The returned values are appended to the given slice, which may
+// be nil.
+//
+// For unary expressions, it returns the single value of the expression.
+//
+// For binary expressions it returns first the left and right value, in that
+// order. For associative operations however, (for instance '&' and '|'), it may
+// return more than two values, where the operation is to be applied in
+// sequence.
+//
+// For selector and index expressions it returns the subject and then the index.
+// For selectors, the index is the string value of the identifier. For slice
+// expressions, it returns the subject, low value, and high value, in that
+// order.
+//
+// For interpolations it returns a sequence of values to be concatenated, some
+// of which will be literal strings and some unevaluated expressions.
+//
+// A builtin call expression returns the value of the builtin followed by the
+// args of the call.
+//
+// If v is not an expression, Partial append v itself.
+// TODO: return v if this is complete? Yes for now
+// TODO: add values if a == nil?
+func (v Value) Expr() (Op, []Value) {
+ if v.path == nil {
+ return NoOp, nil
+ }
+ // TODO: replace appends with []Value{}. For not leave.
+ a := []Value{}
+ op := NoOp
+ switch x := v.path.v.(type) {
+ case *binaryExpr:
+ a = append(a, remakeValue(v, x.left))
+ a = append(a, remakeValue(v, x.right))
+ op = opToOp[x.op]
+ case *unaryExpr:
+ a = append(a, remakeValue(v, x.x))
+ op = opToOp[x.op]
+ case *bound:
+ a = append(a, remakeValue(v, x.value))
+ op = opToOp[x.op]
+ case *unification:
+ // pre-expanded unification
+ for _, conjunct := range x.values {
+ a = append(a, remakeValue(v, conjunct))
+ }
+ op = AndOp
+ case *disjunction:
+ // Filter defaults that are subsumed by another value.
+ count := 0
+ outer:
+ for _, disjunct := range x.values {
+ if disjunct.marked {
+ for _, n := range x.values {
+ if !n.marked && subsumes(v.ctx(), n.val, disjunct.val, 0) {
+ continue outer
+ }
+ }
+ }
+ count++
+ a = append(a, remakeValue(v, disjunct.val))
+ }
+ if count > 1 {
+ op = OrOp
+ }
+ case *interpolation:
+ for _, p := range x.parts {
+ a = append(a, remakeValue(v, p))
+ }
+ op = InterpolationOp
+ case *selectorExpr:
+ a = append(a, remakeValue(v, x.x))
+ a = append(a, remakeValue(v, &stringLit{
+ x.baseValue,
+ v.ctx().labelStr(x.feature),
+ }))
+ op = SelectorOp
+ case *indexExpr:
+ a = append(a, remakeValue(v, x.x))
+ a = append(a, remakeValue(v, x.index))
+ op = IndexOp
+ case *sliceExpr:
+ a = append(a, remakeValue(v, x.x))
+ a = append(a, remakeValue(v, x.lo))
+ a = append(a, remakeValue(v, x.hi))
+ op = SliceOp
+ case *callExpr:
+ a = append(a, remakeValue(v, x.x))
+ for _, arg := range x.args {
+ a = append(a, remakeValue(v, arg))
+ }
+ op = CallOp
+ default:
+ a = append(a, v)
+ }
+ return op, a
+}
diff --git a/cue/types_test.go b/cue/types_test.go
index 3d199d7..0429e90 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -45,6 +45,7 @@
incompleteKind Kind
json string
valid bool
+ concrete bool
// pos token.Pos
}{{ // Not a concrete value.
value: `_`,
@@ -54,18 +55,22 @@
value: `_|_`,
kind: BottomKind,
incompleteKind: BottomKind,
+ concrete: true,
}, {
value: `1&2`,
kind: BottomKind,
incompleteKind: BottomKind,
+ concrete: true,
}, { // TODO: should be error{
value: `b`,
kind: BottomKind,
incompleteKind: BottomKind,
+ concrete: true,
}, {
value: `(b[a])`,
kind: BottomKind,
incompleteKind: BottomKind,
+ concrete: true,
}, { // TODO: should be error{
value: `(b)
b: bool`,
@@ -75,18 +80,22 @@
value: `([][b])`,
kind: BottomKind,
incompleteKind: BottomKind,
+ concrete: true,
}, {
value: `null`,
kind: NullKind,
incompleteKind: NullKind,
+ concrete: true,
}, {
value: `true`,
kind: BoolKind,
incompleteKind: BoolKind,
+ concrete: true,
}, {
value: `false`,
kind: BoolKind,
incompleteKind: BoolKind,
+ concrete: true,
}, {
value: `bool`,
kind: BottomKind,
@@ -95,18 +104,22 @@
value: `2`,
kind: NumberKind,
incompleteKind: NumberKind,
+ concrete: true,
}, {
value: `2.0`,
kind: FloatKind,
incompleteKind: FloatKind,
+ concrete: true,
}, {
value: `2.0Mi`,
kind: NumberKind,
incompleteKind: NumberKind,
+ concrete: true,
}, {
value: `14_000`,
kind: NumberKind,
incompleteKind: NumberKind,
+ concrete: true,
}, {
value: `>=0 & <5`,
kind: BottomKind,
@@ -119,10 +132,12 @@
value: `"str"`,
kind: StringKind,
incompleteKind: StringKind,
+ concrete: true,
}, {
value: "'''\n'''",
kind: BytesKind,
incompleteKind: BytesKind,
+ concrete: true,
}, {
value: "string",
kind: BottomKind,
@@ -131,10 +146,16 @@
value: `{}`,
kind: StructKind,
incompleteKind: StructKind,
+ concrete: true,
}, {
value: `[]`,
kind: ListKind,
incompleteKind: ListKind,
+ concrete: true,
+ }, {
+ value: `{a: int, b: [1][a]}.b`,
+ kind: BottomKind,
+ concrete: false,
}}
for _, tc := range testCases {
t.Run(tc.value, func(t *testing.T) {
@@ -151,6 +172,9 @@
if got := v.IsIncomplete(); got != incomplete {
t.Errorf("IsIncomplete: got %v; want %v", got, incomplete)
}
+ if got := v.IsConcrete(); got != tc.concrete {
+ t.Errorf("IsConcrete: got %v; want %v", got, tc.concrete)
+ }
invalid := tc.kind == BottomKind
if got := v.IsValid(); got != !invalid {
t.Errorf("IsValid: got %v; want %v", got, !invalid)
@@ -653,6 +677,83 @@
}
}
+func TestDefaults(t *testing.T) {
+ testCases := []struct {
+ value string
+ def string
+ val string
+ err string
+ }{{
+ value: `number | *1`,
+ def: "1",
+ val: "number",
+ }, {
+ value: `1 | 2 | *3`,
+ def: "3",
+ val: "1|2|3",
+ }, {
+ value: `*{a:1,b:2}|{a:1}|{b:2}`,
+ def: "<0>{a: 1, b: 2}",
+ val: "<0>{a: 1}|<0>{b: 2}",
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.value, func(t *testing.T) {
+ v := getInstance(t, "a: "+tc.value).Lookup("a")
+
+ _, val := v.Expr()
+ d, _ := v.Default()
+
+ if got := fmt.Sprint(d); got != tc.def {
+ t.Errorf("default: got %v; want %v", got, tc.def)
+ }
+
+ vars := []string{}
+ for _, v := range val {
+ vars = append(vars, fmt.Sprint(v))
+ }
+ if got := strings.Join(vars, "|"); got != tc.val {
+ t.Errorf("value: got %v; want %v", got, tc.val)
+ }
+ })
+ }
+}
+
+func TestLen(t *testing.T) {
+ testCases := []struct {
+ input string
+ length string
+ }{{
+ input: "[1, 3]",
+ length: "2",
+ }, {
+ input: "[1, 3, ...]",
+ length: ">=2",
+ }, {
+ input: `"foo"`,
+ length: "3",
+ }, {
+ input: `'foo'`,
+ length: "3",
+ // TODO: Currently not supported.
+ // }, {
+ // input: "{a:1, b:3, a:1, c?: 3, _hidden: 4}",
+ // length: "2",
+ }, {
+ input: "3",
+ length: "_|_(3:len not supported for type 24)",
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.input, func(t *testing.T) {
+ v := getInstance(t, "a: "+tc.input).Lookup("a")
+
+ length := v.Len()
+ if got := fmt.Sprint(length); got != tc.length {
+ t.Errorf("length: got %v; want %v", got, tc.length)
+ }
+ })
+ }
+}
+
func TestTemplate(t *testing.T) {
testCases := []struct {
value string
@@ -1440,6 +1541,33 @@
}
}
+func TestReference(t *testing.T) {
+ testCases := []struct {
+ input string
+ want string
+ }{{
+ input: "v: _|_",
+ want: "",
+ }, {
+ input: "v: 2",
+ want: "",
+ }, {
+ input: "v: a, a: 1",
+ want: "a",
+ }, {
+ input: "v: a.b.c, a b c: 1",
+ want: "a b c",
+ }}
+ for _, tc := range testCases {
+ t.Run("", func(t *testing.T) {
+ v := getInstance(t, tc.input).Lookup("v")
+ if got := strings.Join(v.Reference(), " "); got != tc.want {
+ t.Errorf("\n got %v;\nwant %v", got, tc.want)
+ }
+ })
+ }
+}
+
func TestReferences(t *testing.T) {
config1 := `
a: {
@@ -1522,3 +1650,148 @@
}
return true
}
+
+func TestExpr(t *testing.T) {
+ testCases := []struct {
+ input string
+ want string
+ }{{
+ input: "v: 3",
+ want: " 3",
+ }, {
+ input: "v: 3 + 4",
+ want: "+ 3 4",
+ }, {
+ input: "v: !a, a: 3",
+ want: "! <0>.a",
+ }, {
+ input: "v: 1 | 2 | 3 | *4",
+ want: "| 1 2 3 4",
+ }, {
+ input: "v: 2 & 5",
+ want: "& 2 5",
+ }, {
+ input: "v: 2 | 5",
+ want: "| 2 5",
+ }, {
+ input: "v: 2 && 5",
+ want: "&& 2 5",
+ }, {
+ input: "v: 2 || 5",
+ want: "|| 2 5",
+ }, {
+ input: "v: 2 == 5",
+ want: "== 2 5",
+ }, {
+ input: "v: !b, b: true",
+ want: "! <0>.b",
+ }, {
+ input: "v: 2 != 5",
+ want: "!= 2 5",
+ }, {
+ input: "v: <5",
+ want: "< 5",
+ }, {
+ input: "v: 2 <= 5",
+ want: "<= 2 5",
+ }, {
+ input: "v: 2 > 5",
+ want: "> 2 5",
+ }, {
+ input: "v: 2 >= 5",
+ want: ">= 2 5",
+ }, {
+ input: "v: 2 =~ 5",
+ want: "=~ 2 5",
+ }, {
+ input: "v: 2 !~ 5",
+ want: "!~ 2 5",
+ }, {
+ input: "v: 2 + 5",
+ want: "+ 2 5",
+ }, {
+ input: "v: 2 - 5",
+ want: "- 2 5",
+ }, {
+ input: "v: 2 * 5",
+ want: "* 2 5",
+ }, {
+ input: "v: 2 / 5",
+ want: "/ 2 5",
+ }, {
+ input: "v: 2 % 5",
+ want: "% 2 5",
+ }, {
+ input: "v: 2 quo 5",
+ want: "quo 2 5",
+ }, {
+ input: "v: 2 rem 5",
+ want: "rem 2 5",
+ }, {
+ input: "v: 2 div 5",
+ want: "div 2 5",
+ }, {
+ input: "v: 2 mod 5",
+ want: "mod 2 5",
+ }, {
+ input: "v: a.b, a b: 4",
+ want: `. <0>.a "b"`,
+ }, {
+ input: `v: a["b"], a b: 3 `,
+ want: `[] <0>.a "b"`,
+ }, {
+ input: "v: a[2:5], a: [1, 2, 3, 4, 5]",
+ want: "[:] <0>.a 2 5",
+ }, {
+ input: "v: len([])",
+ want: "() builtin:len []",
+ }, {
+ input: `v: "Hello, \(x)! Welcome to \(place)", place: string, x: string`,
+ want: `\() "Hello, " <0>.x "! Welcome to " <0>.place ""`,
+ }}
+ for _, tc := range testCases {
+ t.Run(tc.input, func(t *testing.T) {
+ v := getInstance(t, tc.input).Lookup("v")
+ op, operands := v.Expr()
+ got := opToString[op]
+ for _, v := range operands {
+ got += " "
+ got += debugStr(v.ctx(), v.path.v)
+ }
+ if got != tc.want {
+ t.Errorf("\n got %v;\nwant %v", got, tc.want)
+ }
+ })
+ }
+}
+
+var opToString = map[Op]string{
+ AndOp: "&",
+ OrOp: "|",
+ BooleanAndOp: "&&",
+ BooleanOrOp: "||",
+ EqualOp: "==",
+ NotOp: "!",
+ NotEqualOp: "!=",
+ LessThanOp: "<",
+ LessThanEqualOp: "<=",
+ GreaterThanOp: ">",
+ GreaterThanEqualOp: ">=",
+ RegexMatchOp: "=~",
+ NotRegexMatchOp: "!~",
+ AddOp: "+",
+ SubtractOp: "-",
+ MultiplyOp: "*",
+ FloatQuotientOp: "/",
+ FloatRemainOp: "%",
+ IntQuotientOp: "quo",
+ IntRemainderOp: "rem",
+ IntDivideOp: "div",
+ IntModuloOp: "mod",
+
+ SelectorOp: ".",
+ IndexOp: "[]",
+ SliceOp: "[:]",
+ CallOp: "()",
+ InterpolationOp: `\()`,
+}