github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/compiler/analysis/info.go (about) 1 package analysis 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/token" 7 "go/types" 8 "strings" 9 10 "github.com/gopherjs/gopherjs/compiler/astutil" 11 "github.com/gopherjs/gopherjs/compiler/typesutil" 12 ) 13 14 type continueStmt struct { 15 forStmt *ast.ForStmt 16 analyzeStack astPath 17 } 18 19 func newContinueStmt(forStmt *ast.ForStmt, stack astPath) continueStmt { 20 cs := continueStmt{ 21 forStmt: forStmt, 22 analyzeStack: stack.copy(), 23 } 24 return cs 25 } 26 27 // astPath is a list of AST nodes where each previous node is a parent of the 28 // next node. 29 type astPath []ast.Node 30 31 func (src astPath) copy() astPath { 32 dst := make(astPath, len(src)) 33 copy(dst, src) 34 return dst 35 } 36 37 func (ap astPath) String() string { 38 s := &strings.Builder{} 39 s.WriteString("[") 40 for i, n := range ap { 41 if i > 0 { 42 s.WriteString(", ") 43 } 44 fmt.Fprintf(s, "%T(%p)", n, n) 45 } 46 s.WriteString("]") 47 return s.String() 48 } 49 50 type Info struct { 51 *types.Info 52 Pkg *types.Package 53 HasPointer map[*types.Var]bool 54 FuncDeclInfos map[*types.Func]*FuncInfo 55 FuncLitInfos map[*ast.FuncLit]*FuncInfo 56 InitFuncInfo *FuncInfo // Context for package variable initialization. 57 58 isImportedBlocking func(*types.Func) bool // For functions from other packages. 59 allInfos []*FuncInfo 60 } 61 62 func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { 63 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 localNamedCallees: make(map[*types.Func][]astPath), 69 literalFuncCallees: make(map[*ast.FuncLit][]astPath), 70 } 71 72 // Register the function in the appropriate map. 73 switch n := n.(type) { 74 case *ast.FuncDecl: 75 if n.Body == nil { 76 // Function body comes from elsewhere (for example, from a go:linkname 77 // directive), conservatively assume that it may be blocking. 78 // TODO(nevkontakte): It is possible to improve accuracy of this detection. 79 // Since GopherJS supports inly "import-style" go:linkname, at this stage 80 // the compiler already determined whether the implementation function is 81 // blocking, and we could check that. 82 funcInfo.Blocking[n] = true 83 } 84 info.FuncDeclInfos[info.Defs[n.Name].(*types.Func)] = funcInfo 85 case *ast.FuncLit: 86 info.FuncLitInfos[n] = funcInfo 87 } 88 89 // And add it to the list of all functions. 90 info.allInfos = append(info.allInfos, funcInfo) 91 92 return funcInfo 93 } 94 95 func (info *Info) IsBlocking(fun *types.Func) bool { 96 if funInfo := info.FuncDeclInfos[fun]; funInfo != nil { 97 return len(funInfo.Blocking) > 0 98 } 99 panic(fmt.Errorf(`info did not have function declaration for %s`, fun.FullName())) 100 } 101 102 func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool) *Info { 103 info := &Info{ 104 Info: typesInfo, 105 Pkg: typesPkg, 106 HasPointer: make(map[*types.Var]bool), 107 isImportedBlocking: isBlocking, 108 FuncDeclInfos: make(map[*types.Func]*FuncInfo), 109 FuncLitInfos: make(map[*ast.FuncLit]*FuncInfo), 110 } 111 info.InitFuncInfo = info.newFuncInfo(nil) 112 113 // Traverse the full AST of the package and collect information about existing 114 // functions. 115 for _, file := range files { 116 ast.Walk(info.InitFuncInfo, file) 117 } 118 119 for _, funcInfo := range info.allInfos { 120 if !funcInfo.HasDefer { 121 continue 122 } 123 // Conservatively assume that if a function has a deferred call, it might be 124 // blocking, and therefore all return statements need to be treated as 125 // blocking. 126 // TODO(nevkontakte): This could be improved by detecting whether a deferred 127 // call is actually blocking. Doing so might reduce generated code size a 128 // bit. 129 for _, returnStmt := range funcInfo.returnStmts { 130 funcInfo.markBlocking(returnStmt) 131 } 132 } 133 134 // Propagate information about blocking calls to the caller functions. 135 // For each function we check all other functions it may call and if any of 136 // them are blocking, we mark the caller blocking as well. The process is 137 // repeated until no new blocking functions is detected. 138 for { 139 done := true 140 for _, caller := range info.allInfos { 141 // Check calls to named functions and function-typed variables. 142 for callee, callSites := range caller.localNamedCallees { 143 if info.IsBlocking(callee) { 144 for _, callSite := range callSites { 145 caller.markBlocking(callSite) 146 } 147 delete(caller.localNamedCallees, callee) 148 done = false 149 } 150 } 151 152 // Check direct calls to function literals. 153 for callee, callSites := range caller.literalFuncCallees { 154 if len(info.FuncLitInfos[callee].Blocking) > 0 { 155 for _, callSite := range callSites { 156 caller.markBlocking(callSite) 157 } 158 delete(caller.literalFuncCallees, callee) 159 done = false 160 } 161 } 162 } 163 if done { 164 break 165 } 166 } 167 168 // After all function blocking information was propagated, mark flow control 169 // statements as blocking whenever they may lead to a blocking function call. 170 for _, funcInfo := range info.allInfos { 171 for _, continueStmt := range funcInfo.continueStmts { 172 if funcInfo.Blocking[continueStmt.forStmt.Post] { 173 // If a for-loop post-expression is blocking, the continue statement 174 // that leads to it must be treated as blocking. 175 funcInfo.markBlocking(continueStmt.analyzeStack) 176 } 177 } 178 } 179 180 return info 181 } 182 183 type FuncInfo struct { 184 HasDefer bool 185 // Nodes are "flattened" into a switch-case statement when we need to be able 186 // to jump into an arbitrary position in the code with a GOTO statement, or 187 // resume a goroutine after a blocking call unblocks. 188 Flattened map[ast.Node]bool 189 // Blocking indicates that either the AST node itself or its descendant may 190 // block goroutine execution (for example, a channel operation). 191 Blocking map[ast.Node]bool 192 // GotoLavel indicates a label referenced by a goto statement, rather than a 193 // named loop. 194 GotoLabel map[*types.Label]bool 195 // List of continue statements in the function. 196 continueStmts []continueStmt 197 // List of return statements in the function. 198 returnStmts []astPath 199 // List of other named functions from the current package this function calls. 200 // If any of them are blocking, this function will become blocking too. 201 localNamedCallees map[*types.Func][]astPath 202 // List of function literals directly called from this function (for example: 203 // `func() { /* do stuff */ }()`). This is distinct from function literals 204 // assigned to named variables (for example: `doStuff := func() {}; 205 // doStuff()`), which are handled by localNamedCallees. If any of them are 206 // identified as blocking, this function will become blocking too. 207 literalFuncCallees map[*ast.FuncLit][]astPath 208 209 pkgInfo *Info // Function's parent package. 210 visitorStack astPath 211 } 212 213 func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { 214 if node == nil { 215 if len(fi.visitorStack) != 0 { 216 fi.visitorStack = fi.visitorStack[:len(fi.visitorStack)-1] 217 } 218 return nil 219 } 220 fi.visitorStack = append(fi.visitorStack, node) 221 222 switch n := node.(type) { 223 case *ast.FuncDecl, *ast.FuncLit: 224 // Analyze the function in its own context. 225 return fi.pkgInfo.newFuncInfo(n) 226 case *ast.BranchStmt: 227 switch n.Tok { 228 case token.GOTO: 229 // Emulating GOTO in JavaScript requires the code to be flattened into a 230 // switch-statement. 231 fi.markFlattened(fi.visitorStack) 232 fi.GotoLabel[fi.pkgInfo.Uses[n.Label].(*types.Label)] = true 233 case token.CONTINUE: 234 loopStmt := astutil.FindLoopStmt(fi.visitorStack, n, fi.pkgInfo.Info) 235 if forStmt, ok := (loopStmt).(*ast.ForStmt); ok { 236 // In `for x; y; z { ... }` loops `z` may be potentially blocking 237 // and therefore continue expression that triggers it would have to 238 // be treated as blocking. 239 fi.continueStmts = append(fi.continueStmts, newContinueStmt(forStmt, fi.visitorStack)) 240 } 241 } 242 return fi 243 case *ast.CallExpr: 244 return fi.visitCallExpr(n) 245 case *ast.SendStmt: 246 // Sending into a channel is blocking. 247 fi.markBlocking(fi.visitorStack) 248 return fi 249 case *ast.UnaryExpr: 250 switch n.Op { 251 case token.AND: 252 if id, ok := astutil.RemoveParens(n.X).(*ast.Ident); ok { 253 fi.pkgInfo.HasPointer[fi.pkgInfo.Uses[id].(*types.Var)] = true 254 } 255 case token.ARROW: 256 // Receiving from a channel is blocking. 257 fi.markBlocking(fi.visitorStack) 258 } 259 return fi 260 case *ast.RangeStmt: 261 if _, ok := fi.pkgInfo.TypeOf(n.X).Underlying().(*types.Chan); ok { 262 // for-range loop over a channel is blocking. 263 fi.markBlocking(fi.visitorStack) 264 } 265 return fi 266 case *ast.SelectStmt: 267 for _, s := range n.Body.List { 268 if s.(*ast.CommClause).Comm == nil { // default clause 269 return fi 270 } 271 } 272 // Select statements without a default case are blocking. 273 fi.markBlocking(fi.visitorStack) 274 return fi 275 case *ast.CommClause: 276 // FIXME(nevkontakte): Does this need to be manually spelled out? Presumably 277 // ast.Walk would visit all those nodes anyway, and we are not creating any 278 // new contexts here. 279 // https://github.com/gopherjs/gopherjs/issues/230 seems to be relevant? 280 switch comm := n.Comm.(type) { 281 case *ast.SendStmt: 282 ast.Walk(fi, comm.Chan) 283 ast.Walk(fi, comm.Value) 284 case *ast.ExprStmt: 285 ast.Walk(fi, comm.X.(*ast.UnaryExpr).X) 286 case *ast.AssignStmt: 287 ast.Walk(fi, comm.Rhs[0].(*ast.UnaryExpr).X) 288 } 289 for _, s := range n.Body { 290 ast.Walk(fi, s) 291 } 292 return nil // The subtree was manually checked, no need to visit it again. 293 case *ast.GoStmt: 294 // Unlike a regular call, the function in a go statement doesn't block the 295 // caller goroutine, but the expression that determines the function and its 296 // arguments still need to be checked. 297 ast.Walk(fi, n.Call.Fun) 298 for _, arg := range n.Call.Args { 299 ast.Walk(fi, arg) 300 } 301 return nil // The subtree was manually checked, no need to visit it again. 302 case *ast.DeferStmt: 303 fi.HasDefer = true 304 if funcLit, ok := n.Call.Fun.(*ast.FuncLit); ok { 305 ast.Walk(fi, funcLit.Body) 306 } 307 return fi 308 case *ast.ReturnStmt: 309 // Capture all return statements in the function. They could become blocking 310 // if the function has a blocking deferred call. 311 fi.returnStmts = append(fi.returnStmts, fi.visitorStack.copy()) 312 return fi 313 default: 314 return fi 315 } 316 // Deliberately no return here to make sure that each of the cases above is 317 // self-sufficient and explicitly decides in which context the its AST subtree 318 // needs to be analyzed. 319 } 320 321 func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { 322 switch f := astutil.RemoveParens(n.Fun).(type) { 323 case *ast.Ident: 324 fi.callToNamedFunc(fi.pkgInfo.Uses[f]) 325 case *ast.SelectorExpr: 326 if sel := fi.pkgInfo.Selections[f]; sel != nil && typesutil.IsJsObject(sel.Recv()) { 327 // js.Object methods are known to be non-blocking, but we still must 328 // check its arguments. 329 } else { 330 fi.callToNamedFunc(fi.pkgInfo.Uses[f.Sel]) 331 } 332 case *ast.FuncLit: 333 // Collect info about the function literal itself. 334 ast.Walk(fi, n.Fun) 335 336 // Check all argument expressions. 337 for _, arg := range n.Args { 338 ast.Walk(fi, arg) 339 } 340 // Register literal function call site in case it is identified as blocking. 341 fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy()) 342 return nil // No need to walk under this CallExpr, we already did it manually. 343 default: 344 if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { 345 // This is a type conversion, not a call. Type assertion itself is not 346 // blocking, but we will visit the input expression. 347 } else { 348 // The function is returned by a non-trivial expression. We have to be 349 // conservative and assume that function might be blocking. 350 fi.markBlocking(fi.visitorStack) 351 } 352 } 353 354 return fi 355 } 356 357 func (fi *FuncInfo) callToNamedFunc(callee types.Object) { 358 switch o := callee.(type) { 359 case *types.Func: 360 o = o.Origin() 361 if recv := o.Type().(*types.Signature).Recv(); recv != nil { 362 if _, ok := recv.Type().Underlying().(*types.Interface); ok { 363 // Conservatively assume that an interface implementation may be blocking. 364 fi.markBlocking(fi.visitorStack) 365 return 366 } 367 } 368 if o.Pkg() != fi.pkgInfo.Pkg { 369 if fi.pkgInfo.isImportedBlocking(o) { 370 fi.markBlocking(fi.visitorStack) 371 } 372 return 373 } 374 // We probably don't know yet whether the callee function is blocking. 375 // Record the calls site for the later stage. 376 fi.localNamedCallees[o] = append(fi.localNamedCallees[o], fi.visitorStack.copy()) 377 case *types.Var: 378 // Conservatively assume that a function in a variable might be blocking. 379 fi.markBlocking(fi.visitorStack) 380 } 381 } 382 383 func (fi *FuncInfo) markBlocking(stack astPath) { 384 for _, n := range stack { 385 fi.Blocking[n] = true 386 fi.Flattened[n] = true 387 } 388 } 389 390 func (fi *FuncInfo) markFlattened(stack astPath) { 391 for _, n := range stack { 392 fi.Flattened[n] = true 393 } 394 }