Skip to content

Commit e3ba1aa

Browse files
Improving return with defers blocking
1 parent ebb16a4 commit e3ba1aa

File tree

6 files changed

+727
-75
lines changed

6 files changed

+727
-75
lines changed

compiler/internal/analysis/defer.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package analysis
2+
3+
import (
4+
"go/ast"
5+
6+
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
7+
)
8+
9+
// deferStmt represents a defer statement that is blocking or not.
10+
//
11+
// A blocking defer statement will cause a return statement to be blocking
12+
// since the defer is called and potentially blocked while leaving the method.
13+
// We try to determine which defers affect which returns so that we only
14+
// mark returns as blocking if they are affected by a blocking defer.
15+
// In general we know that a defer will affect all returns that have been
16+
// declared after the defer statement.
17+
//
18+
// Since analysis doesn't create [CFG] basic blocks for full control
19+
// flow analysis we can't easily determine several cases:
20+
//
21+
// - Terminating if-statements(i.e. does the body of the if-statement always
22+
// return from the method) are difficult to determine. Any defer that is
23+
// added whilst inside a terminating if-statement body can only affect the
24+
// returns inside that if-statement body.
25+
// Otherwise, the defer may affect returns after the if-statement block has
26+
// rejoined the flow that it branched from. Since terminating if-statements
27+
// are difficult to determine without [CFG] blocks, we treat all
28+
// if-statements as if they are not terminating.
29+
// That means there may be some false positives, since returns declared
30+
// after a terminating branch will be marked as affected by a defer
31+
// declared in that branch, when in reality they are not.
32+
//
33+
// - Same as above but for else blocks, switch cases, and any branching.
34+
//
35+
// - Loops (i.e. for-statements and for-range-statements) can cause return
36+
// statements declared earlier in the loop to be affected by defers
37+
// declared after it in the loop. We can't determine which branches in a
38+
// loop may return to the start of the loop so we assume anywhere inside
39+
// of a loop can return to the start of the loop.
40+
// To handle this, all defers defined anywhere within a loop are assumed
41+
// to affect any return also defined in that loop.
42+
// We only need to track the top-level loop since nested loops will be
43+
// superseded by the top-level loop.
44+
//
45+
// - Labels and goto's are similar to loops in [CFG] blocks but without those
46+
// blocks it's harder to determine which defers will affect which returns.
47+
// To be safe, for any function with any blocking defers, returns, and
48+
// goto's, all the returns are defaulted to blocking.
49+
//
50+
// [CFG]: https://en.wikipedia.org/wiki/Control-flow_graph
51+
type deferStmt struct {
52+
isBlocking func(info *Info) (bool, bool)
53+
}
54+
55+
// newBlockingDefer creates a new defer statement that is blocking or not.
56+
// If the defer is calling a js.Object method then the defer is non-blocking.
57+
// If the defers calling an interface method or function pointer in a var
58+
// then the defer is blocking.
59+
func newDefer(blocking bool) *deferStmt {
60+
return &deferStmt{
61+
isBlocking: func(info *Info) (bool, bool) {
62+
return blocking, false
63+
},
64+
}
65+
}
66+
67+
// newInstDefer creates a new defer statement for an instances of a method.
68+
// The instance is used to look up the blocking information later.
69+
func newInstDefer(inst typeparams.Instance) *deferStmt {
70+
return &deferStmt{
71+
isBlocking: func(info *Info) (bool, bool) {
72+
return info.FuncInfo(inst).IsBlocking(), true
73+
},
74+
}
75+
}
76+
77+
// newLitDefer creates a new defer statement for a function literal.
78+
// The literal is used to look up the blocking information later.
79+
func newLitDefer(lit *ast.FuncLit) *deferStmt {
80+
return &deferStmt{
81+
isBlocking: func(info *Info) (bool, bool) {
82+
return info.FuncLitInfo(lit).IsBlocking(), true
83+
},
84+
}
85+
}
86+
87+
// IsBlocking determines if the defer statement is blocking or not.
88+
//
89+
// The result will be cached for future calls since each return statement
90+
// will check the same defers (plus any new defers) of prior return statements.
91+
func (d *deferStmt) IsBlocking(info *Info) bool {
92+
blocking, cacheResult := d.isBlocking(info)
93+
if cacheResult {
94+
d.isBlocking = func(info *Info) (bool, bool) {
95+
return blocking, false
96+
}
97+
}
98+
return blocking
99+
}
100+
101+
func isAnyDeferBlocking(deferStmts []*deferStmt, info *Info) bool {
102+
for _, def := range deferStmts {
103+
if def.IsBlocking(info) {
104+
return true
105+
}
106+
}
107+
return false
108+
}

0 commit comments

Comments
 (0)