blob: f6edfd825f5886a19eea6faa9098a8246dc5e338 [file] [log] [blame]
package analysis
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"github.com/gopherjs/gopherjs/compiler/astutil"
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
"github.com/gopherjs/gopherjs/compiler/typesutil"
)
type continueStmt struct {
forStmt *ast.ForStmt
analyzeStack astPath
}
func newContinueStmt(forStmt *ast.ForStmt, stack astPath) continueStmt {
cs := continueStmt{
forStmt: forStmt,
analyzeStack: stack.copy(),
}
return cs
}
// astPath is a list of AST nodes where each previous node is a parent of the
// next node.
type astPath []ast.Node
func (src astPath) copy() astPath {
dst := make(astPath, len(src))
copy(dst, src)
return dst
}
func (ap astPath) String() string {
s := &strings.Builder{}
s.WriteString("[")
for i, n := range ap {
if i > 0 {
s.WriteString(", ")
}
fmt.Fprintf(s, "%T(%p)", n, n)
}
s.WriteString("]")
return s.String()
}
type Info struct {
*types.Info
Pkg *types.Package
typeCtx *types.Context
instanceSets *typeparams.PackageInstanceSets
HasPointer map[*types.Var]bool
funcInstInfos *typeparams.InstanceMap[*FuncInfo]
funcLitInfos map[*ast.FuncLit]*FuncInfo
InitFuncInfo *FuncInfo // Context for package variable initialization.
isImportedBlocking func(typeparams.Instance) bool // For functions from other packages.
allInfos []*FuncInfo
}
func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo {
funcInfo := &FuncInfo{
pkgInfo: info,
Flattened: make(map[ast.Node]bool),
Blocking: make(map[ast.Node]bool),
GotoLabel: make(map[*types.Label]bool),
localInstCallees: new(typeparams.InstanceMap[[]astPath]),
literalFuncCallees: make(map[*ast.FuncLit][]astPath),
}
// Register the function in the appropriate map.
switch n := n.(type) {
case *ast.FuncDecl:
if n.Body == nil {
// Function body comes from elsewhere (for example, from a go:linkname
// directive), conservatively assume that it may be blocking.
// TODO(nevkontakte): It is possible to improve accuracy of this detection.
// Since GopherJS supports only "import-style" go:linkname, at this stage
// the compiler already determined whether the implementation function is
// blocking, and we could check that.
funcInfo.Blocking[n] = true
}
if inst == nil {
inst = &typeparams.Instance{Object: info.Defs[n.Name]}
}
info.funcInstInfos.Set(*inst, funcInfo)
case *ast.FuncLit:
info.funcLitInfos[n] = funcInfo
}
// And add it to the list of all functions.
info.allInfos = append(info.allInfos, funcInfo)
return funcInfo
}
func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo {
obj := info.Defs[fd.Name]
instances := info.instanceSets.Pkg(info.Pkg).ForObj(obj)
if len(instances) == 0 {
// No instances found, this is a non-generic function.
return []*FuncInfo{info.newFuncInfo(fd, nil)}
}
funcInfos := make([]*FuncInfo, 0, len(instances))
for _, inst := range instances {
fi := info.newFuncInfo(fd, &inst)
if sig, ok := obj.Type().(*types.Signature); ok {
tp := typeparams.ToSlice(typeparams.SignatureTypeParams(sig))
fi.resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs)
}
funcInfos = append(funcInfos, fi)
}
return funcInfos
}
// IsBlocking returns true if the function may contain blocking calls or operations.
func (info *Info) IsBlocking(inst typeparams.Instance) bool {
if funInfo := info.FuncInfo(inst); funInfo != nil {
return funInfo.HasBlocking()
}
panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst))
}
// FuncInfo returns information about the given function declaration instance, or nil if not found.
func (info *Info) FuncInfo(inst typeparams.Instance) *FuncInfo {
return info.funcInstInfos.Get(inst)
}
// FuncLitInfo returns information about the given function literal, or nil if not found.
func (info *Info) FuncLitInfo(fun *ast.FuncLit) *FuncInfo {
return info.funcLitInfos[fun]
}
// VarsWithInitializers returns a set of package-level variables that have
// explicit initializers.
func (info *Info) VarsWithInitializers() map[*types.Var]bool {
result := map[*types.Var]bool{}
for _, init := range info.InitOrder {
for _, o := range init.Lhs {
result[o] = true
}
}
return result
}
func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typeCtx *types.Context, typesPkg *types.Package, instanceSets *typeparams.PackageInstanceSets, isBlocking func(typeparams.Instance) bool) *Info {
info := &Info{
Info: typesInfo,
Pkg: typesPkg,
typeCtx: typeCtx,
instanceSets: instanceSets,
HasPointer: make(map[*types.Var]bool),
isImportedBlocking: isBlocking,
funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]),
funcLitInfos: make(map[*ast.FuncLit]*FuncInfo),
}
info.InitFuncInfo = info.newFuncInfo(nil, nil)
// Traverse the full AST of the package and collect information about existing
// functions.
for _, file := range files {
ast.Walk(info.InitFuncInfo, file)
}
for _, funcInfo := range info.allInfos {
if !funcInfo.HasDefer {
continue
}
// Conservatively assume that if a function has a deferred call, it might be
// blocking, and therefore all return statements need to be treated as
// blocking.
// TODO(nevkontakte): This could be improved by detecting whether a deferred
// call is actually blocking. Doing so might reduce generated code size a
// bit.
for _, returnStmt := range funcInfo.returnStmts {
funcInfo.markBlocking(returnStmt)
}
}
// Propagate information about blocking calls to the caller functions.
// For each function we check all other functions it may call and if any of
// them are blocking, we mark the caller blocking as well. The process is
// repeated until no new blocking functions is detected.
for {
done := true
for _, caller := range info.allInfos {
// Check calls to named functions and function-typed variables.
caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) {
if info.FuncInfo(callee).HasBlocking() {
for _, callSite := range callSites {
caller.markBlocking(callSite)
}
caller.localInstCallees.Delete(callee)
done = false
}
})
// Check direct calls to function literals.
for callee, callSites := range caller.literalFuncCallees {
if info.FuncLitInfo(callee).HasBlocking() {
for _, callSite := range callSites {
caller.markBlocking(callSite)
}
delete(caller.literalFuncCallees, callee)
done = false
}
}
}
if done {
break
}
}
// After all function blocking information was propagated, mark flow control
// statements as blocking whenever they may lead to a blocking function call.
for _, funcInfo := range info.allInfos {
for _, continueStmt := range funcInfo.continueStmts {
if funcInfo.Blocking[continueStmt.forStmt.Post] {
// If a for-loop post-expression is blocking, the continue statement
// that leads to it must be treated as blocking.
funcInfo.markBlocking(continueStmt.analyzeStack)
}
}
}
return info
}
type FuncInfo struct {
HasDefer bool
// Nodes are "flattened" into a switch-case statement when we need to be able
// to jump into an arbitrary position in the code with a GOTO statement, or
// resume a goroutine after a blocking call unblocks.
Flattened map[ast.Node]bool
// Blocking indicates that either the AST node itself or its descendant may
// block goroutine execution (for example, a channel operation).
Blocking map[ast.Node]bool
// GotoLabel indicates a label referenced by a goto statement, rather than a
// named loop.
GotoLabel map[*types.Label]bool
// List of continue statements in the function.
continueStmts []continueStmt
// List of return statements in the function.
returnStmts []astPath
// List of other named functions from the current package this function calls.
// If any of them are blocking, this function will become blocking too.
localInstCallees *typeparams.InstanceMap[[]astPath]
// List of function literals directly called from this function (for example:
// `func() { /* do stuff */ }()`). This is distinct from function literals
// assigned to named variables (for example: `doStuff := func() {};
// doStuff()`), which are handled by localNamedCallees. If any of them are
// identified as blocking, this function will become blocking too.
literalFuncCallees map[*ast.FuncLit][]astPath
// resolver is used by this function instance to resolve any type arguments
// for internal function calls.
// This may be nil if not an instance of a generic function.
resolver *typeparams.Resolver
pkgInfo *Info // Function's parent package.
visitorStack astPath
}
// HasBlocking indicates if this function may block goroutine execution.
//
// For example, a channel operation in a function or a call to another
// possibly blocking function may block the function.
func (fi *FuncInfo) HasBlocking() bool {
return fi == nil || len(fi.Blocking) != 0
}
func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor {
if node == nil {
if len(fi.visitorStack) != 0 {
fi.visitorStack = fi.visitorStack[:len(fi.visitorStack)-1]
}
return nil
}
fi.visitorStack = append(fi.visitorStack, node)
switch n := node.(type) {
case *ast.FuncDecl:
// Analyze all the instances of the function declarations
// in their own context with their own type arguments.
fis := fi.pkgInfo.newFuncInfoInstances(n)
if n.Body != nil {
for _, fi := range fis {
ast.Walk(fi, n.Body)
}
}
return nil
case *ast.FuncLit:
// Analyze the function literal in its own context.
return fi.pkgInfo.newFuncInfo(n, nil)
case *ast.BranchStmt:
switch n.Tok {
case token.GOTO:
// Emulating GOTO in JavaScript requires the code to be flattened into a
// switch-statement.
fi.markFlattened(fi.visitorStack)
fi.GotoLabel[fi.pkgInfo.Uses[n.Label].(*types.Label)] = true
case token.CONTINUE:
loopStmt := astutil.FindLoopStmt(fi.visitorStack, n, fi.pkgInfo.Info)
if forStmt, ok := (loopStmt).(*ast.ForStmt); ok {
// In `for x; y; z { ... }` loops `z` may be potentially blocking
// and therefore continue expression that triggers it would have to
// be treated as blocking.
fi.continueStmts = append(fi.continueStmts, newContinueStmt(forStmt, fi.visitorStack))
}
}
return fi
case *ast.CallExpr:
return fi.visitCallExpr(n)
case *ast.SendStmt:
// Sending into a channel is blocking.
fi.markBlocking(fi.visitorStack)
return fi
case *ast.UnaryExpr:
switch n.Op {
case token.AND:
if id, ok := astutil.RemoveParens(n.X).(*ast.Ident); ok {
fi.pkgInfo.HasPointer[fi.pkgInfo.Uses[id].(*types.Var)] = true
}
case token.ARROW:
// Receiving from a channel is blocking.
fi.markBlocking(fi.visitorStack)
}
return fi
case *ast.RangeStmt:
if _, ok := fi.pkgInfo.TypeOf(n.X).Underlying().(*types.Chan); ok {
// for-range loop over a channel is blocking.
fi.markBlocking(fi.visitorStack)
}
return fi
case *ast.SelectStmt:
for _, s := range n.Body.List {
if s.(*ast.CommClause).Comm == nil { // default clause
return fi
}
}
// Select statements without a default case are blocking.
fi.markBlocking(fi.visitorStack)
return fi
case *ast.CommClause:
// FIXME(nevkontakte): Does this need to be manually spelled out? Presumably
// ast.Walk would visit all those nodes anyway, and we are not creating any
// new contexts here.
// https://github.com/gopherjs/gopherjs/issues/230 seems to be relevant?
switch comm := n.Comm.(type) {
case *ast.SendStmt:
ast.Walk(fi, comm.Chan)
ast.Walk(fi, comm.Value)
case *ast.ExprStmt:
ast.Walk(fi, comm.X.(*ast.UnaryExpr).X)
case *ast.AssignStmt:
ast.Walk(fi, comm.Rhs[0].(*ast.UnaryExpr).X)
}
for _, s := range n.Body {
ast.Walk(fi, s)
}
return nil // The subtree was manually checked, no need to visit it again.
case *ast.GoStmt:
// Unlike a regular call, the function in a go statement doesn't block the
// caller goroutine, but the expression that determines the function and its
// arguments still need to be checked.
ast.Walk(fi, n.Call.Fun)
for _, arg := range n.Call.Args {
ast.Walk(fi, arg)
}
return nil // The subtree was manually checked, no need to visit it again.
case *ast.DeferStmt:
fi.HasDefer = true
if funcLit, ok := n.Call.Fun.(*ast.FuncLit); ok {
ast.Walk(fi, funcLit.Body)
}
return fi
case *ast.ReturnStmt:
// Capture all return statements in the function. They could become blocking
// if the function has a blocking deferred call.
fi.returnStmts = append(fi.returnStmts, fi.visitorStack.copy())
return fi
default:
return fi
}
// Deliberately no return here to make sure that each of the cases above is
// self-sufficient and explicitly decides in which context the its AST subtree
// needs to be analyzed.
}
func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor {
switch f := astutil.RemoveParens(n.Fun).(type) {
case *ast.Ident:
fi.callToNamedFunc(fi.instanceForIdent(f))
case *ast.SelectorExpr:
if sel := fi.pkgInfo.Selections[f]; sel != nil {
if typesutil.IsJsObject(sel.Recv()) {
// js.Object methods are known to be non-blocking, but we still must
// check its arguments.
} else {
// selection is a method call like `foo.Bar()`, where `foo` might
// be generic and needs to be substituted with the type argument.
fi.callToNamedFunc(fi.instanceFoSelection(sel))
}
} else {
fi.callToNamedFunc(fi.instanceForIdent(f.Sel))
}
case *ast.FuncLit:
// Collect info about the function literal itself.
ast.Walk(fi, n.Fun)
// Check all argument expressions.
for _, arg := range n.Args {
ast.Walk(fi, arg)
}
// Register literal function call site in case it is identified as blocking.
fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy())
return nil // No need to walk under this CallExpr, we already did it manually.
case *ast.IndexExpr:
// Collect info about the instantiated type or function, or index expression.
if astutil.IsTypeExpr(f, fi.pkgInfo.Info) {
// This is a type conversion to an instance of a generic type,
// not a call. Type assertion itself is not blocking, but we will
// visit the input expression.
} else if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) {
// This is a call of an instantiation of a generic function,
// e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }`
fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)))
} else {
// The called function is gotten with an index or key from a map, array, or slice.
// e.g. `m := map[string]func(){}; m["key"]()`, `s := []func(); s[0]()`.
// Since we can't predict if the returned function will be blocking
// or not, we have to be conservative and assume that function might be blocking.
fi.markBlocking(fi.visitorStack)
}
case *ast.IndexListExpr:
// Collect info about the instantiated type or function.
if astutil.IsTypeExpr(f, fi.pkgInfo.Info) {
// This is a type conversion to an instance of a generic type,
// not a call. Type assertion itself is not blocking, but we will
// visit the input expression.
} else {
// This is a call of an instantiation of a generic function,
// e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }`
fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)))
}
default:
if astutil.IsTypeExpr(f, fi.pkgInfo.Info) {
// This is a type conversion, not a call. Type assertion itself is not
// blocking, but we will visit the input expression.
} else {
// The function is returned by a non-trivial expression. We have to be
// conservative and assume that function might be blocking.
fi.markBlocking(fi.visitorStack)
}
}
return fi
}
func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance {
tArgs := fi.pkgInfo.Info.Instances[fnId].TypeArgs
return typeparams.Instance{
Object: fi.pkgInfo.Uses[fnId],
TArgs: fi.resolver.SubstituteAll(tArgs),
}
}
func (fi *FuncInfo) instanceFoSelection(sel *types.Selection) typeparams.Instance {
if _, ok := sel.Obj().Type().(*types.Signature); ok {
// Substitute the selection to ensure that the receiver has the correct
// type arguments propagated down from the caller.
resolved := fi.resolver.SubstituteSelection(sel)
sig := resolved.Obj().Type().(*types.Signature)
// Using the substituted receiver type, find the instance of this call.
// This does require looking up the original method in the receiver type
// that may or may not have been the receiver prior to the substitution.
if recv := sig.Recv(); recv != nil {
typ := recv.Type()
if ptrType, ok := typ.(*types.Pointer); ok {
typ = ptrType.Elem()
}
if rt, ok := typ.(*types.Named); ok {
origMethod, _, _ := types.LookupFieldOrMethod(rt.Origin(), true, rt.Obj().Pkg(), resolved.Obj().Name())
if origMethod == nil {
panic(fmt.Errorf(`failed to lookup field %q in type %v`, resolved.Obj().Name(), rt.Origin()))
}
return typeparams.Instance{
Object: origMethod,
TArgs: fi.resolver.SubstituteAll(rt.TypeArgs()),
}
}
}
}
return typeparams.Instance{Object: sel.Obj()}
}
func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance) {
switch o := callee.Object.(type) {
case *types.Func:
o = o.Origin()
if recv := o.Type().(*types.Signature).Recv(); recv != nil {
if _, ok := recv.Type().Underlying().(*types.Interface); ok {
// Conservatively assume that an interface implementation may be blocking.
fi.markBlocking(fi.visitorStack)
return
}
}
if o.Pkg() != fi.pkgInfo.Pkg {
if fi.pkgInfo.isImportedBlocking(callee) {
fi.markBlocking(fi.visitorStack)
}
return
}
// We probably don't know yet whether the callee function is blocking.
// Record the calls site for the later stage.
paths := fi.localInstCallees.Get(callee)
paths = append(paths, fi.visitorStack.copy())
fi.localInstCallees.Set(callee, paths)
case *types.Var:
// Conservatively assume that a function in a variable might be blocking.
fi.markBlocking(fi.visitorStack)
}
}
func (fi *FuncInfo) markBlocking(stack astPath) {
for _, n := range stack {
fi.Blocking[n] = true
fi.Flattened[n] = true
}
}
func (fi *FuncInfo) markFlattened(stack astPath) {
for _, n := range stack {
fi.Flattened[n] = true
}
}