blob: e600ca23653891365d3a357bd85cab1485c1d8d7 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package golang
import (
"fmt"
"path"
"strings"
"v.io/v23/vdl"
"v.io/v23/vdlroot/vdltool"
"v.io/x/ref/lib/vdl/compile"
)
// defineIsZero returns the VDLIsZero method for the def type.
func defineIsZero(data *goData, def *compile.TypeDef) string {
g := genIsZero{goData: data}
return g.Gen(def)
}
type genIsZero struct {
*goData
}
func (g *genIsZero) Gen(def *compile.TypeDef) string {
// Special-case array and struct to check each elem or field for zero.
// Special-case union to generate methods for each concrete union struct.
//
// Types that have a unique Go zero value can bypass these special-cases, and
// perform a direct comparison against that zero value. Note that def.Type
// always represents a wire type here, since we're generating the VDLIsZero
// method on the wire type.
if !isGoZeroValueUniqueWire(g.goData, def.Type) {
switch def.Type.Kind() {
case vdl.Array:
return g.genArrayDef(def)
case vdl.Struct:
return g.genStructDef(def)
case vdl.Union:
return g.genUnionDef(def)
}
}
return g.genDef(def)
}
func (g *genIsZero) genDef(def *compile.TypeDef) string {
tt, arg := def.Type, namedArg{"x", false}
expr := g.ExprWire(returnEqZero, tt, arg, "")
return fmt.Sprintf(`
func (x %[1]s) VDLIsZero() bool {
return %[2]s
}
`, def.Name, expr)
}
func (g *genIsZero) genArrayDef(def *compile.TypeDef) string {
tt := def.Type
elemArg := typedArg("elem", tt.Elem())
expr := g.Expr(ifNeZero, tt.Elem(), elemArg, "")
return fmt.Sprintf(`
func (x %[1]s) VDLIsZero() bool {
for _, elem := range x {
if %[2]s {
return false
}
}
return true
}
`, def.Name, expr)
}
func (g *genIsZero) genStructDef(def *compile.TypeDef) string {
tt, arg := def.Type, namedArg{"x", false}
s := fmt.Sprintf(`
func (x %[1]s) VDLIsZero() bool {`, def.Name)
for ix := 0; ix < tt.NumField(); ix++ {
field := tt.Field(ix)
expr := g.Expr(ifNeZero, field.Type, arg.Field(field), field.Name)
s += fmt.Sprintf(`
if %[1]s {
return false
}`, expr)
}
s += `
return true
}
`
return s
}
func (g *genIsZero) genUnionDef(def *compile.TypeDef) string {
// The 0th field needs a real zero check.
tt := def.Type
field0 := tt.Field(0)
fieldArg := typedArg("x.Value", field0.Type)
expr := g.Expr(returnEqZero, field0.Type, fieldArg, "")
s := fmt.Sprintf(`
func (x %[1]s%[2]s) VDLIsZero() bool {
return %[3]s
}
`, def.Name, field0.Name, expr)
// All other fields simply return false.
for ix := 1; ix < tt.NumField(); ix++ {
s += fmt.Sprintf(`
func (x %[1]s%[2]s) VDLIsZero() bool {
return false
}
`, def.Name, tt.Field(ix).Name)
}
return s
}
// zeroExpr configures what type of zero expression to generate.
type zeroExpr int
const (
ifEqZero = iota // Generate expression for "if x == 0 {" statement
ifNeZero // Generate expression for "if x != 0 {" statement
returnEqZero // Generate expression for "return x == 0" statement
)
func (ze zeroExpr) GenEqual() bool {
return ze == ifEqZero || ze == returnEqZero
}
func (ze zeroExpr) GenNotEqual() bool {
return !ze.GenEqual()
}
func (ze zeroExpr) GenIfStmt() bool {
return ze == ifEqZero || ze == ifNeZero
}
func (ze zeroExpr) GenReturnStmt() bool {
return !ze.GenIfStmt()
}
// Expr generates the Go code to check whether the arg, which has type tt, is
// equal or not-equal to zero. The tmp string is appended to temporary variable
// names to make them unique.
//
// The returned expression is a boolean Go expression that evaluates whether arg
// is zero or non-zero. It is meant to be used like this:
// if <expr> {
// ...
// }
// Or like this:
// return <expr>
//
// The first argument ze describes whether the expression will be used in an
// "if" or "return" statement, and whether it should evaluate to equal or
// not-equal to zero. The kind of statement affects the expression because of
// Go's parsing and type safety rules.
func (g *genIsZero) Expr(ze zeroExpr, tt *vdl.Type, arg namedArg, tmp string) string {
if native, wirePkg, ok := findNativeType(g.Env, tt); ok {
opNot, eq, ref := "", "==", arg.Ref()
if ze.GenNotEqual() {
opNot, eq = "!", "!="
}
switch {
case native.Zero.Mode == vdltool.GoZeroModeUnique:
// We use an untyped const as the zero value, because Go only allows
// comparison of slices with nil. E.g.
// type MySlice []string
// pass := MySlice(nil) == nil // valid
// fail := MySlice(nil) == MySlice(nil) // invalid
nType := nativeType(g.goData, native, wirePkg)
zeroValue := untypedConstNativeZero(native.Kind, nType)
if ze.GenIfStmt() {
if k := native.Kind; k == vdltool.GoKindStruct || k == vdltool.GoKindArray {
// Without a special-case, we'll get a statement like:
// if x == Foo{} {
// But that isn't valid Go code, so we change it to:
// if x == (Foo{}) {
zeroValue = "(" + zeroValue + ")"
}
}
return ref + eq + zeroValue
case strings.HasPrefix(native.Zero.IsZero, "."):
return opNot + arg.Name + native.Zero.IsZero
case native.Zero.IsZero != "":
// TODO(toddw): Handle the function form of IsZero, including IsZeroImports.
vdlconfig := path.Join(wirePkg.GenPath, "vdl.config")
g.Env.Errors.Errorf("%s: native type %s uses function form of IsZero, which isn't implemented", vdlconfig, native.Type)
return ""
}
}
return g.ExprWire(ze, tt, arg, tmp)
}
// ExprWire is like Expr, but generates code for the wire type tt.
func (g *genIsZero) ExprWire(ze zeroExpr, tt *vdl.Type, arg namedArg, tmp string) string {
opNot, eq, cond, ref := "", "==", "||", arg.Ref()
if ze.GenNotEqual() {
opNot, eq, cond = "!", "!=", "&&"
}
// Handle everything other than Array and Struct.
switch tt.Kind() {
case vdl.Bool:
expr := "!" + ref // false is zero, while true is non-zero
if ze.GenNotEqual() {
expr = ref
}
if ze.GenReturnStmt() && tt.Name() != "" {
// Special-case named bool types, since we'll get an expression like "x",
// but we need an explicit conversion since the type of x is the named
// bool type, not the built-in bool type.
expr = "bool(" + expr + ")"
}
return expr
case vdl.String:
return ref + eq + `""`
case vdl.Byte, vdl.Uint16, vdl.Uint32, vdl.Uint64, vdl.Int8, vdl.Int16, vdl.Int32, vdl.Int64, vdl.Float32, vdl.Float64:
return ref + eq + "0"
case vdl.Enum:
return ref + eq + typeGoWire(g.goData, tt) + tt.EnumLabel(0)
case vdl.TypeObject:
return ref + eq + "nil" + cond + ref + eq + g.Pkg("v.io/v23/vdl") + "AnyType"
case vdl.List, vdl.Set, vdl.Map:
return "len(" + ref + ")" + eq + "0"
case vdl.Optional:
return arg.Name + eq + "nil"
}
// The interface{} representation of any is special-cased, since it behaves
// differently than the *vdl.Value and *vom.RawBytes representations.
if tt.Kind() == vdl.Any && goAnyRepMode(g.Package) == goAnyRepInterface {
return ref + eq + "nil"
}
switch tt.Kind() {
case vdl.Union, vdl.Any:
// Union is always named, and Any is either *vdl.Value or *vom.RawBytes, so
// we call VDLIsZero directly. A slight complication is the fact that all
// of these might be nil, which we need to protect against before making the
// VDLIsZero call.
return ref + eq + "nil" + cond + opNot + arg.Name + ".VDLIsZero()"
}
// Only Array and Struct are left.
//
// If there is a unique Go zero value, we generate a fastpath that simply
// compares against that value. Note that tt always represents a wire type
// here, since native types were handled in Expr.
if isGoZeroValueUniqueWire(g.goData, tt) {
zeroValue := typedConstWire(g.goData, vdl.ZeroValue(tt))
if ze.GenIfStmt() {
// Without a special-case, we'll get a statement like:
// if x == Foo{} {
// But that isn't valid Go code, so we change it to:
// if x == (Foo{}) {
zeroValue = "(" + zeroValue + ")"
}
return ref + eq + zeroValue
}
// Otherwise we call VDLIsZero directly. This takes advantage of the fact
// that Array and Struct are always named, so will always have a VDLIsZero
// method defined.
return opNot + arg.Name + ".VDLIsZero()"
}
// isGoZeroValueUniqueWire returns true iff the Go zero value of the wire type
// tt represents the VDL zero value, and is the *only* value that represents the
// VDL zero value.
func isGoZeroValueUniqueWire(data *goData, tt *vdl.Type) bool {
// Not unique if tt contains inline native subtypes that don't have a unique
// zero representation. This doesn't apply if tt itself is native, but has no
// inline native subtypes, since this function only considers the wire type
// form of tt.
if containsInlineNativeNonUniqueSubTypes(data, tt, true) {
return false
}
// Not unique if tt contains types where there is more than one VDL zero value
// representation:
// Any: nil, or VDLIsZero on vdl.Value/vom.RawBytes
// TypeObject: nil, or AnyType
// Union: nil, or zero value of field 0
// List, Set, Map: nil, or empty
//
// Note that the interface{} representation of Any uses nil as the only VDL
// zero value, while the vdl.Value/vom.RawBytes pointers can either be nil, or
// represent VDL zero through their non-nil pointer.
kkNotUnique := []vdl.Kind{vdl.TypeObject, vdl.Union, vdl.List, vdl.Set, vdl.Map}
if goAnyRepMode(data.Package) != goAnyRepInterface {
kkNotUnique = append(kkNotUnique, vdl.Any)
}
if tt.ContainsKind(vdl.WalkInline, kkNotUnique...) {
return false
}
return true
}
// isGoZeroValueCanonical returns true iff the Go zero value of the type tt
// (which may be a native type) is the canonical representation of the VDL zero
// value. This differs from isGoZeroValueUniqueWire since e.g. the canonical
// zero value of list is nil, which is the Go zero value, but it isn't unique
// since it isn't the only zero value representation. Also this checks if tt is
// a native type, while isGoZeroValueUniqueWire assumes tt is a wire type.
func isGoZeroValueCanonical(data *goData, tt *vdl.Type) bool {
// If tt is a native type in either Canonical or Unique zero mode, the Go zero
// value is canonical. Note that Unique zero mode is stronger than Canonical;
// not only is the Go zero value canonical, it's the only representation.
if native, _, ok := findNativeType(data.Env, tt); ok {
if native.Zero.Mode != vdltool.GoZeroModeUnknown {
return true
}
}
// Not canonical if tt contains inline native subtypes that don't have a
// Canonical or Unique zero mode.
if containsInlineNativeUnknownSubTypes(data, tt, false) {
return false
}
// Not canonical if tt contains types where the Go zero value isn't the
// canonical VDL zero value.
//
// Note that The interface{} representation of Any uses nil as the only VDL
// zero value, while the vdl.Value/vom.RawBytes pointers use a non-nil pointer
// as the canonical VDL zero value.
kkNotCanonical := []vdl.Kind{vdl.TypeObject, vdl.Union}
if goAnyRepMode(data.Package) != goAnyRepInterface {
kkNotCanonical = append(kkNotCanonical, vdl.Any)
}
if tt.ContainsKind(vdl.WalkInline, kkNotCanonical...) {
return false
}
return true
}
func containsInlineNativeNonUniqueSubTypes(data *goData, tt *vdl.Type, wireOnly bool) bool {
// The walk early-exits if the visitor functor returns false. We want the
// early-exit when we detect the first native type, so we use false to mean
// that we've seen a native type, and true if we haven't.
return !tt.Walk(vdl.WalkInline, func(visit *vdl.Type) bool {
if wireOnly && visit == tt {
// We don't want the native check to fire for tt itself, so we return true
// when we visit tt, meaning we haven't detected a native type yet.
return true
}
if native, _, ok := findNativeType(data.Env, visit); ok {
return native.Zero.Mode == vdltool.GoZeroModeUnique
}
return true
})
}
func containsInlineNativeUnknownSubTypes(data *goData, tt *vdl.Type, wireOnly bool) bool {
// The walk early-exits if the visitor functor returns false. We want the
// early-exit when we detect the first native type, so we use false to mean
// that we've seen a native type, and true if we haven't.
return !tt.Walk(vdl.WalkInline, func(visit *vdl.Type) bool {
if wireOnly && visit == tt {
// We don't want the native check to fire for tt itself, so we return true
// when we visit tt, meaning we haven't detected a native type yet.
return true
}
if native, _, ok := findNativeType(data.Env, visit); ok {
return native.Zero.Mode != vdltool.GoZeroModeUnknown
}
return true
})
}
func findNativeType(env *compile.Env, tt *vdl.Type) (vdltool.GoType, *compile.Package, bool) {
if def := env.FindTypeDef(tt); def != nil {
pkg := def.File.Package
key := def.Name
if tt.Kind() == vdl.Optional {
key = "*" + key
}
native, ok := pkg.Config.Go.WireToNativeTypes[key]
return native, pkg, ok
}
return vdltool.GoType{}, nil, false
}
// isNativeType returns true iff t is a native type.
func isNativeType(env *compile.Env, t *vdl.Type) bool {
if def := env.FindTypeDef(t); def != nil {
key := def.Name
if t.Kind() == vdl.Optional {
key = "*" + key
}
_, ok := def.File.Package.Config.Go.WireToNativeTypes[key]
return ok
}
return false
}