Skip to content

Commit 586b21a

Browse files
adonovangopherbot
authored andcommitted
internal/refactor/inline: elide redundant braces
When replacing a CallExpr beneath an ExprStmt, a reduction strategy may return a BlockStmt containing zero or more statements. This change eliminates the braces for the block when it is safe to do so, in other words when these three conditions are met: (a) the parent of the ExprStmt is an unrestricted statement context e.g. a block or the body of a case of a switch or select, but not, say "if f(); cond {". (b) there are no forward gotos in the caller that may jump across a declaration. (Currently we check for any control labels at all in the caller.) (c) there are no conflicts between names declared in the callee block and in the caller block. Plus tests. Also, a fix and test for a latent bug allowing reduction in a restricted "if stmt; expr" context. Fixes golang/go#63259 Change-Id: I558c75d8306dfd0679768cb4b3dbf05f14b23c39 Reviewed-on: https://go-review.googlesource.com/c/tools/+/532099 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent ca34416 commit 586b21a

File tree

7 files changed

+322
-126
lines changed

7 files changed

+322
-126
lines changed

internal/refactor/inline/inline.go

+158-18
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,40 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte,
9999
res.new = &ast.ParenExpr{X: res.new.(ast.Expr)}
100100
}
101101

102+
// Some reduction strategies return a new block holding the
103+
// callee's statements. The block's braces may be elided when
104+
// there is no conflict between names declared in the block
105+
// with those declared by the parent block, and no risk of
106+
// a caller's goto jumping forward across a declaration.
107+
//
108+
// This elision is only safe when the ExprStmt is beneath a
109+
// BlockStmt, CaseClause.Body, or CommClause.Body;
110+
// (see "statement theory").
111+
elideBraces := false
112+
if newBlock, ok := res.new.(*ast.BlockStmt); ok {
113+
parent := caller.path[nodeIndex(caller.path, res.old)+1]
114+
var body []ast.Stmt
115+
switch parent := parent.(type) {
116+
case *ast.BlockStmt:
117+
body = parent.List
118+
case *ast.CommClause:
119+
body = parent.Body
120+
case *ast.CaseClause:
121+
body = parent.Body
122+
}
123+
if body != nil {
124+
if len(callerLabels(caller.path)) > 0 {
125+
// TODO(adonovan): be more precise and reject
126+
// only forward gotos across the inlined block.
127+
logf("keeping block braces: caller uses control labels")
128+
} else if intersects(declares(newBlock.List), declares(body)) {
129+
logf("keeping block braces: avoids name conflict")
130+
} else {
131+
elideBraces = true
132+
}
133+
}
134+
}
135+
102136
// Don't call replaceNode(caller.File, res.old, res.new)
103137
// as it mutates the caller's syntax tree.
104138
// Instead, splice the file, replacing the extent of the "old"
@@ -124,9 +158,16 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte,
124158
// Precise comment handling would make this a
125159
// non-issue. Formatting wouldn't really need a
126160
// FileSet at all.
161+
mark := out.Len()
127162
if err := format.Node(&out, caller.Fset, res.new); err != nil {
128163
return nil, err
129164
}
165+
if elideBraces {
166+
// Overwrite unnecessary {...} braces with spaces.
167+
// TODO(adonovan): less hacky solution.
168+
out.Bytes()[mark] = ' '
169+
out.Bytes()[out.Len()-1] = ' '
170+
}
130171
out.Write(caller.Content[end:])
131172
const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors
132173
f, err = parser.ParseFile(caller.Fset, "callee.go", &out, mode)
@@ -630,7 +671,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
630671
logf("strategy: reduce call to empty body")
631672

