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