cmd/{cloner,viewer},types/views: add viewer and cloner support for slices and maps of both structs (rather than pointers to structs) and views

In this PR we add viewer/cloner codegen support for the following field types, where View is an existing,
likely generated, view type and T is a struct (rather than a pointer to struct).
- []T
- []View
- map[K]View

To support []T, we introduce the generic view type view.ValueSliceView[T, P, V].
Here, P (i.e. *T) implements ViewCloner[*T, V], and V is the concrete view type for T.

For the slices and maps of views we use existing views.Slice and view.Map view types.

This is mostly done in a preparation for generating netmap.NetworkMapView.

Updates #12614
Updates tailscale/corp#27502

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2025-04-17 17:57:50 -05:00
parent dda2c0d2c2
commit 20d6058e7b
No known key found for this signature in database
6 changed files with 111 additions and 6 deletions

View File

@ -150,6 +150,8 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
writef("\tdst.%s[i] = append(src.%s[i][:0:0], src.%s[i]...)", fname, fname, fname)
} else if _, isIface := ft.Elem().Underlying().(*types.Interface); isIface {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
} else if codegen.IsViewType(ft.Elem()) {
writef("\tdst.%s[i] = src.%s[i]", fname, fname)
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}
@ -187,7 +189,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
writef("\t\tdst.%s[k] = append([]%s{}, src.%s[k]...)", fname, n, fname)
writef("\t}")
writef("}")
} else if codegen.ContainsPointers(elem) {
} else if codegen.ContainsPointers(elem) && !codegen.IsViewType(elem) {
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(elem))
writef("\tfor k, v := range src.%s {", fname)

View File

@ -30,6 +30,7 @@ type Map struct {
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
StructWithoutPtrKey map[StructWithoutPtrs]int `json:"-"`
StructWithPtr map[string]StructWithPtrs
StructWithView map[string]StructWithPtrsView
// Unsupported views.
SliceIntPtr map[string][]*int
@ -63,10 +64,11 @@ type StructWithSlices struct {
Slice []string
Prefixes []netip.Prefix
Data []byte
Structs []StructWithPtrs
Views []StructWithPtrsView
// Unsupported views.
Structs []StructWithPtrs
Ints []*int
Ints []*int
}
type OnlyGetClone struct {

View File

@ -114,6 +114,7 @@ func (src *Map) Clone() *Map {
dst.StructWithPtr[k] = *(v.Clone())
}
}
dst.StructWithView = maps.Clone(src.StructWithView)
if dst.SliceIntPtr != nil {
dst.SliceIntPtr = map[string][]*int{}
for k := range src.SliceIntPtr {
@ -136,6 +137,7 @@ var _MapCloneNeedsRegeneration = Map(struct {
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
StructWithoutPtrKey map[StructWithoutPtrs]int
StructWithPtr map[string]StructWithPtrs
StructWithView map[string]StructWithPtrsView
SliceIntPtr map[string][]*int
PointerKey map[*string]int
StructWithPtrKey map[StructWithPtrs]int
@ -179,6 +181,12 @@ func (src *StructWithSlices) Clone() *StructWithSlices {
dst.Structs[i] = *src.Structs[i].Clone()
}
}
if src.Views != nil {
dst.Views = make([]StructWithPtrsView, len(src.Views))
for i := range dst.Views {
dst.Views[i] = src.Views[i]
}
}
if src.Ints != nil {
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
@ -201,6 +209,7 @@ var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct {
Prefixes []netip.Prefix
Data []byte
Structs []StructWithPtrs
Views []StructWithPtrsView
Ints []*int
}{})

View File

@ -220,6 +220,10 @@ func (v MapView) StructWithPtr() views.MapFn[string, StructWithPtrs, StructWithP
return t.View()
})
}
func (v MapView) StructWithView() views.Map[string, StructWithPtrsView] {
return views.MapOf(v.ж.StructWithView)
}
func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") }
func (v MapView) PointerKey() map[*string]int { panic("unsupported") }
func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") }
@ -235,6 +239,7 @@ var _MapViewNeedsRegeneration = Map(struct {
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
StructWithoutPtrKey map[StructWithoutPtrs]int
StructWithPtr map[string]StructWithPtrs
StructWithView map[string]StructWithPtrsView
SliceIntPtr map[string][]*int
PointerKey map[*string]int
StructWithPtrKey map[StructWithPtrs]int
@ -299,8 +304,13 @@ func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] {
return views.SliceOf(v.ж.Prefixes)
}
func (v StructWithSlicesView) Data() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Data) }
func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") }
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
func (v StructWithSlicesView) Structs() views.ValueSliceView[StructWithPtrs, *StructWithPtrs, StructWithPtrsView] {
return views.SliceOfValueViews[StructWithPtrs, *StructWithPtrs](v.ж.Structs)
}
func (v StructWithSlicesView) Views() views.Slice[StructWithPtrsView] {
return views.SliceOf(v.ж.Views)
}
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
@ -311,6 +321,7 @@ var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
Prefixes []netip.Prefix
Data []byte
Structs []StructWithPtrs
Views []StructWithPtrsView
Ints []*int
}{})