632673
// Evaluate the arguments for effects and delete the call entirely.
633-
stmt := callStmt(caller.path) // cannot fail
674+
stmt := callStmt(caller.path, false) // cannot fail
634675
res.old = stmt
635676
if nargs := len(remainingArgs); nargs > 0 {
636677
// Emit "_, _ = args" to discard results.
@@ -862,9 +903,10 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
862903
// - there is no label conflict between caller and callee
863904
// - all parameters and result vars can be eliminated
864905
// or replaced by a binding decl,
906+
// - caller ExprStmt is in unrestricted statement context.
865907
//
866908
// If there is only a single statement, the braces are omitted.
867-
if stmt := callStmt(caller.path); stmt != nil &&
909+
if stmt := callStmt(caller.path, true); stmt != nil &&
868910
(!needBindingDecl || bindingDeclStmt != nil) &&
869911
!callee.HasDefer &&
870912
!hasLabelConflict(caller.path, callee.Labels) &&
@@ -876,7 +918,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
876918
if needBindingDecl {
877919
body.List = prepend(bindingDeclStmt, body.List...)
878920
}
879-
if len(body.List) == 1 {
921+
if len(body.List) == 1 { // FIXME do this opt later
880922
repl = body.List[0] // singleton: omit braces
881923
}
882924
res.old = stmt
@@ -2018,30 +2060,41 @@ func callContext(callPath []ast.Node) ast.Node {
20182060
// enclosing the call (specified as a PathEnclosingInterval)
20192061
// intersects with the set of callee labels.
20202062
func hasLabelConflict(callPath []ast.Node, calleeLabels []string) bool {
2063+
labels := callerLabels(callPath)
2064+
for _, label := range calleeLabels {
2065+
if labels[label] {
2066+
return true // conflict
2067+
}
2068+
}
2069+
return false
2070+
}
2071+
2072+
// callerLabels returns the set of control labels in the function (if
2073+
// any) enclosing the call (specified as a PathEnclosingInterval).
2074+
func callerLabels(callPath []ast.Node) map[string]bool {
20212075
var callerBody *ast.BlockStmt
20222076
switch f := callerFunc(callPath).(type) {
20232077
case *ast.FuncDecl:
20242078
callerBody = f.Body
20252079
case *ast.FuncLit:
20262080
callerBody = f.Body
20272081
}
2028-
conflict := false
2082+
var labels map[string]bool
20292083
if callerBody != nil {
20302084
ast.Inspect(callerBody, func(n ast.Node) bool {
20312085
switch n := n.(type) {
20322086
case *ast.FuncLit:
20332087
return false // prune traversal
20342088
case *ast.LabeledStmt:
2035-
for _, label := range calleeLabels {
2036-
if label == n.Label.Name {
2037-
conflict = true
2038-
}
2089+
if labels == nil {
2090+
labels = make(map[string]bool)
20392091
}
2092+
labels[n.Label.Name] = true
20402093
}
20412094
return true
20422095
})
20432096
}
2044-
return conflict
2097+
return labels
20452098
}
20462099

20472100
// callerFunc returns the innermost Func{Decl,Lit} node enclosing the
@@ -2059,11 +2112,64 @@ func callerFunc(callPath []ast.Node) ast.Node {
20592112
// callStmt reports whether the function call (specified
20602113
// as a PathEnclosingInterval) appears within an ExprStmt,
20612114
// and returns it if so.
2062-
func callStmt(callPath []ast.Node) *ast.ExprStmt {
2063-
stmt, _ := callContext(callPath).(*ast.ExprStmt)
2115+
//
2116+
// If unrestricted, callStmt returns nil if the ExprStmt f() appears
2117+
// in a restricted context (such as "if f(); cond {") where it cannot
2118+
// be replaced by an arbitrary statement. (See "statement theory".)
2119+
func callStmt(callPath []ast.Node, unrestricted bool) *ast.ExprStmt {
2120+
stmt, ok := callContext(callPath).(*ast.ExprStmt)
2121+
if ok && unrestricted {
2122+
switch callPath[nodeIndex(callPath, stmt)+1].(type) {
2123+
case *ast.LabeledStmt,
2124+
*ast.BlockStmt,
2125+
*ast.CaseClause,
2126+
*ast.CommClause:
2127+
// unrestricted
2128+
default:
2129+
// TODO(adonovan): handle restricted
2130+
// XYZStmt.Init contexts (but not ForStmt.Post)
2131+
// by creating a block around the if/for/switch:
2132+
// "if f(); cond {" -> "{ stmts; if cond {"
2133+
2134+
return nil // restricted
2135+
}
2136+
}
20642137
return stmt
20652138
}
20662139

2140+
// Statement theory
2141+
//
2142+
// These are all the places a statement may appear in the AST:
2143+
//
2144+
// LabeledStmt.Stmt Stmt -- any
2145+
// BlockStmt.List []Stmt -- any (but see switch/select)
2146+
// IfStmt.Init Stmt? -- simple
2147+
// IfStmt.Body BlockStmt
2148+
// IfStmt.Else Stmt? -- IfStmt or BlockStmt
2149+
// CaseClause.Body []Stmt -- any
2150+
// SwitchStmt.Init Stmt? -- simple
2151+
// SwitchStmt.Body BlockStmt -- CaseClauses only
2152+
// TypeSwitchStmt.Init Stmt? -- simple
2153+
// TypeSwitchStmt.Assign Stmt -- AssignStmt(TypeAssertExpr) or ExprStmt(TypeAssertExpr)
2154+
// TypeSwitchStmt.Body BlockStmt -- CaseClauses only
2155+
// CommClause.Comm Stmt? -- SendStmt or ExprStmt(UnaryExpr) or AssignStmt(UnaryExpr)
2156+
// CommClause.Body []Stmt -- any
2157+
// SelectStmt.Body BlockStmt -- CommClauses only
2158+
// ForStmt.Init Stmt? -- simple
2159+
// ForStmt.Post Stmt? -- simple
2160+
// ForStmt.Body BlockStmt
2161+
// RangeStmt.Body BlockStmt
2162+
//
2163+
// simple = AssignStmt | SendStmt | IncDecStmt | ExprStmt.
2164+
//
2165+
// A BlockStmt cannot replace an ExprStmt in
2166+
// {If,Switch,TypeSwitch}Stmt.Init or ForStmt.Post.
2167+
// That is allowed only within:
2168+
// LabeledStmt.Stmt Stmt
2169+
// BlockStmt.List []Stmt
2170+
// CaseClause.Body []Stmt
2171+
// CommClause.Body []Stmt
2172+
20672173
// replaceNode performs a destructive update of the tree rooted at
20682174
// root, replacing each occurrence of "from" with "to". If to is nil and
20692175
// the element is within a slice, the slice element is removed.
@@ -2372,13 +2478,7 @@ func consistentOffsets(caller *Caller) bool {
23722478
// ancestor of the CallExpr identified by its PathEnclosingInterval).
23732479
func needsParens(callPath []ast.Node, old, new ast.Node) bool {
23742480
// Find enclosing old node and its parent.
2375-
// TODO(adonovan): Use index[ast.Node]() in go1.20.
2376-
i := -1
2377-
for i = range callPath {
2378-
if callPath[i] == old {
2379-
break
2380-
}
2381-
}
2481+
i := nodeIndex(callPath, old)
23822482
if i == -1 {
23832483
panic("not found")
23842484
}
@@ -2439,3 +2539,43 @@ func needsParens(callPath []ast.Node, old, new ast.Node) bool {
24392539
}
24402540
return false
24412541
}
2542+
2543+
func nodeIndex(nodes []ast.Node, n ast.Node) int {
2544+
// TODO(adonovan): Use index[ast.Node]() in go1.20.
2545+
for i, node := range nodes {
2546+
if node == n {
2547+
return i
2548+
}
2549+
}
2550+
return -1
2551+
}
2552+
2553+
// declares returns the set of lexical names declared by a
2554+
// sequence of statements from the same block, excluding sub-blocks.
2555+
// (Lexical names do not include control labels.)
2556+
func declares(stmts []ast.Stmt) map[string]bool {
2557+
names := make(map[string]bool)
2558+
for _, stmt := range stmts {
2559+
switch stmt := stmt.(type) {
2560+
case *ast.DeclStmt:
2561+
for _, spec := range stmt.Decl.(*ast.GenDecl).Specs {
2562+
switch spec := spec.(type) {
2563+
case *ast.ValueSpec:
2564+
for _, id := range spec.Names {
2565+
names[id.Name] = true
2566+
}
2567+
case *ast.TypeSpec:
2568+
names[spec.Name.Name] = true
2569+
}
2570+
}
2571+
2572+
case *ast.AssignStmt:
2573+
if stmt.Tok == token.DEFINE {
2574+
for _, lhs := range stmt.Lhs {
2575+
names[lhs.(*ast.Ident).Name] = true
2576+
}
2577+
}
2578+
}
2579+
}
2580+
return names
2581+
}

0 commit comments

Comments
 (0)