cmd/compile: represent escape analysis callees as a slice

Change the callee variable in escape analysis from a single fn *ir.Name
to a slice fns []*ir.Name, in prep for a follow-up CL that starts
resolving multiple callees.

I replaced the mutable argumentParam func with some plain loops; it was
getting unweildy to manage this state and the final result feels a lot
more understandable (when read without a diff viewer anyway).

No behavior change: fns still contains at most one element.

Change-Id: Ie317b852d68413354ad2aefe426f745d033370af
Reviewed-on: https://go-review.googlesource.com/c/go/+/775640
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Jake Bailey 2026-05-07 19:40:01 -07:00 committed by Keith Randall
parent f4bfb1a9c6
commit 9be7615aa2

View file

@ -31,56 +31,44 @@ func (e *escape) call(ks []hole, call ir.Node) {
call := call.(*ir.CallExpr)
typecheck.AssertFixedCall(call)
// Pick out the function callee, if statically known.
// Pick out the function callee(s), if statically known.
// fns collects all known callees; for a single static callee
// it has one element. For unknown callees fns is nil.
//
// TODO(mdempsky): Change fn from *ir.Name to *ir.Func, but some
// functions (e.g., runtime builtins, method wrappers, generated
// eq/hash functions) don't have it set. Investigate whether
// that's a concern.
var fn *ir.Name
// TODO(mdempsky): Change fns from []*ir.Name to []*ir.Func,
// but some functions (e.g., runtime builtins, method wrappers,
// generated eq/hash functions) don't have it set. Investigate
// whether that's a concern.
var fns []*ir.Name
switch call.Op() {
case ir.OCALLFUNC:
// TODO(thepudds): use an ir.ReassignOracle here.
v := ir.StaticValue(call.Fun)
fn = ir.StaticCalleeName(v)
if fn == nil {
// If the variable is declared without an initializer
// and assigned exactly once, we can use that
// assignment to identify the callee. The only
// alternative value is the zero value (nil for
// func types), which panics on call and so can't
// cause any escape.
if name, ok := v.(*ir.Name); ok {
orig := name.Canonical()
if as := ir.FuncSingleAssignment(orig); as != nil {
fn = ir.StaticCalleeName(as.Y)
if fn := ir.StaticCalleeName(v); fn != nil {
fns = []*ir.Name{fn}
} else if name, ok := v.(*ir.Name); ok {
orig := name.Canonical()
if as := ir.FuncSingleAssignment(orig); as != nil {
if callee := ir.StaticCalleeName(as.Y); callee != nil {
fns = []*ir.Name{callee}
}
}
}
}
// argumentParam handles escape analysis of assigning a call
// argument to its corresponding parameter.
argumentParam := func(param *types.Field, arg ir.Node) {
e.rewriteArgument(arg, call, fn)
argument(e.tagHole(ks, fn, param), arg)
}
if call.IsCompilerVarLive {
// Don't escape compiler-inserted KeepAlive.
argumentParam = func(param *types.Field, arg ir.Node) {
argument(e.discardHole(), arg)
}
}
fntype := call.Fun.Type()
if fn != nil {
fntype = fn.Type()
if len(fns) == 1 {
fntype = fns[0].Type()
}
if ks != nil && fn != nil && e.inMutualBatch(fn) {
for i, result := range fn.Type().Results() {
e.expr(ks[i], result.Nname.(*ir.Name))
// Wire result flows for in-batch callees.
if ks != nil {
for _, f := range fns {
if e.inMutualBatch(f) {
for i, result := range f.Type().Results() {
e.expr(ks[i], result.Nname.(*ir.Name))
}
}
}
}
@ -88,7 +76,7 @@ func (e *escape) call(ks []hole, call ir.Node) {
if call.Op() == ir.OCALLFUNC {
// Evaluate callee function expression.
calleeK := e.discardHole()
if fn == nil { // unknown callee
if len(fns) == 0 { // unknown callee
for _, k := range ks {
if k.dst != &e.blankLoc {
// The results flow somewhere, but we don't statically
@ -105,46 +93,48 @@ func (e *escape) call(ks []hole, call ir.Node) {
recvArg = call.Fun.(*ir.SelectorExpr).X
}
// internal/abi.EscapeNonString forces its argument to be on
// the heap, if it contains a non-string pointer.
// This is used in hash/maphash.Comparable, where we cannot
// hash pointers to local variables, as the address of the
// local variable might change on stack growth.
// Strings are okay as the hash depends on only the content,
// not the pointer.
// This is also used in unique.clone, to model the data flow
// edge on the value with strings excluded, because strings
// are cloned (by content).
// The actual call we match is
// internal/abi.EscapeNonString[go.shape.T](dict, go.shape.T)
if fn != nil && fn.Sym().Pkg.Path == "internal/abi" && strings.HasPrefix(fn.Sym().Name, "EscapeNonString[") {
ps := fntype.Params()
if len(ps) == 2 && ps[1].Type.IsShape() {
if !hasNonStringPointers(ps[1].Type) {
argumentParam = func(param *types.Field, arg ir.Node) {
argument(e.discardHole(), arg)
}
} else {
argumentParam = func(param *types.Field, arg ir.Node) {
argument(e.heapHole(), arg)
}
}
}
}
args := call.Args
if recvParam := fntype.Recv(); recvParam != nil {
if fntype.Recv() != nil {
if recvArg == nil {
// Function call using method expression. Receiver argument is
// at the front of the regular arguments list.
recvArg, args = args[0], args[1:]
}
argumentParam(recvParam, recvArg)
}
for i, param := range fntype.Params() {
argumentParam(param, args[i])
if call.IsCompilerVarLive {
// Don't escape compiler-inserted KeepAlive.
if recvArg != nil {
argument(e.discardHole(), recvArg)
}
for _, arg := range args {
argument(e.discardHole(), arg)
}
} else if isEscapeNonString(fns, fntype) {
// internal/abi.EscapeNonString forces its argument to
// the heap if it contains a non-string pointer. This is
// used in hash/maphash.Comparable (where we cannot hash
// pointers to locals whose address may change on stack
// growth) and unique.clone (to model the data flow edge
// with strings excluded, because strings are cloned by
// content). The actual call we match is:
// internal/abi.EscapeNonString[go.shape.T](dict, go.shape.T)
k := e.heapHole()
if !hasNonStringPointers(fntype.Params()[1].Type) {
k = e.discardHole()
}
for _, arg := range args {
argument(k, arg)
}
} else {
if recvArg != nil {
e.rewriteArgument(recvArg, call, fns)
argument(e.mergedTagHole(ks, fns, -1, len(fntype.Params())), recvArg)
}
for i := range fntype.Params() {
e.rewriteArgument(args[i], call, fns)
argument(e.mergedTagHole(ks, fns, i, len(fntype.Params())), args[i])
}
}
case ir.OINLCALL:
@ -283,12 +273,14 @@ func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
}
// rewriteArgument rewrites the argument arg of the given call expression.
// fn is the static callee function, if known.
func (e *escape) rewriteArgument(arg ir.Node, call *ir.CallExpr, fn *ir.Name) {
if fn == nil || fn.Func == nil {
return
// fns is the list of statically known callees, if any.
func (e *escape) rewriteArgument(arg ir.Node, call *ir.CallExpr, fns []*ir.Name) {
var pragma ir.PragmaFlag
for _, fn := range fns {
if fn.Func != nil {
pragma |= fn.Func.Pragma
}
}
pragma := fn.Func.Pragma
if pragma&(ir.UintptrKeepAlive|ir.UintptrEscapes) == 0 {
return
}
@ -375,6 +367,25 @@ func (e *escape) copyExpr(pos src.XPos, expr ir.Node, init *ir.Nodes) *ir.Name {
return tmp
}
func (e *escape) mergedTagHole(ks []hole, fns []*ir.Name, paramIdx int, nParams int) hole {
if len(fns) == 0 {
return e.heapHole()
}
holes := make([]hole, 0, len(fns))
for _, f := range fns {
offset := nParams - len(f.Type().Params())
j := paramIdx - offset
var p *types.Field
if j >= 0 {
p = f.Type().Params()[j]
} else {
p = f.Type().Recv()
}
holes = append(holes, e.tagHole(ks, f, p))
}
return e.teeHole(holes...)
}
// tagHole returns a hole for evaluating an argument passed to param.
// ks should contain the holes representing where the function
// callee's results flows. fn is the statically-known callee function,
@ -418,6 +429,13 @@ func (e *escape) tagHole(ks []hole, fn *ir.Name, param *types.Field) hole {
return e.teeHole(tagKs...)
}
func isEscapeNonString(fns []*ir.Name, fntype *types.Type) bool {
return len(fns) == 1 &&
fns[0].Sym().Pkg.Path == "internal/abi" &&
strings.HasPrefix(fns[0].Sym().Name, "EscapeNonString[") &&
len(fntype.Params()) == 2 && fntype.Params()[1].Type.IsShape()
}
func hasNonStringPointers(t *types.Type) bool {
if !t.HasPointers() {
return false