Skip to content

Commit 4ec52e3

Browse files
committed
go/types: add check that code is monomorphizable
This CL adds a check to ensure that generic Go code doesn't involve any unbounded recursive instantiation, which are incompatible with an implementation that uses static instantiation (i.e., monomorphization or compile-time dictionary construction). Updates #48098. Change-Id: I9d051f0f9369ab881592a361a5d0e2a716788a6b Reviewed-on: https://go-review.googlesource.com/c/go/+/357449 Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Robert Griesemer <gri@golang.org> Trust: Matthew Dempsky <mdempsky@google.com>
1 parent 7548327 commit 4ec52e3

File tree

8 files changed

+442
-1
lines changed

8 files changed

+442
-1
lines changed

src/go/types/call.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func (check *Checker) instantiateSignature(pos token.Pos, typ *Signature, targs
9191
pos = posList[i]
9292
}
9393
check.softErrorf(atPos(pos), _Todo, err.Error())
94+
} else {
95+
check.mono.recordInstance(check.pkg, pos, tparams, targs, posList)
9496
}
9597

9698
return inst

src/go/types/check.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ type Checker struct {
129129
imports []*PkgName // list of imported packages
130130
dotImportMap map[dotImportKey]*PkgName // maps dot-imported objects to the package they were dot-imported through
131131
recvTParamMap map[*ast.Ident]*TypeParam // maps blank receiver type parameters to their type
132+
mono monoGraph // graph for detecting non-monomorphizable instantiation loops
132133

133134
firstErr error // first error encountered
134135
methods map[*TypeName][]*Func // maps package scope type names to associated non-blank (non-interface) methods
@@ -306,6 +307,11 @@ func (check *Checker) checkFiles(files []*ast.File) (err error) {
306307

307308
check.recordUntyped()
308309

310+
if check.firstErr == nil {
311+
// TODO(mdempsky): Ensure monomorph is safe when errors exist.
312+
check.monomorph()
313+
}
314+
309315
check.pkg.complete = true
310316

311317
// no longer needed - release memory

src/go/types/errorcodes.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,13 @@ const (
13011301
// var _ = unsafe.Slice(&x, uint64(1) << 63)
13021302
_InvalidUnsafeSlice
13031303

1304+
// _InvalidInstanceCycle occurs when an invalid cycle is detected
1305+
// within the instantiation graph.
1306+
//
1307+
// Example:
1308+
// func f[T any]() { f[*T]() }
1309+
_InvalidInstanceCycle
1310+
13041311
// _Todo is a placeholder for error codes that have not been decided.
13051312
// TODO(rFindley) remove this error code after deciding on errors for generics code.
13061313
_Todo

src/go/types/mono.go

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package types
6+
7+
import (
8+
"go/token"
9+
)
10+
11+
// This file implements a check to validate that a Go package doesn't
12+
// have unbounded recursive instantiation, which is not compatible
13+
// with compilers using static instantiation (such as
14+
// monomorphization).
15+
//
16+
// It implements a sort of "type flow" analysis by detecting which
17+
// type parameters are instantiated with other type parameters (or
18+
// types derived thereof). A package cannot be statically instantiated
19+
// if the graph has any cycles involving at least one derived type.
20+
//
21+
// Concretely, we construct a directed, weighted graph. Vertices are
22+
// used to represent type parameters as well as some defined
23+
// types. Edges are used to represent how types depend on each other:
24+
//
25+
// * Everywhere a type-parameterized function or type is instantiated,
26+
// we add edges to each type parameter from the vertices (if any)
27+
// representing each type parameter or defined type referenced by
28+
// the type argument. If the type argument is just the referenced
29+
// type itself, then the edge has weight 0, otherwise 1.
30+
//
31+
// * For every defined type declared within a type-parameterized
32+
// function or method, we add an edge of weight 1 to the defined
33+
// type from each ambient type parameter.
34+
//
35+
// For example, given:
36+
//
37+
// func f[A, B any]() {
38+
// type T int
39+
// f[T, map[A]B]()
40+
// }
41+
//
42+
// we construct vertices representing types A, B, and T. Because of
43+
// declaration "type T int", we construct edges T<-A and T<-B with
44+
// weight 1; and because of instantiation "f[T, map[A]B]" we construct
45+
// edges A<-T with weight 0, and B<-A and B<-B with weight 1.
46+
//
47+
// Finally, we look for any positive-weight cycles. Zero-weight cycles
48+
// are allowed because static instantiation will reach a fixed point.
49+
50+
type monoGraph struct {
51+
vertices []monoVertex
52+
edges []monoEdge
53+
54+
// canon maps method receiver type parameters to their respective
55+
// receiver type's type parameters.
56+
canon map[*TypeParam]*TypeParam
57+
58+
// nameIdx maps a defined type or (canonical) type parameter to its
59+
// vertex index.
60+
nameIdx map[*TypeName]int
61+
}
62+
63+
type monoVertex struct {
64+
weight int // weight of heaviest known path to this vertex
65+
pre int // previous edge (if any) in the above path
66+
len int // length of the above path
67+
68+
// obj is the defined type or type parameter represented by this
69+
// vertex.
70+
obj *TypeName
71+
}
72+
73+
type monoEdge struct {
74+
dst int
75+
src int
76+
weight int
77+
78+
// report emits an error describing why this edge exists.
79+
//
80+
// TODO(mdempsky): Avoid requiring a function closure for each edge.
81+
report func(check *Checker)
82+
}
83+
84+
func (check *Checker) monomorph() {
85+
// We detect unbounded instantiation cycles using a variant of
86+
// Bellman-Ford's algorithm. Namely, instead of always running |V|
87+
// iterations, we run until we either reach a fixed point or we've
88+
// found a path of length |V|. This allows us to terminate earlier
89+
// when there are no cycles, which should be the common case.
90+
91+
again := true
92+
for again {
93+
again = false
94+
95+
for i, edge := range check.mono.edges {
96+
src := &check.mono.vertices[edge.src]
97+
dst := &check.mono.vertices[edge.dst]
98+
99+
// N.B., we're looking for the greatest weight paths, unlike
100+
// typical Bellman-Ford.
101+
w := src.weight + edge.weight
102+
if w <= dst.weight {
103+
continue
104+
}
105+
106+
dst.pre = i
107+
dst.len = src.len + 1
108+
if dst.len == len(check.mono.vertices) {
109+
check.reportInstanceLoop(edge.dst)
110+
return
111+
}
112+
113+
dst.weight = w
114+
again = true
115+
}
116+
}
117+
}
118+
119+
func (check *Checker) reportInstanceLoop(v int) {
120+
var stack []int
121+
seen := make([]bool, len(check.mono.vertices))
122+
123+
// We have a path that contains a cycle and ends at v, but v may
124+
// only be reachable from the cycle, not on the cycle itself. We
125+
// start by walking backwards along the path until we find a vertex
126+
// that appears twice.
127+
for !seen[v] {
128+
stack = append(stack, v)
129+
seen[v] = true
130+
v = check.mono.edges[check.mono.vertices[v].pre].src
131+
}
132+
133+
// Trim any vertices we visited before visiting v the first
134+
// time. Since v is the first vertex we found within the cycle, any
135+
// vertices we visited earlier cannot be part of the cycle.
136+
for stack[0] != v {
137+
stack = stack[1:]
138+
}
139+
140+
// TODO(mdempsky): Pivot stack so we report the cycle from the top?
141+
142+
obj := check.mono.vertices[v].obj
143+
check.errorf(obj, _InvalidInstanceCycle, "instantiation cycle:")
144+
145+
for _, v := range stack {
146+
edge := check.mono.edges[check.mono.vertices[v].pre]
147+
edge.report(check)
148+
}
149+
}
150+
151+
// recordCanon records that tpar is the canonical type parameter
152+
// corresponding to method type parameter mpar.
153+
func (w *monoGraph) recordCanon(mpar, tpar *TypeParam) {
154+
if w.canon == nil {
155+
w.canon = make(map[*TypeParam]*TypeParam)
156+
}
157+
w.canon[mpar] = tpar
158+
}
159+
160+
// recordInstance records that the given type parameters were
161+
// instantiated with the corresponding type arguments.
162+
func (w *monoGraph) recordInstance(pkg *Package, pos token.Pos, tparams []*TypeParam, targs []Type, posList []token.Pos) {
163+
for i, tpar := range tparams {
164+
pos := pos
165+
if i < len(posList) {
166+
pos = posList[i]
167+
}
168+
w.assign(pkg, pos, tpar, targs[i])
169+
}
170+
}
171+
172+
// assign records that tpar was instantiated as targ at pos.
173+
func (w *monoGraph) assign(pkg *Package, pos token.Pos, tpar *TypeParam, targ Type) {
174+
// Go generics do not have an analog to C++`s template-templates,
175+
// where a template parameter can itself be an instantiable
176+
// template. So any instantiation cycles must occur within a single
177+
// package. Accordingly, we can ignore instantiations of imported
178+
// type parameters.
179+
//
180+
// TODO(mdempsky): Push this check up into recordInstance? All type
181+
// parameters in a list will appear in the same package.
182+
if tpar.Obj().Pkg() != pkg {
183+
return
184+
}
185+
186+
// flow adds an edge from vertex src representing that typ flows to tpar.
187+
flow := func(src int, typ Type) {
188+
weight := 1
189+
if typ == targ {
190+
weight = 0
191+
}
192+
193+
w.addEdge(w.typeParamVertex(tpar), src, weight, func(check *Checker) {
194+
qf := RelativeTo(check.pkg)
195+
check.errorf(atPos(pos), _InvalidInstanceCycle, "\t%s instantiated as %s", tpar.Obj().Name(), TypeString(targ, qf)) // secondary error, \t indented
196+
})
197+
}
198+
199+
// Recursively walk the type argument to find any defined types or
200+
// type parameters.
201+
var do func(typ Type)
202+
do = func(typ Type) {
203+
switch typ := typ.(type) {
204+
default:
205+
panic("unexpected type")
206+
207+
case *TypeParam:
208+
assert(typ.Obj().Pkg() == pkg)
209+
flow(w.typeParamVertex(typ), typ)
210+
211+
case *Named:
212+
if src := w.localNamedVertex(pkg, typ.Origin()); src >= 0 {
213+
flow(src, typ)
214+
}
215+
216+
targs := typ.TypeArgs()
217+
for i := 0; i < targs.Len(); i++ {
218+
do(targs.At(i))
219+
}
220+
221+
case *Array:
222+
do(typ.Elem())
223+
case *Basic:
224+
// ok
225+
case *Chan:
226+
do(typ.Elem())
227+
case *Map:
228+
do(typ.Key())
229+
do(typ.Elem())
230+
case *Pointer:
231+
do(typ.Elem())
232+
case *Slice:
233+
do(typ.Elem())
234+
235+
case *Interface:
236+
for i := 0; i < typ.NumMethods(); i++ {
237+
do(typ.Method(i).Type())
238+
}
239+
case *Signature:
240+
tuple := func(tup *Tuple) {
241+
for i := 0; i < tup.Len(); i++ {
242+
do(tup.At(i).Type())
243+
}
244+
}
245+
tuple(typ.Params())
246+
tuple(typ.Results())
247+
case *Struct:
248+
for i := 0; i < typ.NumFields(); i++ {
249+
do(typ.Field(i).Type())
250+
}
251+
}
252+
}
253+
do(targ)
254+
}
255+
256+
// localNamedVertex returns the index of the vertex representing
257+
// named, or -1 if named doesn't need representation.
258+
func (w *monoGraph) localNamedVertex(pkg *Package, named *Named) int {
259+
obj := named.Obj()
260+
if obj.Pkg() != pkg {
261+
return -1 // imported type
262+
}
263+
264+
root := pkg.Scope()
265+
if obj.Parent() == root {
266+
return -1 // package scope, no ambient type parameters
267+
}
268+
269+
if idx, ok := w.nameIdx[obj]; ok {
270+
return idx
271+
}
272+
273+
idx := -1
274+
275+
// Walk the type definition's scope to find any ambient type
276+
// parameters that it's implicitly parameterized by.
277+
for scope := obj.Parent(); scope != root; scope = scope.Parent() {
278+
for _, elem := range scope.elems {
279+
if elem, ok := elem.(*TypeName); ok && !elem.IsAlias() && elem.Pos() < obj.Pos() {
280+
if tpar, ok := elem.Type().(*TypeParam); ok {
281+
if idx < 0 {
282+
idx = len(w.vertices)
283+
w.vertices = append(w.vertices, monoVertex{obj: obj})
284+
}
285+
286+
w.addEdge(idx, w.typeParamVertex(tpar), 1, func(check *Checker) {
287+
check.errorf(obj, _InvalidInstanceCycle, "\t%s implicitly parameterized by %s", obj.Name(), elem.Name())
288+
})
289+
}
290+
}
291+
}
292+
}
293+
294+
if w.nameIdx == nil {
295+
w.nameIdx = make(map[*TypeName]int)
296+
}
297+
w.nameIdx[obj] = idx
298+
return idx
299+
}
300+
301+
// typeParamVertex returns the index of the vertex representing tpar.
302+
func (w *monoGraph) typeParamVertex(tpar *TypeParam) int {
303+
if x, ok := w.canon[tpar]; ok {
304+
tpar = x
305+
}
306+
307+
obj := tpar.Obj()
308+
309+
if idx, ok := w.nameIdx[obj]; ok {
310+
return idx
311+
}
312+
313+
if w.nameIdx == nil {
314+
w.nameIdx = make(map[*TypeName]int)
315+
}
316+
317+
idx := len(w.vertices)
318+
w.vertices = append(w.vertices, monoVertex{obj: obj})
319+
w.nameIdx[obj] = idx
320+
return idx
321+
}
322+
323+
func (w *monoGraph) addEdge(dst, src, weight int, report func(check *Checker)) {
324+
// TODO(mdempsky): Deduplicate redundant edges?
325+
w.edges = append(w.edges, monoEdge{
326+
dst: dst,
327+
src: src,
328+
weight: weight,
329+
report: report,
330+
})
331+
}

0 commit comments

Comments
 (0)