Skip to content

Commit 1c8e684

Browse files
adonovangopherbot
authored andcommitted
internal/refactor/inline: sound treatment of named results
Previously the condition for eliminating named result vars was that they do not escape, but that's not strong enough: it should be that they are never referenced. If they are referenced, they can be accommodated in a binding decl, as done in this change. Also, tests for the interaction of named results with each strategy. Change-Id: I2a4bdeec17c98efd488f9939aebe6ab1d72a0814 Reviewed-on: https://go-review.googlesource.com/c/tools/+/530438 Reviewed-by: Robert Findley <rfindley@google.com> Auto-Submit: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent d32f97a commit 1c8e684

File tree

3 files changed

+175
-53
lines changed

3 files changed

+175
-53
lines changed

internal/refactor/inline/callee.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type gobCallee struct {
4242
Params []*paramInfo // information about parameters (incl. receiver)
4343
Results []*paramInfo // information about result variables
4444
HasDefer bool // uses defer
45+
HasBareReturn bool // uses bare return in non-void function
4546
TotalReturns int // number of return statements
4647
TrivialReturns int // number of return statements with trivial result conversions
4748
Labels []string // names of all control labels
@@ -246,6 +247,7 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
246247
// (but not any nested functions).
247248
var (
248249
hasDefer = false
250+
hasBareReturn = false
249251
totalReturns = 0
250252
trivialReturns = 0
251253
labels []string
@@ -281,6 +283,8 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
281283
break
282284
}
283285
}
286+
} else if sig.Results().Len() > 0 {
287+
hasBareReturn = true
284288
}
285289
if trivial {
286290
trivialReturns++
@@ -329,6 +333,7 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
329333
Params: params,
330334
Results: results,
331335
HasDefer: hasDefer,
336+
HasBareReturn: hasBareReturn,
332337
TotalReturns: totalReturns,
333338
TrivialReturns: trivialReturns,
334339
Labels: labels,

internal/refactor/inline/inline.go

Lines changed: 94 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
508508
updateCalleeParams(calleeDecl, params)
509509

510510
// Create a var (param = arg; ...) decl for use by some strategies.
511-
bindingDeclStmt := createBindingDecl(logf, caller, args, calleeDecl)
511+
bindingDeclStmt := createBindingDecl(logf, caller, args, calleeDecl, callee.Results)
512512

513513
var remainingArgs []ast.Expr
514514
for _, arg := range args {
@@ -577,14 +577,12 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
577577
return res, nil
578578
}
579579

580-
// Attempt to reduce parameterless calls
581-
// whose result variables do not escape.
582-
allParamsSubstituted := forall(params, func(i int, p *parameter) bool {
583-
return p == nil
584-
})
585-
noResultEscapes := !exists(callee.Results, func(i int, r *paramInfo) bool {
586-
return r.Escapes
587-
})
580+
// If all parameters have been substituted and no result
581+
// variable is referenced, we don't need a binding decl.
582+
// This may enable better reduction strategies.
583+
allResultsUnreferenced := forall(callee.Results, func(i int, r *paramInfo) bool { return len(r.Refs) == 0 })
584+
needBindingDecl := !allResultsUnreferenced ||
585+
exists(params, func(i int, p *parameter) bool { return p != nil })
588586

589587
// Special case: call to { return exprs }.
590588
//
@@ -595,9 +593,8 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
595593
//
596594
// If:
597595
// - the body is just "return expr" with trivial implicit conversions,
598-
// - all parameters can be eliminated
599-
// (by substitution, or a binding decl),
600-
// - no result var escapes,
596+
// - all parameters and result vars can be eliminated
597+
// or replaced by a binding decl,
601598
// then the call expression can be replaced by the
602599
// callee's body expression, suitably substituted.
603600
if len(calleeDecl.Body.List) == 1 &&
@@ -610,14 +607,14 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
610607

611608
// statement context
612609
if stmt, ok := context.(*ast.ExprStmt); ok &&
613-
(allParamsSubstituted && noResultEscapes || bindingDeclStmt != nil) {
610+
(!needBindingDecl || bindingDeclStmt != nil) {
614611
logf("strategy: reduce stmt-context call to { return exprs }")
615612
clearPositions(calleeDecl.Body)
616613

617614
if callee.ValidForCallStmt {
618615
logf("callee body is valid as statement")
619616
// Inv: len(results) == 1
620-
if allParamsSubstituted && noResultEscapes {
617+
if !needBindingDecl {
621618
// Reduces to: expr
622619
res.old = caller.Call
623620
res.new = results[0]
@@ -643,7 +640,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
643640
Rhs: results,
644641
}
645642
res.old = stmt
646-
if allParamsSubstituted && noResultEscapes {
643+
if !needBindingDecl {
647644
// Reduces to: _, _ = exprs
648645
res.new = discard
649646
} else {
@@ -660,7 +657,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
660657
}
661658

662659
// expression context
663-
if allParamsSubstituted && noResultEscapes {
660+
if !needBindingDecl {
664661
clearPositions(calleeDecl.Body)
665662

666663
if callee.NumResults == 1 {
@@ -725,12 +722,12 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
725722
// { var (bindings); body }
726723
// { body }
727724
// so long as:
728-
// - all parameters can be eliminated
729-
// (by substitution, or a binding decl),
725+
// - all parameters can be eliminated or replaced by a binding decl,
730726
// - call is a tail-call;
731727
// - all returns in body have trivial result conversions;
732728
// - there is no label conflict;
733-
// - no result variable is referenced by name.
729+
// - no result variable is referenced by name,
730+
// or implicitly by a bare return.
734731
//
735732
// The body may use defer, arbitrary control flow, and
736733
// multiple returns.
@@ -744,16 +741,14 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
744741
if ret, ok := callContext(caller.path).(*ast.ReturnStmt); ok &&
745742
len(ret.Results) == 1 &&
746743
callee.TrivialReturns == callee.TotalReturns &&
747-
(allParamsSubstituted && noResultEscapes || bindingDeclStmt != nil) &&
744+
!callee.HasBareReturn &&
745+
(!needBindingDecl || bindingDeclStmt != nil) &&
748746
!hasLabelConflict(caller.path, callee.Labels) &&
749-
forall(callee.Results, func(i int, p *paramInfo) bool {
750-
// all result vars are unreferenced
751-
return len(p.Refs) == 0
752-
}) {
747+
allResultsUnreferenced {
753748
logf("strategy: reduce tail-call")
754749
body := calleeDecl.Body
755750
clearPositions(body)
756-
if !(allParamsSubstituted && noResultEscapes) {
751+
if needBindingDecl {
757752
body.List = prepend(bindingDeclStmt, body.List...)
758753
}
759754
res.old = ret
@@ -774,20 +769,20 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
774769
// - callee is a void function (no returns)
775770
// - callee does not use defer
776771
// - there is no label conflict between caller and callee
777-
// - all parameters can be eliminated
778-
// (by substitution, or a binding decl),
772+
// - all parameters and result vars can be eliminated
773+
// or replaced by a binding decl,
779774
//
780775
// If there is only a single statement, the braces are omitted.
781776
if stmt := callStmt(caller.path); stmt != nil &&
782-
(allParamsSubstituted && noResultEscapes || bindingDeclStmt != nil) &&
777+
(!needBindingDecl || bindingDeclStmt != nil) &&
783778
!callee.HasDefer &&
784779
!hasLabelConflict(caller.path, callee.Labels) &&
785780
callee.TotalReturns == 0 {
786781
logf("strategy: reduce stmt-context call to { stmts }")
787782
body := calleeDecl.Body
788783
var repl ast.Stmt = body
789784
clearPositions(repl)
790-
if !(allParamsSubstituted && noResultEscapes) {
785+
if needBindingDecl {
791786
body.List = prepend(bindingDeclStmt, body.List...)
792787
}
793788
if len(body.List) == 1 {
@@ -1175,11 +1170,13 @@ func updateCalleeParams(calleeDecl *ast.FuncDecl, params []*parameter) {
11751170
}
11761171

11771172
// createBindingDecl constructs a "binding decl" that implements
1178-
// parameter assignment.
1173+
// parameter assignment and declares any named result variables
1174+
// referenced by the callee.
11791175
//
1180-
// If we succeed, the declaration may be used by reduction
1181-
// strategies to relax the requirement that all parameters
1182-
// have been substituted.
1176+
// It may not always be possible to create the decl (e.g. due to
1177+
// shadowing), in which case it returns nil; but if it succeeds, the
1178+
// declaration may be used by reduction strategies to relax the
1179+
// requirement that all parameters have been substituted.
11831180
//
11841181
// For example, a call:
11851182
//
@@ -1210,7 +1207,7 @@ func updateCalleeParams(calleeDecl *ast.FuncDecl, params []*parameter) {
12101207
//
12111208
// Strategies may impose additional checks on return
12121209
// conversions, labels, defer, etc.
1213-
func createBindingDecl(logf func(string, ...any), caller *Caller, args []*argument, calleeDecl *ast.FuncDecl) ast.Stmt {
1210+
func createBindingDecl(logf func(string, ...any), caller *Caller, args []*argument, calleeDecl *ast.FuncDecl, results []*paramInfo) ast.Stmt {
12141211
// Spread calls are tricky as they may not align with the
12151212
// parameters' field groupings nor types.
12161213
// For example, given
@@ -1228,27 +1225,15 @@ func createBindingDecl(logf func(string, ...any), caller *Caller, args []*argume
12281225
return nil
12291226
}
12301227

1231-
// Compute remaining argument expressions.
1232-
var values []ast.Expr
1233-
for _, arg := range args {
1234-
if arg != nil {
1235-
values = append(values, arg.expr)
1236-
}
1237-
}
1238-
12391228
var (
12401229
specs []ast.Spec
12411230
shadowed = make(map[string]bool) // names defined by previous specs
12421231
)
1243-
for _, field := range calleeDecl.Type.Params.List {
1244-
// Each field (param group) becomes a ValueSpec.
1245-
spec := &ast.ValueSpec{
1246-
Names: field.Names,
1247-
Type: field.Type,
1248-
Values: values[:len(field.Names)],
1249-
}
1250-
values = values[len(field.Names):]
1251-
1232+
// shadow reports whether any name referenced by spec is
1233+
// shadowed by a name declared by a previous spec (since,
1234+
// unlike parameters, each spec of a var decl is within the
1235+
// scope of the previous specs).
1236+
shadow := func(spec *ast.ValueSpec) bool {
12521237
// Compute union of free names of type and values
12531238
// and detect shadowing. Values is the arguments
12541239
// (caller syntax), so we can use type info.
@@ -1260,22 +1245,78 @@ func createBindingDecl(logf func(string, ...any), caller *Caller, args []*argume
12601245
free[name] = true
12611246
}
12621247
}
1263-
freeishNames(free, field.Type)
1248+
freeishNames(free, spec.Type)
12641249
for name := range free {
12651250
if shadowed[name] {
12661251
logf("binding decl would shadow free name %q", name)
1267-
return nil
1252+
return true
12681253
}
12691254
}
12701255
for _, id := range spec.Names {
12711256
if id.Name != "_" {
12721257
shadowed[id.Name] = true
12731258
}
12741259
}
1260+
return false
1261+
}
12751262

1263+
// parameters
1264+
//
1265+
// Bind parameters that were not eliminated through
1266+
// substitution. (Non-nil arguments correspond to the
1267+
// remaining parameters in calleeDecl.)
1268+
var values []ast.Expr
1269+
for _, arg := range args {
1270+
if arg != nil {
1271+
values = append(values, arg.expr)
1272+
}
1273+
}
1274+
for _, field := range calleeDecl.Type.Params.List {
1275+
// Each field (param group) becomes a ValueSpec.
1276+
spec := &ast.ValueSpec{
1277+
Names: field.Names,
1278+
Type: field.Type,
1279+
Values: values[:len(field.Names)],
1280+
}
1281+
values = values[len(field.Names):]
1282+
if shadow(spec) {
1283+
return nil
1284+
}
12761285
specs = append(specs, spec)
12771286
}
12781287
assert(len(values) == 0, "args/params mismatch")
1288+
1289+
// results
1290+
//
1291+
// Add specs to declare any named result
1292+
// variables that are referenced by the body.
1293+
if calleeDecl.Type.Results != nil {
1294+
resultIdx := 0
1295+
for _, field := range calleeDecl.Type.Results.List {
1296+
if field.Names == nil {
1297+
resultIdx++
1298+
continue // unnamed field
1299+
}
1300+
var names []*ast.Ident
1301+
for _, id := range field.Names {
1302+
if len(results[resultIdx].Refs) > 0 {
1303+
names = append(names, id)
1304+
}
1305+
resultIdx++
1306+
}
1307+
if len(names) > 0 {
1308+
spec := &ast.ValueSpec{
1309+
Names: names,
1310+
Type: field.Type,
1311+
}
1312+
if shadow(spec) {
1313+
return nil
1314+
}
1315+
specs = append(specs, spec)
1316+
}
1317+
}
1318+
}
1319+
12791320
decl := &ast.DeclStmt{
12801321
Decl: &ast.GenDecl{
12811322
Tok: token.VAR,

internal/refactor/inline/inline_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,82 @@ func TestTable(t *testing.T) {
526526
}
527527
}`,
528528
},
529+
// Treatment of named result vars across strategies:
530+
{
531+
"Stmt-context call to {return g()} that mentions named result.",
532+
`func f() (x int) { return g(x) }; func g(int) int`,
533+
`func _() { f() }`,
534+
`func _() {
535+
{
536+
var x int
537+
g(x)
538+
}
539+
}`,
540+
},
541+
{
542+
"Ditto, with binding decl (due to repeated y refs).",
543+
`func f(y string) (x string) { return x+y+y }`,
544+
`func _() { f("") }`,
545+
`func _() {
546+
{
547+
var (
548+
y string = ""
549+
x string
550+
)
551+
_ = x + y + y
552+
}
553+
}`,
554+
},
555+
{
556+
"Stmt-context call to {return binary} that mentions named result.",
557+
`func f() (x int) { return x+x }`,
558+
`func _() { f() }`,
559+
`func _() {
560+
{
561+
var x int
562+
_ = x + x
563+
}
564+
}`,
565+
},
566+
{
567+
"Ditto, with binding decl again.",
568+
`func f(y string) (x int) { return x+x+len(y+y) }`,
569+
`func _() { f("") }`,
570+
`func _() {
571+
{
572+
var (
573+
y string = ""
574+
x int
575+
)
576+
_ = x + x + len(y+y)
577+
}
578+
}`,
579+
},
580+
{
581+
"Tail call to {return expr} that mentions named result.",
582+
`func f() (x int) { return x }`,
583+
`func _() int { return f() }`,
584+
`func _() int { return func() (x int) { return x }() }`,
585+
},
586+
{
587+
"Tail call to {return} that implicitly reads named result.",
588+
`func f() (x int) { return }`,
589+
`func _() int { return f() }`,
590+
`func _() int { return func() (x int) { return }() }`,
591+
},
592+
{
593+
"Spread-context call to {return expr} that mentions named result.",
594+
`func f() (x, y int) { return x, y }`,
595+
`func _() { var _, _ = f() }`,
596+
`func _() { var _, _ = func() (x, y int) { return x, y }() }`,
597+
},
598+
{
599+
"Shadowing in binding decl for named results => literalization.",
600+
`func f(y string) (x y) { return x+x+len(y+y) }; type y = int`,
601+
`func _() { f("") }`,
602+
`func _() { func(y string) (x y) { return x + x + len(y+y) }("") }`,
603+
},
604+
529605
// TODO(adonovan): improve coverage of the cross
530606
// product of each strategy with the checklist of
531607
// concerns enumerated in the package doc comment.

0 commit comments

Comments
 (0)