cue: improve coverage of Go value conversion
- cover more types
- make behavior more like encoding/json
- better test coverage
- better error messages
Change-Id: Ia017726cf027589ba694a6b7fa5bcd5396c2eab9
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2741
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/go.go b/cue/go.go
index 5235f16..7012edf 100644
--- a/cue/go.go
+++ b/cue/go.go
@@ -188,6 +188,14 @@
}
func convert(ctx *context, src source, x interface{}) evaluated {
+ v := convertRec(ctx, src, x)
+ if v == nil {
+ return ctx.mkErr(baseValue{}, "unsupported Go type (%v)", v)
+ }
+ return v
+}
+
+func convertRec(ctx *context, src source, x interface{}) evaluated {
switch v := x.(type) {
case nil:
// Interpret a nil pointer as an undefined value that is only
@@ -279,19 +287,42 @@
return toUint(ctx, src, uint64(v))
case uint64:
return toUint(ctx, src, uint64(v))
+ case uintptr:
+ return toUint(ctx, src, uint64(v))
case float64:
r := newNum(src, floatKind)
r.v.SetString(fmt.Sprintf("%g", v))
return r
+ case float32:
+ r := newNum(src, floatKind)
+ r.v.SetString(fmt.Sprintf("%g", v))
+ return r
case reflect.Value:
if v.CanInterface() {
- return convert(ctx, src, v.Interface())
+ return convertRec(ctx, src, v.Interface())
}
default:
value := reflect.ValueOf(v)
switch value.Kind() {
+ case reflect.Bool:
+ return &boolLit{src.base(), value.Bool()}
+
+ case reflect.String:
+ return &stringLit{src.base(), value.String(), nil}
+
+ case reflect.Int, reflect.Int8, reflect.Int16,
+ reflect.Int32, reflect.Int64:
+ return toInt(ctx, src, value.Int())
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16,
+ reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return toUint(ctx, src, value.Uint())
+
+ case reflect.Float32, reflect.Float64:
+ return convertRec(ctx, src, value.Float())
+
case reflect.Ptr:
if value.IsNil() {
// Interpret a nil pointer as an undefined value that is only
@@ -299,7 +330,7 @@
elem := goTypeToValue(ctx, false, reflect.TypeOf(v).Elem())
return makeNullable(elem, false).(evaluated)
}
- return convert(ctx, src, value.Elem().Interface())
+ return convertRec(ctx, src, value.Elem().Interface())
case reflect.Struct:
obj := newStruct(src)
@@ -313,7 +344,15 @@
if isOmitEmpty(&t) && isZero(val) {
continue
}
- sub := convert(ctx, src, val.Interface())
+ sub := convertRec(ctx, src, val.Interface())
+ if sub == nil {
+ // mimic behavior of encoding/json: skip fields of unsupported types
+ continue
+ }
+ if isBottom(sub) {
+ return sub
+ }
+
// leave errors like we do during normal evaluation or do we
// want to return the error?
name := getName(&t)
@@ -328,21 +367,49 @@
case reflect.Map:
obj := newStruct(src)
- t := value.Type()
- if t.Key().Kind() != reflect.String {
- return ctx.mkErr(src, "builtin map key not a string, but unsupported type %s", t.Key().String())
- }
+
+ sorted := []string{}
keys := []string{}
- for _, k := range value.MapKeys() {
- keys = append(keys, k.String())
+ t := value.Type()
+ switch key := t.Key(); key.Kind() {
+ case reflect.String,
+ reflect.Int, reflect.Int8, reflect.Int16,
+ reflect.Int32, reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16,
+ reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ for _, k := range value.MapKeys() {
+ s := fmt.Sprint(k)
+ keys = append(keys, s)
+ sorted = append(sorted, s)
+
+ val := value.MapIndex(k).Interface()
+ sub := convertRec(ctx, src, val)
+ // mimic behavior of encoding/json: report error of
+ // unsupported type.
+ if sub == nil {
+ return ctx.mkErr(baseValue{}, "unsupported Go type (%v)", val)
+ }
+ if isBottom(sub) {
+ return sub
+ }
+
+ // Set feature later.
+ obj.arcs = append(obj.arcs, arc{feature: 0, v: sub})
+ }
+
+ default:
+ return ctx.mkErr(baseValue{}, "unsupported Go type for map key (%v)", key)
}
- sort.Strings(keys)
- for _, k := range keys {
- sub := convert(ctx, src, value.MapIndex(reflect.ValueOf(k)).Interface())
- // leave errors like we do during normal evaluation or do we
- // want to return the error?
- f := ctx.strLabel(k)
- obj.arcs = append(obj.arcs, arc{feature: f, v: sub})
+
+ // Assign label in normalized order.
+ sort.Strings(sorted)
+ for _, k := range sorted {
+ ctx.strLabel(k)
+ }
+
+ // Now assign the labels to the arcs.
+ for i, k := range keys {
+ obj.arcs[i].feature = ctx.strLabel(k)
}
sort.Sort(obj)
return obj
@@ -351,7 +418,11 @@
list := &list{baseValue: src.base()}
arcs := []arc{}
for i := 0; i < value.Len(); i++ {
- x := convert(ctx, src, value.Index(i).Interface())
+ val := value.Index(i).Interface()
+ x := convertRec(ctx, src, val)
+ if x == nil {
+ return ctx.mkErr(baseValue{}, "unsupported Go type (%v)", val)
+ }
if isBottom(x) {
return x
}
@@ -365,7 +436,7 @@
return list
}
}
- return ctx.mkErr(src, "builtin returned unsupported type %T", x)
+ return nil
}
func toInt(ctx *context, src source, x int64) evaluated {
@@ -398,11 +469,33 @@
//
// TODO: if this value will always be unified with a concrete type in Go, then
// many of the fields may be omitted.
-func goTypeToValue(ctx *context, allowNullDefault bool, t reflect.Type) (e value) {
+func goTypeToValue(ctx *context, allowNullDefault bool, t reflect.Type) value {
+ v := goTypeToValueRec(ctx, allowNullDefault, t)
+ if v == nil {
+ return ctx.mkErr(baseValue{}, "unsupported Go type (%v)", t)
+ }
+ return v
+}
+
+func goTypeToValueRec(ctx *context, allowNullDefault bool, t reflect.Type) (e value) {
if e, ok := ctx.typeCache.Load(t); ok {
return e.(value)
}
+ switch reflect.Zero(t).Interface().(type) {
+ case *big.Int, big.Int:
+ e = &basicType{k: intKind}
+ goto store
+
+ case *big.Float, big.Float, *big.Rat, big.Rat:
+ e = &basicType{k: numKind}
+ goto store
+
+ case *apd.Decimal, apd.Decimal:
+ e = &basicType{k: numKind}
+ goto store
+ }
+
// Even if this is for types that we know cast to a certain type, it can't
// hurt to return top, as in these cases the concrete values will be
// strict instances and there cannot be any tags that further constrain
@@ -417,7 +510,7 @@
for elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
- e = goTypeToValue(ctx, false, elem)
+ e = goTypeToValueRec(ctx, false, elem)
if allowNullDefault {
e = wrapOrNull(e)
}
@@ -451,22 +544,6 @@
e = &basicType{k: floatKind}
case reflect.Struct:
- // Some of these values have MarshalJSON methods, but that may not be
- // the case for older Go versions.
- name := fmt.Sprint(t)
- switch name {
- case "big.Int":
- e = &basicType{k: intKind}
- goto store
- case "big.Rat", "big.Float", "apd.Decimal":
- e = &basicType{k: floatKind}
- goto store
- case "time.Time":
- // We let the concrete value decide.
- e = topSentinel
- goto store
- }
-
// First iterate to create struct, then iterate another time to
// resolve field tags to allow field tags to refer to the struct fields.
tags := map[label]string{}
@@ -479,8 +556,8 @@
continue
}
_, ok := f.Tag.Lookup("cue")
- elem := goTypeToValue(ctx, !ok, f.Type)
- if elem == nil {
+ elem := goTypeToValueRec(ctx, !ok, f.Type)
+ if elem == nil || isBottom(elem) {
continue // Ignore fields for unsupported types
}
@@ -506,6 +583,9 @@
for label, tag := range tags {
v := parseTag(ctx, obj, label, tag)
+ if isBottom(v) {
+ return v
+ }
for i, a := range obj.arcs {
if a.feature == label {
// Instead of unifying with the existing type, we substitute
@@ -522,7 +602,10 @@
if t.Elem().Kind() == reflect.Uint8 {
e = &basicType{k: bytesKind}
} else {
- elem := goTypeToValue(ctx, allowNullDefault, t.Elem())
+ elem := goTypeToValueRec(ctx, allowNullDefault, t.Elem())
+ if elem == nil {
+ return ctx.mkErr(baseValue{}, "unsupported Go type (%v)", t.Elem())
+ }
var ln value = &top{}
if t.Kind() == reflect.Array {
@@ -535,16 +618,24 @@
}
case reflect.Map:
- if key := t.Key(); key.Kind() != reflect.String {
- // What does the JSON library do here?
- e = ctx.mkErr(baseValue{}, "type %v not supported as key type", key)
- break
+ switch key := t.Key(); key.Kind() {
+ case reflect.String, reflect.Int, reflect.Int8, reflect.Int16,
+ reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8,
+ reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ default:
+ return ctx.mkErr(baseValue{}, "unsupported Go type for map key (%v)", key)
}
obj := newStruct(baseValue{})
sig := ¶ms{}
sig.add(ctx.label("_", true), &basicType{k: stringKind})
- v := goTypeToValue(ctx, allowNullDefault, t.Elem())
+ v := goTypeToValueRec(ctx, allowNullDefault, t.Elem())
+ if v == nil {
+ return ctx.mkErr(baseValue{}, "unsupported Go type (%v)", t.Elem())
+ }
+ if isBottom(v) {
+ return v
+ }
obj.template = &lambdaExpr{params: sig, value: v}
e = wrapOrNull(obj)
@@ -559,7 +650,7 @@
}
func wrapOrNull(e value) value {
- if e.kind().isAnyOf(nullKind) {
+ if e == nil || isBottom(e) || e.kind().isAnyOf(nullKind) {
return e
}
return makeNullable(e, true)
diff --git a/cue/go_test.go b/cue/go_test.go
index c632f8a..cedfbe8 100644
--- a/cue/go_test.go
+++ b/cue/go_test.go
@@ -22,6 +22,7 @@
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
+ "github.com/cockroachdb/apd/v2"
)
func TestConvert(t *testing.T) {
@@ -65,6 +66,10 @@
}, {
float64(3.1), "3.1",
}, {
+ float32(3.1), "3.1",
+ }, {
+ uintptr(3), "3",
+ }, {
&i34, "34",
}, {
&f34, "34",
@@ -82,9 +87,11 @@
"b": []int{3, 4},
}, "<0>{a: [1], b: [3,4]}",
}, {
- map[int]int{}, "_|_(builtin map key not a string, but unsupported type int)",
+ map[bool]int{}, "_|_(unsupported Go type for map key (bool))",
}, {
- map[int]int{1: 2}, "_|_(builtin map key not a string, but unsupported type int)",
+ map[struct{}]int{struct{}{}: 2}, "_|_(unsupported Go type for map key (struct {}))",
+ }, {
+ map[int]int{1: 2}, "<0>{1: 2}",
}, {
struct {
a int
@@ -146,14 +153,14 @@
}{},
// TODO: indicate that B is explicitly an int only.
`<0>{A: ((int & >=-9223372036854775808 & int & <=9223372036854775807) & (>=0 & <100)), ` +
- `B: >=0, ` +
- `C?: _, ` +
+ `B: (int & >=0), ` +
+ `C?: int, ` +
`D: int, ` +
- `F?: _}`,
+ `F?: number}`,
}, {
&struct {
A int16 `cue:">=0&<100"`
- B error `json:"b"`
+ B error `json:"b,"`
C string
D bool
F float64
@@ -170,6 +177,21 @@
`T: _})`,
}, {
struct {
+ A int `cue:"<"` // invalid
+ }{},
+ "_|_(invalid tag \"<\" for field \"A\": expected operand, found 'EOF' )",
+ }, {
+ struct {
+ A int `json:"-"` // skip
+ D *apd.Decimal
+ P ***apd.Decimal
+ I interface{ Foo() }
+ T string `cue:""` // allowed
+ h int
+ }{},
+ "<0>{D?: number, T: string, P?: (*null | number), I?: _}",
+ }, {
+ struct {
A int8 `cue:"C-B"`
B int8 `cue:"C-A,opt"`
C int8 `cue:"A+B"`
@@ -185,6 +207,9 @@
[4]string{},
`4*[string]`,
}, {
+ []func(){},
+ "_|_(unsupported Go type (func()))",
+ }, {
map[string]struct{ A map[string]uint }{},
`(*null | ` +
`<0>{<>: <1>(_: string)-><2>{` +
@@ -192,7 +217,16 @@
`<3>{<>: <4>(_: string)->(int & >=0 & int & <=18446744073709551615), })}, })`,
}, {
map[float32]int{},
- `_|_(type float32 not supported as key type)`,
+ `_|_(unsupported Go type for map key (float32))`,
+ }, {
+ map[int]map[float32]int{},
+ `_|_(unsupported Go type for map key (float32))`,
+ }, {
+ map[int]func(){},
+ `_|_(unsupported Go type (func()))`,
+ }, {
+ time.Now, // a function
+ "_|_(unsupported Go type (func() time.Time))",
}}
inst := getInstance(t, "foo")
diff --git a/cue/kind.go b/cue/kind.go
index 9deac1b..cae1ea0 100644
--- a/cue/kind.go
+++ b/cue/kind.go
@@ -246,7 +246,7 @@
return bottomKind, false, msg
}
switch {
- case a&nonGround == 0 && b&nonGround == 0:
+ case aGround && bGround:
// both ground values: nothing to do
case op != opUnify && op != opLand && op != opLor && op != opNeq:
diff --git a/cuego/cuego.go b/cuego/cuego.go
index 5dd44cc..6f8e35c 100644
--- a/cuego/cuego.go
+++ b/cuego/cuego.go
@@ -178,6 +178,9 @@
mutex.Lock()
v = internal.FromGoValue(runtime, x).(cue.Value)
mutex.Unlock()
+ if err := v.Err(); err != nil {
+ return v, err
+ }
return v, nil
// // This should be equivalent to the following: