Skip to content

Commit 5d0d87a

Browse files
committed
cmd/compile: fix package initialization ordering
This CL rewrites cmd/compile's package-level initialization ordering algorithm to be compliant with the Go spec. See documentation in initorder.go for details. Incidentally, this CL also improves fidelity of initialization loop diagnostics by including referenced functions in the emitted output like go/types does. Fixes #22326. Change-Id: I7c9ac47ff563df4d4f700cf6195387a0f372cc7b Reviewed-on: https://go-review.googlesource.com/c/go/+/170062 Run-TryBot: Matthew Dempsky <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Robert Griesemer <[email protected]>
1 parent e883d00 commit 5d0d87a

File tree

6 files changed

+439
-238
lines changed

6 files changed

+439
-238
lines changed

src/cmd/compile/internal/gc/init.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func renameinit() *types.Sym {
3131
// 2) Initialize all the variables that have initializers.
3232
// 3) Run any init functions.
3333
func fninit(n []*Node) {
34-
nf := initfix(n)
34+
nf := initOrder(n)
3535

3636
var deps []*obj.LSym // initTask records for packages the current package depends on
3737
var fns []*obj.LSym // functions to call for package initialization
+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
// Copyright 2019 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 gc
6+
7+
import (
8+
"bytes"
9+
"container/heap"
10+
"fmt"
11+
)
12+
13+
// Package initialization
14+
//
15+
// Here we implement the algorithm for ordering package-level variable
16+
// initialization. The spec is written in terms of variable
17+
// initialization, but multiple variables initialized by a single
18+
// assignment are handled together, so here we instead focus on
19+
// ordering initialization assignments. Conveniently, this maps well
20+
// to how we represent package-level initializations using the Node
21+
// AST.
22+
//
23+
// Assignments are in one of three phases: NotStarted, Pending, or
24+
// Done. For assignments in the Pending phase, we use Xoffset to
25+
// record the number of unique variable dependencies whose
26+
// initialization assignment is not yet Done. We also maintain a
27+
// "blocking" map that maps assignments back to all of the assignments
28+
// that depend on it.
29+
//
30+
// For example, for an initialization like:
31+
//
32+
// var x = f(a, b, b)
33+
// var a, b = g()
34+
//
35+
// the "x = f(a, b, b)" assignment depends on two variables (a and b),
36+
// so its Xoffset will be 2. Correspondingly, the "a, b = g()"
37+
// assignment's "blocking" entry will have two entries back to x's
38+
// assignment.
39+
//
40+
// Logically, initialization works by (1) taking all NotStarted
41+
// assignments, calculating their dependencies, and marking them
42+
// Pending; (2) adding all Pending assignments with Xoffset==0 to a
43+
// "ready" priority queue (ordered by variable declaration position);
44+
// and (3) iteratively processing the next Pending assignment from the
45+
// queue, decreasing the Xoffset of assignments it's blocking, and
46+
// adding them to the queue if decremented to 0.
47+
//
48+
// As an optimization, we actually apply each of these three steps for
49+
// each assignment. This yields the same order, but keeps queue size
50+
// down and thus also heap operation costs.
51+
52+
// Static initialization phase.
53+
// These values are stored in two bits in Node.flags.
54+
const (
55+
InitNotStarted = iota
56+
InitDone
57+
InitPending
58+
)
59+
60+
type InitOrder struct {
61+
// blocking maps initialization assignments to the assignments
62+
// that depend on it.
63+
blocking map[*Node][]*Node
64+
65+
// ready is the queue of Pending initialization assignments
66+
// that are ready for initialization.
67+
ready declOrder
68+
}
69+
70+
// initOrder computes initialization order for a list l of
71+
// package-level declarations (in declaration order) and outputs the
72+
// corresponding list of statements to include in the init() function
73+
// body.
74+
func initOrder(l []*Node) []*Node {
75+
s := InitSchedule{
76+
initplans: make(map[*Node]*InitPlan),
77+
inittemps: make(map[*Node]*Node),
78+
}
79+
o := InitOrder{
80+
blocking: make(map[*Node][]*Node),
81+
}
82+
83+
// Process all package-level assignment in declaration order.
84+
for _, n := range l {
85+
switch n.Op {
86+
case OAS, OAS2DOTTYPE, OAS2FUNC, OAS2MAPR, OAS2RECV:
87+
o.processAssign(n)
88+
o.flushReady(s.staticInit)
89+
case ODCLCONST, ODCLFUNC, ODCLTYPE:
90+
// nop
91+
default:
92+
Fatalf("unexpected package-level statement: %v", n)
93+
}
94+
}
95+
96+
// Check that all assignments are now Done; if not, there must
97+
// have been a dependency cycle.
98+
for _, n := range l {
99+
switch n.Op {
100+
case OAS, OAS2DOTTYPE, OAS2FUNC, OAS2MAPR, OAS2RECV:
101+
if n.Initorder() != InitDone {
102+
// If there have already been errors
103+
// printed, those errors may have
104+
// confused us and there might not be
105+
// a loop. Let the user fix those
106+
// first.
107+
if nerrors > 0 {
108+
errorexit()
109+
}
110+
111+
findInitLoopAndExit(firstLHS(n), new([]*Node))
112+
Fatalf("initialization unfinished, but failed to identify loop")
113+
}
114+
}
115+
}
116+
117+
// Invariant consistency check. If this is non-zero, then we
118+
// should have found a cycle above.
119+
if len(o.blocking) != 0 {
120+
Fatalf("expected empty map: %v", o.blocking)
121+
}
122+
123+
return s.out
124+
}
125+
126+
func (o *InitOrder) processAssign(n *Node) {
127+
if n.Initorder() != InitNotStarted || n.Xoffset != BADWIDTH {
128+
Fatalf("unexpected state: %v, %v, %v", n, n.Initorder(), n.Xoffset)
129+
}
130+
131+
n.SetInitorder(InitPending)
132+
n.Xoffset = 0
133+
134+
// Compute number of variable dependencies and build the
135+
// inverse dependency ("blocking") graph.
136+
for dep := range collectDeps(n, true) {
137+
defn := dep.Name.Defn
138+
// Skip dependencies on functions (PFUNC) and
139+
// variables already initialized (InitDone).
140+
if dep.Class() != PEXTERN || defn.Initorder() == InitDone {
141+
continue
142+
}
143+
n.Xoffset++
144+
o.blocking[defn] = append(o.blocking[defn], n)
145+
}
146+
147+
if n.Xoffset == 0 {
148+
heap.Push(&o.ready, n)
149+
}
150+
}
151+
152+
// flushReady repeatedly applies initialize to the earliest (in
153+
// declaration order) assignment ready for initialization and updates
154+
// the inverse dependency ("blocking") graph.
155+
func (o *InitOrder) flushReady(initialize func(*Node)) {
156+
for o.ready.Len() != 0 {
157+
n := heap.Pop(&o.ready).(*Node)
158+
if n.Initorder() != InitPending || n.Xoffset != 0 {
159+
Fatalf("unexpected state: %v, %v, %v", n, n.Initorder(), n.Xoffset)
160+
}
161+
162+
initialize(n)
163+
n.SetInitorder(InitDone)
164+
n.Xoffset = BADWIDTH
165+
166+
blocked := o.blocking[n]
167+
delete(o.blocking, n)
168+
169+
for _, m := range blocked {
170+
m.Xoffset--
171+
if m.Xoffset == 0 {
172+
heap.Push(&o.ready, m)
173+
}
174+
}
175+
}
176+
}
177+
178+
// findInitLoopAndExit searches for an initialization loop involving variable
179+
// or function n. If one is found, it reports the loop as an error and exits.
180+
//
181+
// path points to a slice used for tracking the sequence of
182+
// variables/functions visited. Using a pointer to a slice allows the
183+
// slice capacity to grow and limit reallocations.
184+
func findInitLoopAndExit(n *Node, path *[]*Node) {
185+
// We implement a simple DFS loop-finding algorithm. This
186+
// could be faster, but initialization cycles are rare.
187+
188+
for i, x := range *path {
189+
if x == n {
190+
reportInitLoopAndExit((*path)[i:])
191+
return
192+
}
193+
}
194+
195+
// There might be multiple loops involving n; by sorting
196+
// references, we deterministically pick the one reported.
197+
refers := collectDeps(n.Name.Defn, false).Sorted(func(ni, nj *Node) bool {
198+
return ni.Pos.Before(nj.Pos)
199+
})
200+
201+
*path = append(*path, n)
202+
for _, ref := range refers {
203+
// Short-circuit variables that were initialized.
204+
if ref.Class() == PEXTERN && ref.Name.Defn.Initorder() == InitDone {
205+
continue
206+
}
207+
208+
findInitLoopAndExit(ref, path)
209+
}
210+
*path = (*path)[:len(*path)-1]
211+
}
212+
213+
// reportInitLoopAndExit reports and initialization loop as an error
214+
// and exits. However, if l is not actually an initialization loop, it
215+
// simply returns instead.
216+
func reportInitLoopAndExit(l []*Node) {
217+
// Rotate loop so that the earliest variable declaration is at
218+
// the start.
219+
i := -1
220+
for j, n := range l {
221+
if n.Class() == PEXTERN && (i == -1 || n.Pos.Before(l[i].Pos)) {
222+
i = j
223+
}
224+
}
225+
if i == -1 {
226+
// False positive: loop only involves recursive
227+
// functions. Return so that findInitLoop can continue
228+
// searching.
229+
return
230+
}
231+
l = append(l[i:], l[:i]...)
232+
233+
// TODO(mdempsky): Method values are printed as "T.m-fm"
234+
// rather than "T.m". Figure out how to avoid that.
235+
236+
var msg bytes.Buffer
237+
fmt.Fprintf(&msg, "initialization loop:\n")
238+
for _, n := range l {
239+
fmt.Fprintf(&msg, "\t%v: %v refers to\n", n.Line(), n)
240+
}
241+
fmt.Fprintf(&msg, "\t%v: %v", l[0].Line(), l[0])
242+
243+
yyerrorl(l[0].Pos, msg.String())
244+
errorexit()
245+
}
246+
247+
// collectDeps returns all of the package-level functions and
248+
// variables that declaration n depends on. If transitive is true,
249+
// then it also includes the transitive dependencies of any depended
250+
// upon functions (but not variables).
251+
func collectDeps(n *Node, transitive bool) NodeSet {
252+
d := initDeps{transitive: transitive}
253+
switch n.Op {
254+
case OAS:
255+
d.inspect(n.Right)
256+
case OAS2DOTTYPE, OAS2FUNC, OAS2MAPR, OAS2RECV:
257+
d.inspect(n.Rlist.First())
258+
case ODCLFUNC:
259+
d.inspectList(n.Nbody)
260+
default:
261+
Fatalf("unexpected Op: %v", n.Op)
262+
}
263+
return d.seen
264+
}
265+
266+
type initDeps struct {
267+
transitive bool
268+
seen NodeSet
269+
}
270+
271+
func (d *initDeps) inspect(n *Node) { inspect(n, d.visit) }
272+
func (d *initDeps) inspectList(l Nodes) { inspectList(l, d.visit) }
273+
274+
// visit calls foundDep on any package-level functions or variables
275+
// referenced by n, if any.
276+
func (d *initDeps) visit(n *Node) bool {
277+
switch n.Op {
278+
case ONAME:
279+
if n.isMethodExpression() {
280+
d.foundDep(asNode(n.Type.FuncType().Nname))
281+
return false
282+
}
283+
284+
switch n.Class() {
285+
case PEXTERN, PFUNC:
286+
d.foundDep(n)
287+
}
288+
289+
case OCLOSURE:
290+
d.inspectList(n.Func.Closure.Nbody)
291+
292+
case ODOTMETH, OCALLPART:
293+
d.foundDep(asNode(n.Type.FuncType().Nname))
294+
}
295+
296+
return true
297+
}
298+
299+
// foundDep records that we've found a dependency on n by adding it to
300+
// seen.
301+
func (d *initDeps) foundDep(n *Node) {
302+
// Can happen with method expressions involving interface
303+
// types; e.g., fixedbugs/issue4495.go.
304+
if n == nil {
305+
return
306+
}
307+
308+
// Names without definitions aren't interesting as far as
309+
// initialization ordering goes.
310+
if n.Name.Defn == nil {
311+
return
312+
}
313+
314+
if d.seen.Has(n) {
315+
return
316+
}
317+
d.seen.Add(n)
318+
if d.transitive && n.Class() == PFUNC {
319+
d.inspectList(n.Name.Defn.Nbody)
320+
}
321+
}
322+
323+
// declOrder implements heap.Interface, ordering assignment statements
324+
// by the position of their first LHS expression.
325+
//
326+
// N.B., the Pos of the first LHS expression is used because because
327+
// an OAS node's Pos may not be unique. For example, given the
328+
// declaration "var a, b = f(), g()", "a" must be ordered before "b",
329+
// but both OAS nodes use the "=" token's position as their Pos.
330+
type declOrder []*Node
331+
332+
func (s declOrder) Len() int { return len(s) }
333+
func (s declOrder) Less(i, j int) bool { return firstLHS(s[i]).Pos.Before(firstLHS(s[j]).Pos) }
334+
func (s declOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
335+
336+
func (s *declOrder) Push(x interface{}) { *s = append(*s, x.(*Node)) }
337+
func (s *declOrder) Pop() interface{} {
338+
n := (*s)[len(*s)-1]
339+
*s = (*s)[:len(*s)-1]
340+
return n
341+
}
342+
343+
// firstLHS returns the first expression on the left-hand side of
344+
// assignment n.
345+
func firstLHS(n *Node) *Node {
346+
switch n.Op {
347+
case OAS:
348+
return n.Left
349+
case OAS2DOTTYPE, OAS2FUNC, OAS2RECV, OAS2MAPR:
350+
return n.List.First()
351+
}
352+
353+
Fatalf("unexpected Op: %v", n.Op)
354+
return nil
355+
}

0 commit comments

Comments
 (0)