Skip to content

Commit 3f8f90c

Browse files
authored
Merge pull request #1115 from nevkontakte/issue955
Propagate blocking function information through function literal calls.
2 parents a87c66b + 49993f5 commit 3f8f90c

File tree

4 files changed

+189
-51
lines changed

4 files changed

+189
-51
lines changed

compiler/analysis/info.go

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ type Info struct {
6161

6262
func (info *Info) newFuncInfo(n ast.Node) *FuncInfo {
6363
funcInfo := &FuncInfo{
64-
pkgInfo: info,
65-
Flattened: make(map[ast.Node]bool),
66-
Blocking: make(map[ast.Node]bool),
67-
GotoLabel: make(map[*types.Label]bool),
68-
localCallees: make(map[*types.Func][]astPath),
64+
pkgInfo: info,
65+
Flattened: make(map[ast.Node]bool),
66+
Blocking: make(map[ast.Node]bool),
67+
GotoLabel: make(map[*types.Label]bool),
68+
localNamedCallees: make(map[*types.Func][]astPath),
69+
literalFuncCallees: make(map[*ast.FuncLit][]astPath),
6970
}
7071

7172
// Register the function in the appropriate map.
@@ -128,15 +129,30 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info
128129
}
129130

130131
// Propagate information about blocking calls to the caller functions.
132+
// For each function we check all other functions it may call and if any of
133+
// them are blocking, we mark the caller blocking as well. The process is
134+
// repeated until no new blocking functions is detected.
131135
for {
132136
done := true
133137
for _, caller := range info.allInfos {
134-
for callee, callSites := range caller.localCallees {
138+
// Check calls to named functions and function-typed variables.
139+
for callee, callSites := range caller.localNamedCallees {
135140
if info.IsBlocking(callee) {
136141
for _, callSite := range callSites {
137142
caller.markBlocking(callSite)
138143
}
139-
delete(caller.localCallees, callee)
144+
delete(caller.localNamedCallees, callee)
145+
done = false
146+
}
147+
}
148+
149+
// Check direct calls to function literals.
150+
for callee, callSites := range caller.literalFuncCallees {
151+
if len(info.FuncLitInfos[callee].Blocking) > 0 {
152+
for _, callSite := range callSites {
153+
caller.markBlocking(callSite)
154+
}
155+
delete(caller.literalFuncCallees, callee)
140156
done = false
141157
}
142158
}
@@ -177,9 +193,15 @@ type FuncInfo struct {
177193
continueStmts []continueStmt
178194
// List of return statements in the function.
179195
returnStmts []astPath
180-
// List of other functions from the current package this function calls. If
181-
// any of them are blocking, this function will become blocking too.
182-
localCallees map[*types.Func][]astPath
196+
// List of other named functions from the current package this function calls.
197+
// If any of them are blocking, this function will become blocking too.
198+
localNamedCallees map[*types.Func][]astPath
199+
// List of function literals directly called from this function (for example:
200+
// `func() { /* do stuff */ }()`). This is distinct from function literals
201+
// assigned to named variables (for example: `doStuff := func() {};
202+
// doStuff()`), which are handled by localNamedCallees. If any of them are
203+
// identified as blocking, this function will become blocking too.
204+
literalFuncCallees map[*ast.FuncLit][]astPath
183205

184206
pkgInfo *Info // Function's parent package.
185207
visitorStack astPath
@@ -296,13 +318,13 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor {
296318
func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor {
297319
switch f := astutil.RemoveParens(n.Fun).(type) {
298320
case *ast.Ident:
299-
fi.callTo(fi.pkgInfo.Uses[f])
321+
fi.callToNamedFunc(fi.pkgInfo.Uses[f])
300322
case *ast.SelectorExpr:
301323
if sel := fi.pkgInfo.Selections[f]; sel != nil && typesutil.IsJsObject(sel.Recv()) {
302324
// js.Object methods are known to be non-blocking, but we still must
303325
// check its arguments.
304326
} else {
305-
fi.callTo(fi.pkgInfo.Uses[f.Sel])
327+
fi.callToNamedFunc(fi.pkgInfo.Uses[f.Sel])
306328
}
307329
case *ast.FuncLit:
308330
// Collect info about the function literal itself.
@@ -312,13 +334,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor {
312334
for _, arg := range n.Args {
313335
ast.Walk(fi, arg)
314336
}
315-
// If the function literal is blocking, this function is blocking to.
316-
// FIXME(nevkontakte): What if the function literal is calling a blocking
317-
// function through several layers of indirection? This will only become
318-
// known at a later stage of analysis.
319-
if len(fi.pkgInfo.FuncLitInfos[f].Blocking) != 0 {
320-
fi.markBlocking(fi.visitorStack)
321-
}
337+
// Register literal function call site in case it is identified as blocking.
338+
fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy())
322339
return nil // No need to walk under this CallExpr, we already did it manually.
323340
default:
324341
if astutil.IsTypeExpr(f, fi.pkgInfo.Info) {
@@ -334,12 +351,12 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor {
334351
return fi
335352
}
336353

337-
func (fi *FuncInfo) callTo(callee types.Object) {
354+
func (fi *FuncInfo) callToNamedFunc(callee types.Object) {
338355
switch o := callee.(type) {
339356
case *types.Func:
340357
if recv := o.Type().(*types.Signature).Recv(); recv != nil {
341358
if _, ok := recv.Type().Underlying().(*types.Interface); ok {
342-
// Conservatively assume that an interfact implementation might be blocking.
359+
// Conservatively assume that an interface implementation may be blocking.
343360
fi.markBlocking(fi.visitorStack)
344361
return
345362
}
@@ -352,7 +369,7 @@ func (fi *FuncInfo) callTo(callee types.Object) {
352369
}
353370
// We probably don't know yet whether the callee function is blocking.
354371
// Record the calls site for the later stage.
355-
fi.localCallees[o] = append(fi.localCallees[o], fi.visitorStack.copy())
372+
fi.localNamedCallees[o] = append(fi.localNamedCallees[o], fi.visitorStack.copy())
356373
case *types.Var:
357374
// Conservatively assume that a function in a variable might be blocking.
358375
fi.markBlocking(fi.visitorStack)

compiler/analysis/info_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package analysis
2+
3+
import (
4+
"go/ast"
5+
"go/token"
6+
"go/types"
7+
"testing"
8+
9+
"github.com/gopherjs/gopherjs/internal/srctesting"
10+
)
11+
12+
// See: https://github.com/gopherjs/gopherjs/issues/955.
13+
func TestBlockingFunctionLiteral(t *testing.T) {
14+
src := `
15+
package test
16+
17+
func blocking() {
18+
c := make(chan bool)
19+
<-c
20+
}
21+
22+
func indirectlyBlocking() {
23+
func() { blocking() }()
24+
}
25+
26+
func directlyBlocking() {
27+
func() {
28+
c := make(chan bool)
29+
<-c
30+
}()
31+
}
32+
33+
func notBlocking() {
34+
func() { println() } ()
35+
}
36+
`
37+
fset := token.NewFileSet()
38+
file := srctesting.Parse(t, fset, src)
39+
typesInfo, typesPkg := srctesting.Check(t, fset, file)
40+
41+
pkgInfo := AnalyzePkg([]*ast.File{file}, fset, typesInfo, typesPkg, func(f *types.Func) bool {
42+
panic("isBlocking() should be never called for imported functions in this test.")
43+
})
44+
45+
assertBlocking(t, file, pkgInfo, "blocking")
46+
assertBlocking(t, file, pkgInfo, "indirectlyBlocking")
47+
assertBlocking(t, file, pkgInfo, "directlyBlocking")
48+
assertNotBlocking(t, file, pkgInfo, "notBlocking")
49+
}
50+
51+
func assertBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) {
52+
typesFunc := getTypesFunc(t, file, pkgInfo, funcName)
53+
if !pkgInfo.IsBlocking(typesFunc) {
54+
t.Errorf("Got: %q is not blocking. Want: %q is blocking.", typesFunc, typesFunc)
55+
}
56+
}
57+
58+
func assertNotBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) {
59+
typesFunc := getTypesFunc(t, file, pkgInfo, funcName)
60+
if pkgInfo.IsBlocking(typesFunc) {
61+
t.Errorf("Got: %q is blocking. Want: %q is not blocking.", typesFunc, typesFunc)
62+
}
63+
}
64+
65+
func getTypesFunc(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) *types.Func {
66+
obj := file.Scope.Lookup(funcName)
67+
if obj == nil {
68+
t.Fatalf("Declaration of %q is not found in the AST.", funcName)
69+
}
70+
decl, ok := obj.Decl.(*ast.FuncDecl)
71+
if !ok {
72+
t.Fatalf("Got: %q is %v. Want: a function declaration.", funcName, obj.Kind)
73+
}
74+
blockingType, ok := pkgInfo.Defs[decl.Name]
75+
if !ok {
76+
t.Fatalf("No type information is found for %v.", decl.Name)
77+
}
78+
return blockingType.(*types.Func)
79+
}

compiler/astutil/astutil_test.go

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package astutil
22

33
import (
4-
"go/ast"
5-
"go/parser"
64
"go/token"
75
"testing"
6+
7+
"github.com/gopherjs/gopherjs/internal/srctesting"
88
)
99

1010
func TestImportsUnsafe(t *testing.T) {
@@ -43,7 +43,7 @@ func TestImportsUnsafe(t *testing.T) {
4343
t.Run(test.desc, func(t *testing.T) {
4444
src := "package testpackage\n\n" + test.imports
4545
fset := token.NewFileSet()
46-
file := parse(t, fset, src)
46+
file := srctesting.Parse(t, fset, src)
4747
got := ImportsUnsafe(file)
4848
if got != test.want {
4949
t.Fatalf("ImportsUnsafe() returned %t, want %t", got, test.want)
@@ -74,7 +74,7 @@ func TestFuncKey(t *testing.T) {
7474
}
7575
for _, test := range tests {
7676
t.Run(test.desc, func(t *testing.T) {
77-
fdecl := parseFuncDecl(t, test.src)
77+
fdecl := srctesting.ParseFuncDecl(t, test.src)
7878
if got := FuncKey(fdecl); got != test.want {
7979
t.Errorf("Got %q, want %q", got, test.want)
8080
}
@@ -122,7 +122,7 @@ func TestPruneOriginal(t *testing.T) {
122122
}
123123
for _, test := range tests {
124124
t.Run(test.desc, func(t *testing.T) {
125-
fdecl := parseFuncDecl(t, test.src)
125+
fdecl := srctesting.ParseFuncDecl(t, test.src)
126126
if got := PruneOriginal(fdecl); got != test.want {
127127
t.Errorf("PruneOriginal() returned %t, want %t", got, test.want)
128128
}
@@ -173,34 +173,11 @@ func TestEndsWithReturn(t *testing.T) {
173173

174174
for _, test := range tests {
175175
t.Run(test.desc, func(t *testing.T) {
176-
fdecl := parseFuncDecl(t, "package testpackage\n"+test.src)
176+
fdecl := srctesting.ParseFuncDecl(t, "package testpackage\n"+test.src)
177177
got := EndsWithReturn(fdecl.Body.List)
178178
if got != test.want {
179179
t.Errorf("EndsWithReturn() returned %t, want %t", got, test.want)
180180
}
181181
})
182182
}
183183
}
184-
185-
func parse(t *testing.T, fset *token.FileSet, src string) *ast.File {
186-
t.Helper()
187-
f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments)
188-
if err != nil {
189-
t.Fatalf("Failed to parse test source: %s", err)
190-
}
191-
return f
192-
}
193-
194-
func parseFuncDecl(t *testing.T, src string) *ast.FuncDecl {
195-
t.Helper()
196-
fset := token.NewFileSet()
197-
file := parse(t, fset, src)
198-
if l := len(file.Decls); l != 1 {
199-
t.Fatalf("Got %d decls in the sources, expected exactly 1", l)
200-
}
201-
fdecl, ok := file.Decls[0].(*ast.FuncDecl)
202-
if !ok {
203-
t.Fatalf("Got %T decl, expected *ast.FuncDecl", file.Decls[0])
204-
}
205-
return fdecl
206-
}

internal/srctesting/srctesting.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Package srctesting contains common helpers for unit testing source code
2+
// analysis and transformation.
3+
package srctesting
4+
5+
import (
6+
"go/ast"
7+
"go/parser"
8+
"go/token"
9+
"go/types"
10+
"testing"
11+
)
12+
13+
// Parse source from the string and return complete AST.
14+
//
15+
// Assumes source file name `test.go`. Fails the test on parsing error.
16+
func Parse(t *testing.T, fset *token.FileSet, src string) *ast.File {
17+
t.Helper()
18+
f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments)
19+
if err != nil {
20+
t.Fatalf("Failed to parse test source: %s", err)
21+
}
22+
return f
23+
}
24+
25+
// Check type correctness of the provided AST.
26+
//
27+
// Assumes "test" package import path. Fails the test if type checking fails.
28+
// Provided AST is expected not to have any imports.
29+
func Check(t *testing.T, fset *token.FileSet, files ...*ast.File) (*types.Info, *types.Package) {
30+
t.Helper()
31+
typesInfo := &types.Info{
32+
Types: make(map[ast.Expr]types.TypeAndValue),
33+
Defs: make(map[*ast.Ident]types.Object),
34+
Uses: make(map[*ast.Ident]types.Object),
35+
Implicits: make(map[ast.Node]types.Object),
36+
Selections: make(map[*ast.SelectorExpr]*types.Selection),
37+
Scopes: make(map[ast.Node]*types.Scope),
38+
}
39+
config := &types.Config{
40+
Sizes: &types.StdSizes{WordSize: 4, MaxAlign: 8},
41+
}
42+
typesPkg, err := config.Check("test", fset, files, typesInfo)
43+
if err != nil {
44+
t.Fatalf("Filed to type check test source: %s", err)
45+
}
46+
return typesInfo, typesPkg
47+
}
48+
49+
// ParseFuncDecl parses source with a single function defined and returns the
50+
// function AST.
51+
//
52+
// Fails the test if there isn't exactly one function declared in the source.
53+
func ParseFuncDecl(t *testing.T, src string) *ast.FuncDecl {
54+
t.Helper()
55+
fset := token.NewFileSet()
56+
file := Parse(t, fset, src)
57+
if l := len(file.Decls); l != 1 {
58+
t.Fatalf("Got %d decls in the sources, expected exactly 1", l)
59+
}
60+
fdecl, ok := file.Decls[0].(*ast.FuncDecl)
61+
if !ok {
62+
t.Fatalf("Got %T decl, expected *ast.FuncDecl", file.Decls[0])
63+
}
64+
return fdecl
65+
}

0 commit comments

Comments
 (0)