From 04193e9756171278c6bfaaebb595a62ba68f7c56 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 22 Oct 2024 15:39:06 -0600 Subject: [PATCH 1/3] Move analysis package into internal --- compiler/decls.go | 2 +- compiler/expressions.go | 2 +- compiler/functions.go | 2 +- compiler/{ => internal}/analysis/bool.go | 0 compiler/{ => internal}/analysis/break.go | 0 compiler/{ => internal}/analysis/escape.go | 0 compiler/{ => internal}/analysis/info.go | 0 compiler/{ => internal}/analysis/info_test.go | 0 compiler/{ => internal}/analysis/sideeffect.go | 0 compiler/package.go | 2 +- compiler/statements.go | 2 +- compiler/utils.go | 2 +- 12 files changed, 6 insertions(+), 6 deletions(-) rename compiler/{ => internal}/analysis/bool.go (100%) rename compiler/{ => internal}/analysis/break.go (100%) rename compiler/{ => internal}/analysis/escape.go (100%) rename compiler/{ => internal}/analysis/info.go (100%) rename compiler/{ => internal}/analysis/info_test.go (100%) rename compiler/{ => internal}/analysis/sideeffect.go (100%) diff --git a/compiler/decls.go b/compiler/decls.go index b6427a697..dbd47b6d8 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -12,7 +12,7 @@ import ( "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" diff --git a/compiler/expressions.go b/compiler/expressions.go index dcf1b7844..e2234275d 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -11,8 +11,8 @@ import ( "strconv" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) diff --git a/compiler/functions.go b/compiler/functions.go index 31a9974eb..1641174b4 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -12,8 +12,8 @@ import ( "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) diff --git a/compiler/analysis/bool.go b/compiler/internal/analysis/bool.go similarity index 100% rename from compiler/analysis/bool.go rename to compiler/internal/analysis/bool.go diff --git a/compiler/analysis/break.go b/compiler/internal/analysis/break.go similarity index 100% rename from compiler/analysis/break.go rename to compiler/internal/analysis/break.go diff --git a/compiler/analysis/escape.go b/compiler/internal/analysis/escape.go similarity index 100% rename from compiler/analysis/escape.go rename to compiler/internal/analysis/escape.go diff --git a/compiler/analysis/info.go b/compiler/internal/analysis/info.go similarity index 100% rename from compiler/analysis/info.go rename to compiler/internal/analysis/info.go diff --git a/compiler/analysis/info_test.go b/compiler/internal/analysis/info_test.go similarity index 100% rename from compiler/analysis/info_test.go rename to compiler/internal/analysis/info_test.go diff --git a/compiler/analysis/sideeffect.go b/compiler/internal/analysis/sideeffect.go similarity index 100% rename from compiler/analysis/sideeffect.go rename to compiler/internal/analysis/sideeffect.go diff --git a/compiler/package.go b/compiler/package.go index 430542b65..9fcf9d0b0 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" diff --git a/compiler/statements.go b/compiler/statements.go index d4ca76471..495cbcd1a 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -9,9 +9,9 @@ import ( "go/types" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/filter" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/typesutil" ) diff --git a/compiler/utils.go b/compiler/utils.go index a69d0fe77..8404c8ef4 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -18,7 +18,7 @@ import ( "text/template" "unicode" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) From 948c73a9586be2016b25082738c70e3a8448b1eb Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 22 Oct 2024 15:53:26 -0600 Subject: [PATCH 2/3] Update function info blocking to use instances --- compiler/decls.go | 6 +- compiler/functions.go | 14 +- compiler/internal/analysis/info.go | 207 +++- compiler/internal/analysis/info_test.go | 1252 +++++++++++++++++++++- compiler/internal/typeparams/instance.go | 12 + compiler/package.go | 15 +- internal/srctesting/srctesting.go | 2 +- 7 files changed, 1415 insertions(+), 93 deletions(-) diff --git a/compiler/decls.go b/compiler/decls.go index dbd47b6d8..37fdcca6e 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -317,7 +317,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) o := fc.pkgCtx.Defs[fun.Name].(*types.Func) d := &Decl{ FullName: o.FullName(), - Blocking: fc.pkgCtx.IsBlocking(o), + Blocking: fc.pkgCtx.IsBlocking(inst), LinkingName: symbol.New(o), } d.Dce().SetName(o) @@ -349,7 +349,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) func (fc *funcContext) callInitFunc(init *types.Func) ast.Stmt { id := fc.newIdentFor(init) call := &ast.CallExpr{Fun: id} - if fc.pkgCtx.IsBlocking(init) { + if fc.pkgCtx.IsBlocking(typeparams.Instance{Object: init}) { fc.Blocking[call] = true } return &ast.ExprStmt{X: call} @@ -373,7 +373,7 @@ func (fc *funcContext) callMainFunc(main *types.Func) ast.Stmt { }, }, } - if fc.pkgCtx.IsBlocking(main) { + if fc.pkgCtx.IsBlocking(typeparams.Instance{Object: main}) { fc.Blocking[call] = true fc.Flattened[ifStmt] = true } diff --git a/compiler/functions.go b/compiler/functions.go index 1641174b4..657bf9926 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -72,7 +72,7 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typep // namedFuncContext creates a new funcContext for a named Go function // (standalone or method). func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { - info := fc.pkgCtx.FuncDeclInfos[inst.Object.(*types.Func)] + info := fc.pkgCtx.FuncInfo(inst) c := fc.nestedFunctionContext(info, inst) return c @@ -82,7 +82,7 @@ func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { // go/types doesn't generate *types.Func objects for function literals, we // generate a synthetic one for it. func (fc *funcContext) literalFuncContext(fun *ast.FuncLit) *funcContext { - info := fc.pkgCtx.FuncLitInfos[fun] + info := fc.pkgCtx.FuncLitInfo(fun) sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) inst := typeparams.Instance{Object: o} @@ -237,7 +237,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } bodyOutput := string(fc.CatchOutput(1, func() { - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] fc.handleEscapingVars(body) } @@ -283,14 +283,14 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { fc.localVars = append(fc.localVars, "$deferred") suffix = " }" + suffix - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { suffix = " }" + suffix } } localVarDefs := "" // Function-local var declaration at the top. - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { localVars := append([]string{}, fc.localVars...) // There are several special variables involved in handling blocking functions: // $r is sometimes used as a temporary variable to store blocking call result. @@ -314,7 +314,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { prefix = prefix + " var $err = null; try {" deferSuffix := " } catch(err) { $err = err;" - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { deferSuffix += " $s = -1;" } if fc.resultNames == nil && fc.sig.HasResults() { @@ -324,7 +324,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.resultNames != nil { deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", fc.translateResults(fc.resultNames)) } - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { deferSuffix += " if($curGoroutine.asleep) {" } suffix = deferSuffix + suffix diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 44ea6c165..f6edfd825 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -50,22 +51,24 @@ func (ap astPath) String() string { type Info struct { *types.Info Pkg *types.Package + typeCtx *types.Context + instanceSets *typeparams.PackageInstanceSets HasPointer map[*types.Var]bool - FuncDeclInfos map[*types.Func]*FuncInfo - FuncLitInfos map[*ast.FuncLit]*FuncInfo + funcInstInfos *typeparams.InstanceMap[*FuncInfo] + funcLitInfos map[*ast.FuncLit]*FuncInfo InitFuncInfo *FuncInfo // Context for package variable initialization. - isImportedBlocking func(*types.Func) bool // For functions from other packages. + isImportedBlocking func(typeparams.Instance) bool // For functions from other packages. allInfos []*FuncInfo } -func (info *Info) newFuncInfo(n ast.Node) *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), - localNamedCallees: make(map[*types.Func][]astPath), + localInstCallees: new(typeparams.InstanceMap[[]astPath]), literalFuncCallees: make(map[*ast.FuncLit][]astPath), } @@ -76,14 +79,19 @@ func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { // 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 inly "import-style" go:linkname, at this stage + // 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 } - info.FuncDeclInfos[info.Defs[n.Name].(*types.Func)] = funcInfo + + if inst == nil { + inst = &typeparams.Instance{Object: info.Defs[n.Name]} + } + info.funcInstInfos.Set(*inst, funcInfo) + case *ast.FuncLit: - info.FuncLitInfos[n] = funcInfo + info.funcLitInfos[n] = funcInfo } // And add it to the list of all functions. @@ -92,12 +100,42 @@ func (info *Info) newFuncInfo(n ast.Node) *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(fun *types.Func) bool { - if funInfo := info.FuncDeclInfos[fun]; funInfo != nil { - return len(funInfo.Blocking) > 0 +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 for %s`, fun.FullName())) + 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 @@ -112,16 +150,18 @@ func (info *Info) VarsWithInitializers() map[*types.Var]bool { return result } -func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool) *Info { +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, - FuncDeclInfos: make(map[*types.Func]*FuncInfo), - FuncLitInfos: make(map[*ast.FuncLit]*FuncInfo), + funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), + funcLitInfos: make(map[*ast.FuncLit]*FuncInfo), } - info.InitFuncInfo = info.newFuncInfo(nil) + info.InitFuncInfo = info.newFuncInfo(nil, nil) // Traverse the full AST of the package and collect information about existing // functions. @@ -152,19 +192,19 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info done := true for _, caller := range info.allInfos { // Check calls to named functions and function-typed variables. - for callee, callSites := range caller.localNamedCallees { - if info.IsBlocking(callee) { + caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { + if info.FuncInfo(callee).HasBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } - delete(caller.localNamedCallees, callee) + caller.localInstCallees.Delete(callee) done = false } - } + }) // Check direct calls to function literals. for callee, callSites := range caller.literalFuncCallees { - if len(info.FuncLitInfos[callee].Blocking) > 0 { + if info.FuncLitInfo(callee).HasBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } @@ -202,7 +242,7 @@ type FuncInfo struct { // 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 - // GotoLavel indicates a label referenced by a goto statement, rather than a + // 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. @@ -211,18 +251,30 @@ type FuncInfo struct { 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. - localNamedCallees map[*types.Func][]astPath + 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 { @@ -233,9 +285,19 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { fi.visitorStack = append(fi.visitorStack, node) switch n := node.(type) { - case *ast.FuncDecl, *ast.FuncLit: - // Analyze the function in its own context. - return fi.pkgInfo.newFuncInfo(n) + 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: @@ -334,13 +396,19 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { switch f := astutil.RemoveParens(n.Fun).(type) { case *ast.Ident: - fi.callToNamedFunc(fi.pkgInfo.Uses[f]) + fi.callToNamedFunc(fi.instanceForIdent(f)) case *ast.SelectorExpr: - if sel := fi.pkgInfo.Selections[f]; sel != nil && typesutil.IsJsObject(sel.Recv()) { - // js.Object methods are known to be non-blocking, but we still must - // check its arguments. + 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.pkgInfo.Uses[f.Sel]) + fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) } case *ast.FuncLit: // Collect info about the function literal itself. @@ -353,6 +421,34 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { // 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 @@ -367,8 +463,47 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { return fi } -func (fi *FuncInfo) callToNamedFunc(callee types.Object) { - switch o := callee.(type) { +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 { @@ -379,14 +514,16 @@ func (fi *FuncInfo) callToNamedFunc(callee types.Object) { } } if o.Pkg() != fi.pkgInfo.Pkg { - if fi.pkgInfo.isImportedBlocking(o) { + 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. - fi.localNamedCallees[o] = append(fi.localNamedCallees[o], fi.visitorStack.copy()) + 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) diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 588f09a6c..f4475cf0f 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -5,74 +5,1238 @@ import ( "go/types" "testing" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/internal/srctesting" ) -// See: https://github.com/gopherjs/gopherjs/issues/955. -func TestBlockingFunctionLiteral(t *testing.T) { - src := ` -package test +func TestBlocking_Simple(t *testing.T) { + bt := newBlockingTest(t, + `package test -func blocking() { - c := make(chan bool) - <-c + func notBlocking() { + println("hi") + }`) + bt.assertNotBlocking(`notBlocking`) } -func indirectlyBlocking() { - func() { blocking() }() +func TestBlocking_Recursive(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking(i int) { + if i > 0 { + println(i) + notBlocking(i - 1) + } + }`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_AlternatingRecursive(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func near(i int) { + if i > 0 { + println(i) + far(i) + } + } + + func far(i int) { + near(i - 1) + }`) + bt.assertNotBlocking(`near`) + bt.assertNotBlocking(`far`) +} + +func TestBlocking_Channels(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func readFromChannel(c chan bool) { + <-c + } + + func readFromChannelAssign(c chan bool) { + v := <-c + println(v) + } + + func readFromChannelAsArg(c chan bool) { + println(<-c) + } + + func sendToChannel(c chan bool) { + c <- true + } + + func rangeOnChannel(c chan bool) { + for v := range c { + println(v) + } + } + + func rangeOnSlice(c []bool) { + for v := range c { + println(v) + } + }`) + bt.assertBlocking(`readFromChannel`) + bt.assertBlocking(`sendToChannel`) + bt.assertBlocking(`rangeOnChannel`) + bt.assertBlocking(`readFromChannelAssign`) + bt.assertBlocking(`readFromChannelAsArg`) + bt.assertNotBlocking(`rangeOnSlice`) +} + +func TestBlocking_Selects(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func selectReadWithoutDefault(a, b chan bool) { + select { + case <-a: + println("a") + case v := <-b: + println("b", v) + } + } + + func selectReadWithDefault(a, b chan bool) { + select { + case <-a: + println("a") + case v := <-b: + println("b", v) + default: + println("nothing") + } + } + + func selectSendWithoutDefault(a, b chan bool) { + select { + case a <- true: + println("a") + case b <- false: + println("b") + } + } + + func selectSendWithDefault(a, b chan bool) { + select { + case a <- true: + println("a") + case b <- false: + println("b") + default: + println("nothing") + } + }`) + bt.assertBlocking(`selectReadWithoutDefault`) + bt.assertBlocking(`selectSendWithoutDefault`) + bt.assertNotBlocking(`selectReadWithDefault`) + bt.assertNotBlocking(`selectSendWithDefault`) +} + +func TestBlocking_GoRoutines_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking(c chan bool) { + go func(c chan bool) { // line 4 + println(<-c) + }(c) + } + + func blocking(c chan bool) { + go func(v bool) { // line 10 + println(v) + }(<-c) + }`) + bt.assertNotBlocking(`notBlocking`) + bt.assertBlockingLit(4) + + bt.assertBlocking(`blocking`) + bt.assertNotBlockingLit(10) +} + +func TestBlocking_GoRoutines_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingRoutine(c chan bool) { + println(<-c) + } + + func nonBlockingRoutine(v bool) { + println(v) + } + + func notBlocking(c chan bool) { + go blockingRoutine(c) + } + + func blocking(c chan bool) { + go nonBlockingRoutine(<-c) + }`) + bt.assertBlocking(`blockingRoutine`) + bt.assertNotBlocking(`nonBlockingRoutine`) + + bt.assertNotBlocking(`notBlocking`) + bt.assertBlocking(`blocking`) +} + +func TestBlocking_Defers_WithoutReturns_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingBody(c chan bool) { + defer func(c chan bool) { // line 4 + println(<-c) + }(c) + } + + func blockingArg(c chan bool) { + defer func(v bool) { // line 10 + println(v) + }(<-c) + } + + func notBlocking(c chan bool) { + defer func(v bool) { // line 16 + println(v) + }(true) + }`) + bt.assertBlocking(`blockingBody`) + bt.assertBlockingLit(4) + + bt.assertBlocking(`blockingArg`) + bt.assertNotBlockingLit(10) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(16) +} + +func TestBlocking_Defers_WithoutReturns_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingPrint(c chan bool) { + println(<-c) + } + + func nonBlockingPrint(v bool) { + println(v) + } + + func blockingBody(c chan bool) { + defer blockingPrint(c) + } + + func blockingArg(c chan bool) { + defer nonBlockingPrint(<-c) + } + + func notBlocking(c chan bool) { + defer nonBlockingPrint(true) + }`) + bt.assertBlocking(`blockingPrint`) + bt.assertNotBlocking(`nonBlockingPrint`) + + bt.assertBlocking(`blockingBody`) + bt.assertBlocking(`blockingArg`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Defers_WithReturns_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingBody(c chan bool) int { + defer func(c chan bool) { // line 4 + println(<-c) + }(c) + return 42 + } + + func blockingArg(c chan bool) int { + defer func(v bool) { // line 11 + println(v) + }(<-c) + return 42 + } + + func notBlocking(c chan bool) int { + defer func(v bool) { // line 18 + println(v) + }(true) + return 42 + }`) + bt.assertBlocking(`blockingBody`) + bt.assertBlockingLit(4) + + bt.assertBlocking(`blockingArg`) + bt.assertNotBlockingLit(11) + + // TODO: The following is blocking because currently any defer with a return + // is assumed to be blocking. This limitation should be fixed in the future. + bt.assertBlocking(`notBlocking`) + bt.assertNotBlockingLit(18) +} + +func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingPrint(c chan bool) { + println(<-c) + } + + func nonBlockingPrint(v bool) { + println(v) + } + + func blockingBody(c chan bool) int { + defer blockingPrint(c) + return 42 + } + + func blockingArg(c chan bool) int { + defer nonBlockingPrint(<-c) + return 42 + } + + func notBlocking(c chan bool) int { + defer nonBlockingPrint(true) + return 42 + }`) + bt.assertBlocking(`blockingPrint`) + bt.assertNotBlocking(`nonBlockingPrint`) + + bt.assertBlocking(`blockingBody`) + bt.assertBlocking(`blockingArg`) + + // TODO: The following is blocking because currently any defer with a return + // is assumed to be blocking. This limitation should be fixed in the future. + bt.assertBlocking(`notBlocking`) +} + +func TestBlocking_Returns_WithoutDefers(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking(c chan bool) bool { + return <-c + } + + func indirectlyBlocking(c chan bool) bool { + return blocking(c) + } + + func notBlocking(c chan bool) bool { + return true + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indirectlyBlocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_FunctionLiteral(t *testing.T) { + // See: https://github.com/gopherjs/gopherjs/issues/955. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan bool) + <-c + } + + func indirectlyBlocking() { + func() { blocking() }() // line 9 + } + + func directlyBlocking() { + func() { // line 13 + c := make(chan bool) + <-c + }() + } + + func notBlocking() { + func() { println() } () // line 20 + }`) + bt.assertBlocking(`blocking`) + + bt.assertBlocking(`indirectlyBlocking`) + bt.assertBlockingLit(9) + + bt.assertBlocking(`directlyBlocking`) + bt.assertBlockingLit(13) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(20) +} + +func TestBlocking_LinkedFunction(t *testing.T) { + bt := newBlockingTest(t, + `package test + + // linked to some other function + func blocking() + + func indirectlyBlocking() { + blocking() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indirectlyBlocking`) +} + +func TestBlocking_Instances_WithSingleTypeArg(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking[T any]() { + c := make(chan T) + <-c + } + + func notBlocking[T any]() { + var v T + println(v) + } + + func bInt() { + blocking[int]() + } + + func nbUint() { + notBlocking[uint]() + }`) + bt.assertFuncInstCount(4) + // blocking and notBlocking as generics do not have FuncInfo, + // only non-generic and instances have FuncInfo. + + bt.assertBlockingInst(`test.blocking[int]`) + bt.assertBlocking(`bInt`) + bt.assertNotBlockingInst(`test.notBlocking[uint]`) + bt.assertNotBlocking(`nbUint`) +} + +func TestBlocking_Instances_WithMultipleTypeArgs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking[K comparable, V any, M ~map[K]V]() { + c := make(chan M) + <-c + } + + func notBlocking[K comparable, V any, M ~map[K]V]() { + var m M + println(m) + } + + func bInt() { + blocking[string, int, map[string]int]() + } + + func nbUint() { + notBlocking[string, uint, map[string]uint]() + }`) + bt.assertFuncInstCount(4) + // blocking and notBlocking as generics do not have FuncInfo, + // only non-generic and instances have FuncInfo. + + bt.assertBlockingInst(`test.blocking[string, int, map[string]int]`) + bt.assertBlocking(`bInt`) + bt.assertNotBlockingInst(`test.notBlocking[string, uint, map[string]uint]`) + bt.assertNotBlocking(`nbUint`) +} + +func TestBlocking_Indexed_FunctionSlice(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the slice they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = []func() { blocking, notBlocking } + + func indexer(i int) { + funcs[i]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Indexed_FunctionMap(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the map they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = map[string]func() { + "b": blocking, + "nb": notBlocking, + } + + func indexer(key string) { + funcs[key]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Indexed_FunctionArray(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the array they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = [2]func() { blocking, notBlocking } + + func indexer(i int) { + funcs[i]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Casting_InterfaceInstanceWithSingleTypeParam(t *testing.T) { + // This checks that casting to an instance type with a single type parameter + // is treated as a cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo[T any] interface { + Baz() T + } + + type Bar struct { + name string + } + + func (b Bar) Baz() string { + return b.name + } + + func caster() Foo[string] { + b := Bar{name: "foo"} + return Foo[string](b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_Casting_InterfaceInstanceWithMultipleTypeParams(t *testing.T) { + // This checks that casting to an instance type with multiple type parameters + // is treated as a cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo[K comparable, V any] interface { + Baz(K) V + } + + type Bar struct { + dat map[string]int + } + + func (b Bar) Baz(key string) int { + return b.dat[key] + } + + func caster() Foo[string, int] { + b := Bar{ dat: map[string]int{ "foo": 2 }} + return Foo[string, int](b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_Casting_Interface(t *testing.T) { + // This checks that non-generic casting of type is treated as a + // cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Baz() string + } + + type Bar struct { + name string + } + + func (b Bar) Baz() string { + return b.name + } + + func caster() Foo { + b := Bar{"foo"} + return Foo(b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_ComplexCasting(t *testing.T) { + // This checks a complex casting to a type is treated as a + // cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Bar() string + } + + func doNothing(f Foo) Foo { + return interface{ Bar() string }(f) + }`) + bt.assertNotBlocking(`doNothing`) } -func directlyBlocking() { - func() { - c := make(chan bool) - <-c - }() +func TestBlocking_ComplexCall(t *testing.T) { + // This checks a complex call of a function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + type Foo func() string + + func bar(f any) string { + return f.(Foo)() + }`) + bt.assertBlocking(`bar`) } -func notBlocking() { - func() { println() } () +func TestBlocking_CallWithNamedInterfaceReceiver(t *testing.T) { + // This checks that calling a named interface function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Baz() + } + + func bar(f Foo) { + f.Baz() + }`) + bt.assertBlocking(`bar`) } -` + +func TestBlocking_CallWithUnnamedInterfaceReceiver(t *testing.T) { + // This checks that calling an unnamed interface function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + func bar(f interface { Baz() }) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_VarFunctionCall(t *testing.T) { + // This checks that calling a function in a var is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + var foo = func() { // line 3 + println("hi") + } + + func bar() { + foo() + }`) + bt.assertNotBlockingLit(3) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FieldFunctionCallOnNamed(t *testing.T) { + // This checks that calling a function in a field is defaulted to blocking. + // This should be the same as the previous test but with a field since + // all function pointers are treated as blocking. + bt := newBlockingTest(t, + `package test + + type foo struct { + Baz func() + } + + func bar(f foo) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FieldFunctionCallOnUnnamed(t *testing.T) { + // Same as previous test but with an unnamed struct. + bt := newBlockingTest(t, + `package test + + func bar(f struct { Baz func() }) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_ParamFunctionCall(t *testing.T) { + // Same as previous test but with an unnamed function parameter. + bt := newBlockingTest(t, + `package test + + func bar(baz func()) { + baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FunctionUnwrapping(t *testing.T) { + // Test that calling a function that calls a function etc. + // is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + func bar(baz func()func()func()) { + baz()()() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_MethodCall_NonPointer(t *testing.T) { + // Test that calling a method on a non-pointer receiver. + bt := newBlockingTest(t, + `package test + + type Foo struct {} + + func (f Foo) blocking() { + ch := make(chan bool) + <-ch + } + + func (f Foo) notBlocking() { + println("hi") + } + + func blocking(f Foo) { + f.blocking() + } + + func notBlocking(f Foo) { + f.notBlocking() + }`) + bt.assertBlocking(`Foo.blocking`) + bt.assertNotBlocking(`Foo.notBlocking`) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_MethodCall_Pointer(t *testing.T) { + // Test that calling a method on a pointer receiver. + bt := newBlockingTest(t, + `package test + + type Foo struct {} + + func (f *Foo) blocking() { + ch := make(chan bool) + <-ch + } + + func (f *Foo) notBlocking() { + println("hi") + } + + func blocking(f *Foo) { + f.blocking() + } + + func notBlocking(f *Foo) { + f.notBlocking() + }`) + bt.assertBlocking(`Foo.blocking`) + bt.assertNotBlocking(`Foo.notBlocking`) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_InstantiationBlocking(t *testing.T) { + // This checks that the instantiation of a generic function is + // being used when checking for blocking not the type argument interface. + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo](foo T) { + foo.Baz() + } + + func blockingViaExplicit() { + FooBaz[BazBlocker](BazBlocker{c: make(chan bool)}) + } + + func notBlockingViaExplicit() { + FooBaz[BazNotBlocker](BazNotBlocker{}) + } + + func blockingViaImplicit() { + FooBaz(BazBlocker{c: make(chan bool)}) + } + + func notBlockingViaImplicit() { + FooBaz(BazNotBlocker{}) + }`) + bt.assertFuncInstCount(8) + // `FooBaz` as a generic function does not have FuncInfo for it, + // only non-generic or instantiations of a generic functions have FuncInfo. + + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertBlocking(`blockingViaExplicit`) + bt.assertBlocking(`blockingViaImplicit`) + bt.assertBlockingInst(`test.FooBaz[pkg/test.BazBlocker]`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlocking(`notBlockingViaExplicit`) + bt.assertNotBlocking(`notBlockingViaImplicit`) + bt.assertNotBlockingInst(`test.FooBaz[pkg/test.BazNotBlocker]`) +} + +func TestBlocking_NestedInstantiations(t *testing.T) { + // Checking that the type parameters are being propagated down into calls. + bt := newBlockingTest(t, + `package test + + func Foo[T any](t T) { + println(t) + } + + func Bar[K comparable, V any, M ~map[K]V](m M) { + Foo(m) + } + + func Baz[T any, S ~[]T](s S) { + m:= map[int]T{} + for i, v := range s { + m[i] = v + } + Bar(m) + } + + func bazInt() { + Baz([]int{1, 2, 3}) + } + + func bazString() { + Baz([]string{"one", "two", "three"}) + }`) + bt.assertFuncInstCount(8) + bt.assertNotBlocking(`bazInt`) + bt.assertNotBlocking(`bazString`) + bt.assertNotBlockingInst(`test.Foo[map[int]int]`) + bt.assertNotBlockingInst(`test.Foo[map[int]string]`) + bt.assertNotBlockingInst(`test.Bar[int, int, map[int]int]`) + bt.assertNotBlockingInst(`test.Bar[int, string, map[int]string]`) + bt.assertNotBlockingInst(`test.Baz[int, []int]`) + bt.assertNotBlockingInst(`test.Baz[string, []string]`) +} + +func TestBlocking_MethodSelection(t *testing.T) { + // This tests method selection using method expression (receiver as the first + // argument) selecting on type and method call selecting on a variable. + // This tests in both generic (FooBaz[T]) and non-generic contexts. + bt := newBlockingTest(t, + `package test + + type Foo interface { Baz() } + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type FooBaz[T Foo] struct {} + func (fb FooBaz[T]) ByMethodExpression() { + var foo T + T.Baz(foo) + } + func (fb FooBaz[T]) ByInstance() { + var foo T + foo.Baz() + } + + func blocking() { + fb := FooBaz[BazBlocker]{} + + FooBaz[BazBlocker].ByMethodExpression(fb) + FooBaz[BazBlocker].ByInstance(fb) + + fb.ByMethodExpression() + fb.ByInstance() + } + + func notBlocking() { + fb := FooBaz[BazNotBlocker]{} + + FooBaz[BazNotBlocker].ByMethodExpression(fb) + FooBaz[BazNotBlocker].ByInstance(fb) + + fb.ByMethodExpression() + fb.ByInstance() + }`) + bt.assertFuncInstCount(8) + + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertBlockingInst(`test.ByMethodExpression[pkg/test.BazBlocker]`) + bt.assertBlockingInst(`test.ByInstance[pkg/test.BazBlocker]`) + bt.assertBlocking(`blocking`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlockingInst(`test.ByMethodExpression[pkg/test.BazNotBlocker]`) + bt.assertNotBlockingInst(`test.ByInstance[pkg/test.BazNotBlocker]`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_Simple(t *testing.T) { + otherSrc := `package other + + func Blocking() { + ch := make(chan bool) + <-ch + } + + func NotBlocking() { + println("hi") + }` + + testSrc := `package test + + import "pkg/other" + + func blocking() { + other.Blocking() + } + + func notBlocking() { + other.NotBlocking() + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_ForwardInstances(t *testing.T) { + otherSrc := `package other + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + }` + + testSrc := `package test + + import "pkg/other" + + type Foo interface { Baz() } + func FooBaz[T Foo](f T) { + f.Baz() + } + + func blocking() { + FooBaz(other.BazBlocker{}) + } + + func notBlocking() { + FooBaz(other.BazNotBlocker{}) + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_BackwardInstances(t *testing.T) { + t.Skip(`isImportedBlocking doesn't fully handle instances yet`) + // TODO(grantnelson-wf): This test is currently failing because the info + // for the test package is need while creating the instances for FooBaz + // while analyzing the other package. However the other package is analyzed + // first since the test package is dependent on it. One possible fix is that + // we add some mechanism similar to the localInstCallees but for remote + // instances then perform the blocking propagation steps for all packages + // including the localInstCallees propagation at the same time. After all the + // propagation of the calls then the flow control statements can be marked. + + otherSrc := `package other + + type Foo interface { Baz() } + func FooBaz[T Foo](f T) { + f.Baz() + }` + + testSrc := `package test + + import "pkg/other" + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + func blocking() { + other.FooBaz(BazBlocker{}) + } + + func notBlocking() { + other.FooBaz(BazNotBlocker{}) + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +type blockingTest struct { + f *srctesting.Fixture + file *ast.File + pkgInfo *Info +} + +func newBlockingTest(t *testing.T, src string) *blockingTest { f := srctesting.New(t) - file := f.Parse("test.go", src) - typesInfo, typesPkg := f.Check("pkg/test", file) + tc := typeparams.Collector{ + TContext: types.NewContext(), + Info: f.Info, + Instances: &typeparams.PackageInstanceSets{}, + } - pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, typesInfo, typesPkg, func(f *types.Func) bool { - panic("isBlocking() should be never called for imported functions in this test.") - }) + file := f.Parse(`test.go`, src) + testInfo, testPkg := f.Check(`pkg/test`, file) + tc.Scan(testPkg, file) - assertBlocking(t, file, pkgInfo, "blocking") - assertBlocking(t, file, pkgInfo, "indirectlyBlocking") - assertBlocking(t, file, pkgInfo, "directlyBlocking") - assertNotBlocking(t, file, pkgInfo, "notBlocking") + isImportBlocking := func(i typeparams.Instance) bool { + t.Fatalf(`isImportBlocking should not be called in this test, called with %v`, i) + return true + } + pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, testInfo, types.NewContext(), testPkg, tc.Instances, isImportBlocking) + + return &blockingTest{ + f: f, + file: file, + pkgInfo: pkgInfo, + } } -func assertBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) { - typesFunc := getTypesFunc(t, file, pkgInfo, funcName) - if !pkgInfo.IsBlocking(typesFunc) { - t.Errorf("Got: %q is not blocking. Want: %q is blocking.", typesFunc, typesFunc) +func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc string) *blockingTest { + f := srctesting.New(t) + tc := typeparams.Collector{ + TContext: types.NewContext(), + Info: f.Info, + Instances: &typeparams.PackageInstanceSets{}, + } + + pkgInfo := map[*types.Package]*Info{} + isImportBlocking := func(i typeparams.Instance) bool { + if info, ok := pkgInfo[i.Object.Pkg()]; ok { + return info.IsBlocking(i) + } + t.Fatalf(`unexpected package in isImportBlocking for %v`, i) + return true + } + + otherFile := f.Parse(`other.go`, otherSrc) + _, otherPkg := f.Check(`pkg/other`, otherFile) + tc.Scan(otherPkg, otherFile) + + testFile := f.Parse(`test.go`, testSrc) + _, testPkg := f.Check(`pkg/test`, testFile) + tc.Scan(testPkg, testFile) + + otherPkgInfo := AnalyzePkg([]*ast.File{otherFile}, f.FileSet, f.Info, types.NewContext(), otherPkg, tc.Instances, isImportBlocking) + pkgInfo[otherPkg] = otherPkgInfo + + testPkgInfo := AnalyzePkg([]*ast.File{testFile}, f.FileSet, f.Info, types.NewContext(), testPkg, tc.Instances, isImportBlocking) + pkgInfo[testPkg] = testPkgInfo + + return &blockingTest{ + f: f, + file: testFile, + pkgInfo: testPkgInfo, } } -func assertNotBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) { - typesFunc := getTypesFunc(t, file, pkgInfo, funcName) - if pkgInfo.IsBlocking(typesFunc) { - t.Errorf("Got: %q is blocking. Want: %q is not blocking.", typesFunc, typesFunc) +func (bt *blockingTest) assertFuncInstCount(expCount int) { + if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { + bt.f.T.Errorf(`Got %d function infos but expected %d.`, got, expCount) + for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { + bt.f.T.Logf(` %d. %q`, i+1, inst.TypeString()) + } } } -func getTypesFunc(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) *types.Func { - obj := file.Scope.Lookup(funcName) - if obj == nil { - t.Fatalf("Declaration of %q is not found in the AST.", funcName) +func (bt *blockingTest) assertBlocking(funcName string) { + if !bt.isTypesFuncBlocking(funcName) { + bt.f.T.Errorf(`Got %q as not blocking but expected it to be blocking.`, funcName) } - decl, ok := obj.Decl.(*ast.FuncDecl) - if !ok { - t.Fatalf("Got: %q is %v. Want: a function declaration.", funcName, obj.Kind) +} + +func (bt *blockingTest) assertNotBlocking(funcName string) { + if bt.isTypesFuncBlocking(funcName) { + bt.f.T.Errorf(`Got %q as blocking but expected it to be not blocking.`, funcName) } - blockingType, ok := pkgInfo.Defs[decl.Name] +} + +func getFuncDeclName(fd *ast.FuncDecl) string { + name := fd.Name.Name + if fd.Recv != nil && len(fd.Recv.List) == 1 && fd.Recv.List[0].Type != nil { + typ := fd.Recv.List[0].Type + if p, ok := typ.(*ast.StarExpr); ok { + typ = p.X + } + if id, ok := typ.(*ast.Ident); ok { + name = id.Name + `.` + name + } + } + return name +} + +func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { + var decl *ast.FuncDecl + ast.Inspect(bt.file, func(n ast.Node) bool { + if f, ok := n.(*ast.FuncDecl); ok && getFuncDeclName(f) == funcName { + decl = f + return false + } + return decl == nil + }) + + if decl == nil { + bt.f.T.Fatalf(`Declaration of %q is not found in the AST.`, funcName) + } + + blockingType, ok := bt.pkgInfo.Defs[decl.Name] if !ok { - t.Fatalf("No type information is found for %v.", decl.Name) + bt.f.T.Fatalf(`No function declaration found for %q.`, decl.Name) + } + + inst := typeparams.Instance{Object: blockingType.(*types.Func)} + return bt.pkgInfo.IsBlocking(inst) +} + +func (bt *blockingTest) assertBlockingLit(lineNo int) { + if !bt.isFuncLitBlocking(lineNo) { + bt.f.T.Errorf(`Got FuncLit at line %d as not blocking but expected it to be blocking.`, lineNo) + } +} + +func (bt *blockingTest) assertNotBlockingLit(lineNo int) { + if bt.isFuncLitBlocking(lineNo) { + bt.f.T.Errorf(`Got FuncLit at line %d as blocking but expected it to be not blocking.`, lineNo) + } +} + +func (bt *blockingTest) getFuncLitLineNo(fl *ast.FuncLit) int { + return bt.f.FileSet.Position(fl.Pos()).Line +} + +func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { + var fnLit *ast.FuncLit + ast.Inspect(bt.file, func(n ast.Node) bool { + if fl, ok := n.(*ast.FuncLit); ok && bt.getFuncLitLineNo(fl) == lineNo { + fnLit = fl + return false + } + return fnLit == nil + }) + + if fnLit == nil { + bt.f.T.Fatalf(`FuncLit found on line %d not found in the AST.`, lineNo) + } + return bt.pkgInfo.FuncLitInfo(fnLit).HasBlocking() +} + +func (bt *blockingTest) assertBlockingInst(instanceStr string) { + if !bt.isFuncInstBlocking(instanceStr) { + bt.f.T.Errorf(`Got function instance of %q as not blocking but expected it to be blocking.`, instanceStr) + } +} + +func (bt *blockingTest) assertNotBlockingInst(instanceStr string) { + if bt.isFuncInstBlocking(instanceStr) { + bt.f.T.Errorf(`Got function instance of %q as blocking but expected it to be not blocking.`, instanceStr) + } +} + +func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { + instances := bt.pkgInfo.funcInstInfos.Keys() + for _, inst := range instances { + if inst.TypeString() == instanceStr { + return bt.pkgInfo.FuncInfo(inst).HasBlocking() + } + } + bt.f.T.Logf(`Function instances found in package info:`) + for i, inst := range instances { + bt.f.T.Logf(` %d. %s`, i+1, inst.TypeString()) } - return blockingType.(*types.Func) + bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) + return false } diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index f847a9825..763cd6428 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -147,6 +147,18 @@ func (iset *InstanceSet) ByObj() map[types.Object][]Instance { return result } +// ForObj returns instances for a given object type belong to. Order is not specified. +// This returns the same values as `ByObj()[obj]`. +func (iset *InstanceSet) ForObj(obj types.Object) []Instance { + result := []Instance{} + for _, inst := range iset.values { + if inst.Object == obj { + result = append(result, inst) + } + } + return result +} + // PackageInstanceSets stores an InstanceSet for each package in a program, keyed // by import path. type PackageInstanceSets map[string]*InstanceSet diff --git a/compiler/package.go b/compiler/package.go index 9fcf9d0b0..4cd800607 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -118,14 +118,14 @@ type funcContext struct { funcLitCounter int } -func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext { +func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(typeparams.Instance) bool, minify bool) *funcContext { tc := typeparams.Collector{ TContext: tContext, Info: typesInfo, Instances: &typeparams.PackageInstanceSets{}, } tc.Scan(typesPkg, srcs.Files...) - pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, typesPkg, isBlocking) + pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, tContext, typesPkg, tc.Instances, isBlocking) funcCtx := &funcContext{ FuncInfo: pkgInfo.InitFuncInfo, pkgCtx: &pkgContext{ @@ -176,11 +176,20 @@ type ImportContext struct { // Note: see analysis.FuncInfo.Blocking if you need to determine if a function // in the _current_ package is blocking. Usually available via functionContext // object. -func (ic *ImportContext) isBlocking(f *types.Func) bool { +func (ic *ImportContext) isBlocking(inst typeparams.Instance) bool { + f, ok := inst.Object.(*types.Func) + if !ok { + panic(bailout(fmt.Errorf("can't determine if instance %v is blocking: instance isn't for a function object", inst))) + } + archive, err := ic.Import(f.Pkg().Path()) if err != nil { panic(err) } + + // TODO(grantnelson-wf): f.FullName() does not differentiate between + // different instantiations of the same generic function. This needs to be + // fixed when the declaration names are updated to better support instances. fullName := f.FullName() for _, d := range archive.Declarations { if string(d.FullName) == fullName { diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 961dffd0b..35d9e25cc 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -74,7 +74,7 @@ func (f *Fixture) Check(importPath string, files ...*ast.File) (*types.Info, *ty } pkg, err := config.Check(importPath, f.FileSet, files, info) if err != nil { - f.T.Fatalf("Filed to type check test source: %s", err) + f.T.Fatalf("Failed to type check test source: %s", err) } f.Packages[importPath] = pkg return info, pkg From 40b7fe49418b386d991ce377a3bbecdc09d30210 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 23 Oct 2024 10:50:02 -0600 Subject: [PATCH 3/3] Fixed requested changes --- compiler/functions.go | 10 ++-- compiler/internal/analysis/info.go | 64 +++++++++++++----------- compiler/internal/analysis/info_test.go | 4 +- compiler/internal/typeparams/instance.go | 4 +- 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/compiler/functions.go b/compiler/functions.go index 657bf9926..86d3916bf 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -237,7 +237,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } bodyOutput := string(fc.CatchOutput(1, func() { - if fc.HasBlocking() { + if fc.IsBlocking() { fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] fc.handleEscapingVars(body) } @@ -283,14 +283,14 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { fc.localVars = append(fc.localVars, "$deferred") suffix = " }" + suffix - if fc.HasBlocking() { + if fc.IsBlocking() { suffix = " }" + suffix } } localVarDefs := "" // Function-local var declaration at the top. - if fc.HasBlocking() { + if fc.IsBlocking() { localVars := append([]string{}, fc.localVars...) // There are several special variables involved in handling blocking functions: // $r is sometimes used as a temporary variable to store blocking call result. @@ -314,7 +314,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { prefix = prefix + " var $err = null; try {" deferSuffix := " } catch(err) { $err = err;" - if fc.HasBlocking() { + if fc.IsBlocking() { deferSuffix += " $s = -1;" } if fc.resultNames == nil && fc.sig.HasResults() { @@ -324,7 +324,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.resultNames != nil { deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", fc.translateResults(fc.resultNames)) } - if fc.HasBlocking() { + if fc.IsBlocking() { deferSuffix += " if($curGoroutine.asleep) {" } suffix = deferSuffix + suffix diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index f6edfd825..6ab8a2f26 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -123,7 +123,7 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { // 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() + return funInfo.IsBlocking() } panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst)) } @@ -193,7 +193,7 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info 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() { + if info.FuncInfo(callee).IsBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } @@ -204,7 +204,7 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info // Check direct calls to function literals. for callee, callSites := range caller.literalFuncCallees { - if info.FuncLitInfo(callee).HasBlocking() { + if info.FuncLitInfo(callee).IsBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } @@ -267,11 +267,11 @@ type FuncInfo struct { visitorStack astPath } -// HasBlocking indicates if this function may block goroutine execution. +// IsBlocking 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 { +func (fi *FuncInfo) IsBlocking() bool { return fi == nil || len(fi.Blocking) != 0 } @@ -397,19 +397,22 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { switch f := astutil.RemoveParens(n.Fun).(type) { case *ast.Ident: fi.callToNamedFunc(fi.instanceForIdent(f)) + return fi 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)) + return fi } - } else { - fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) + // 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.instanceForSelection(sel)) + return fi } + + fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) + return fi case *ast.FuncLit: // Collect info about the function literal itself. ast.Walk(fi, n.Fun) @@ -427,40 +430,43 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { // 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) { + return fi + } + 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) + return fi } + // 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) + return fi 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))) + return fi } + // 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))) + return fi 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 } + // 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 } - - return fi } func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance { @@ -471,7 +477,7 @@ func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance { } } -func (fi *FuncInfo) instanceFoSelection(sel *types.Selection) typeparams.Instance { +func (fi *FuncInfo) instanceForSelection(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. diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index f4475cf0f..62238c377 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -1211,7 +1211,7 @@ func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { if fnLit == nil { bt.f.T.Fatalf(`FuncLit found on line %d not found in the AST.`, lineNo) } - return bt.pkgInfo.FuncLitInfo(fnLit).HasBlocking() + return bt.pkgInfo.FuncLitInfo(fnLit).IsBlocking() } func (bt *blockingTest) assertBlockingInst(instanceStr string) { @@ -1230,7 +1230,7 @@ func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { instances := bt.pkgInfo.funcInstInfos.Keys() for _, inst := range instances { if inst.TypeString() == instanceStr { - return bt.pkgInfo.FuncInfo(inst).HasBlocking() + return bt.pkgInfo.FuncInfo(inst).IsBlocking() } } bt.f.T.Logf(`Function instances found in package info:`) diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 763cd6428..3e4c04d2c 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -147,8 +147,8 @@ func (iset *InstanceSet) ByObj() map[types.Object][]Instance { return result } -// ForObj returns instances for a given object type belong to. Order is not specified. -// This returns the same values as `ByObj()[obj]`. +// ForObj returns the instances that belong to the given object type. +// Order is not specified. This returns the same values as `ByObj()[obj]`. func (iset *InstanceSet) ForObj(obj types.Object) []Instance { result := []Instance{} for _, inst := range iset.values {