View File

@ -75,6 +75,8 @@ func (v *{{.ViewName}}{{.TypeParamNames}}) UnmarshalJSON(b []byte) error {
{{end}}
{{define "viewSliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
{{end}}
{{define "viewValueSliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.ValueSliceView[{{.FieldType}},*{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfValueViews[{{.FieldType}},*{{.FieldType}}](v.ж.{{.FieldName}}) }
{{end}}
{{define "viewField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldViewName}} { return v.ж.{{.FieldName}}.View() }
{{end}}
{{define "makeViewField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldViewName}} { return {{.MakeViewFnName}}(&v.ж.{{.FieldName}}) }
@ -108,6 +110,9 @@ func init() {
}
func requiresCloning(t types.Type) (shallow, deep bool, base types.Type) {
if codegen.IsViewType(t) {
return false, false, t
}
switch v := t.(type) {
case *types.Pointer:
_, deep, base = requiresCloning(v.Elem())
@ -198,6 +203,10 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ *
writeTemplate("unsupportedField")
}
continue
case *types.Struct:
args.FieldViewName = appendNameSuffix(it.QualifiedName(elem), "View")
writeTemplate("viewValueSliceField")
continue
case *types.Interface:
if viewType := viewTypeForValueType(elem); viewType != nil {
args.FieldViewName = it.QualifiedName(viewType)
@ -260,7 +269,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ *
case *types.Struct, *types.Named, *types.Alias:
strucT := u
args.FieldType = it.QualifiedName(fieldType)
if codegen.ContainsPointers(strucT) {
if codegen.ContainsPointers(strucT) && !codegen.IsViewType(strucT) {
args.MapFn = "t.View()"
template = "mapFnField"
args.MapValueType = it.QualifiedName(mElem)

View File

@ -822,6 +822,78 @@ func (p *ValuePointer[T]) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &p.ж)
}
// ViewClonerPointer is a constraint that permits pointer types
// implementing the [ViewCloner] interface.
type ViewClonerPointer[T any, V StructView[*T]] interface {
ViewCloner[*T, V]
*T
}
// SliceOfValueViews returns a [ValueSliceView] for x.
// It is like [SliceOfViews], but x is a slice of values whose pointers
// implement the [ViewCloner] interface rather than a slice of pointers.
func SliceOfValueViews[T any, P ViewClonerPointer[T, V], V StructView[*T]](x []T) ValueSliceView[T, P, V] {
return ValueSliceView[T, P, V]{x}
}
// ValueSliceView is like [SliceView], but wraps a slice of values
// (whose pointers implement the [ViewCloner] interface) instead of a slice of pointers.
// In other words, the [ViewCloner] interface must be implemented by *T rather than T.
type ValueSliceView[
T any,
P ViewClonerPointer[T, V],
V StructView[*T],
] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж []T
}
// All returns an iterator over v.
func (v ValueSliceView[T, P, V]) All() iter.Seq2[int, V] {
return func(yield func(int, V) bool) {
for i := range v.ж {
if !yield(i, P(&v.ж[i]).View()) {
return
}
}
}
}
// MarshalJSON implements json.Marshaler.
func (v ValueSliceView[T, P, V]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// UnmarshalJSON implements json.Unmarshaler.
func (v *ValueSliceView[T, P, V]) UnmarshalJSON(b []byte) error {
return unmarshalSliceFromJSON(b, &v.ж)
}
// IsNil reports whether the underlying slice is nil.
func (v ValueSliceView[T, P, V]) IsNil() bool { return v.ж == nil }
// Len returns the length of the slice.
func (v ValueSliceView[T, P, V]) Len() int { return len(v.ж) }
// At returns a View of the element at index `i` of the slice.
func (v ValueSliceView[T, P, V]) At(i int) V { return P(&v.ж[i]).View() }
// SliceFrom returns v[i:].
func (v ValueSliceView[T, P, V]) SliceFrom(i int) ValueSliceView[T, P, V] {
return ValueSliceView[T, P, V]{v.ж[i:]}
}
// SliceTo returns v[:i].
func (v ValueSliceView[T, P, V]) SliceTo(i int) ValueSliceView[T, P, V] {
return ValueSliceView[T, P, V]{v.ж[:i]}
}
// Slice returns v[i:j].
func (v ValueSliceView[T, P, V]) Slice(i, j int) ValueSliceView[T, P, V] {
return ValueSliceView[T, P, V]{v.ж[i:j]}
}
// ContainsPointers reports whether T contains any pointers,
// either explicitly or implicitly.
// It has special handling for some types that contain pointers