/
zero.go
383 lines (362 loc) · 13 KB
/
zero.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
// 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
}