Skip to content

Commit 0841661

Browse files
findleyrgopherbot
authored andcommitted
internal/refactor/inline: avoid unnecessary interface conversions
Avoid unnecessary interface conversions when inlining by detecting when references are in an interface->interface assignment context. Fixes golang/go#68554 Change-Id: I4d789d7bd1f29a16523d4feacfbe89a85f4bed0e Reviewed-on: https://go-review.googlesource.com/c/tools/+/629875 Auto-Submit: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 68b67b4 commit 0841661

File tree

4 files changed

+303
-40
lines changed

4 files changed

+303
-40
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
This test checks that inlining removes unnecessary interface conversions.
2+
3+
-- main.go --
4+
package main
5+
6+
import (
7+
"fmt"
8+
"io"
9+
)
10+
11+
func f(d discard) {
12+
g(d) //@codeaction("g", "refactor.inline.call", result=out)
13+
}
14+
15+
func g(w io.Writer) { fmt.Println(w) }
16+
17+
var d discard
18+
type discard struct{}
19+
func (discard) Write(p []byte) (int, error) { return len(p), nil }
20+
-- @out/main.go --
21+
package main
22+
23+
import (
24+
"fmt"
25+
"io"
26+
)
27+
28+
func f(d discard) {
29+
fmt.Println(d) //@codeaction("g", "refactor.inline.call", result=out)
30+
}
31+
32+
func g(w io.Writer) { fmt.Println(w) }
33+
34+
var d discard
35+
36+
type discard struct{}
37+
38+
func (discard) Write(p []byte) (int, error) { return len(p), nil }

internal/refactor/inline/callee.go

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,11 +387,16 @@ type paramInfo struct {
387387
IsResult bool // false for receiver or parameter, true for result variable
388388
Assigned bool // parameter appears on left side of an assignment statement
389389
Escapes bool // parameter has its address taken
390-
Refs []int // FuncDecl-relative byte offset of parameter ref within body
390+
Refs []refInfo // information about references to parameter within body
391391
Shadow map[string]bool // names shadowed at one of the above refs
392392
FalconType string // name of this parameter's type (if basic) in the falcon system
393393
}
394394

395+
type refInfo struct {
396+
Offset int // FuncDecl-relative byte offset of parameter ref within body
397+
NeedType bool // type of substituted arg must be identical to type of param
398+
}
399+
395400
// analyzeParams computes information about parameters of function fn,
396401
// including a simple "address taken" escape analysis.
397402
//
@@ -458,10 +463,23 @@ func analyzeParams(logf func(string, ...any), fset *token.FileSet, info *types.I
458463
if id, ok := n.(*ast.Ident); ok {
459464
if v, ok := info.Uses[id].(*types.Var); ok {
460465
if pinfo, ok := paramInfos[v]; ok {
461-
// Record location of ref to parameter/result
462-
// and any intervening (shadowing) names.
463-
offset := int(n.Pos() - decl.Pos())
464-
pinfo.Refs = append(pinfo.Refs, offset)
466+
// Record ref information, and any intervening (shadowing) names.
467+
//
468+
// If the parameter v has an interface type, and the reference id
469+
// appears in a context where assignability rules apply, there may be
470+
// an implicit interface-to-interface widening. In that case it is
471+
// not necessary to insert an explicit conversion from the argument
472+
// to the parameter's type.
473+
//
474+
// Contrapositively, if param is not an interface type, then the
475+
// assignment may lose type information, for example in the case that
476+
// the substituted expression is an untyped constant or unnamed type.
477+
ifaceParam := isNonTypeParamInterface(v.Type())
478+
ref := refInfo{
479+
Offset: int(n.Pos() - decl.Pos()),
480+
NeedType: !ifaceParam || !isAssignableOperand(info, stack),
481+
}
482+
pinfo.Refs = append(pinfo.Refs, ref)
465483
pinfo.Shadow = addShadows(pinfo.Shadow, info, pinfo.Name, stack)
466484
}
467485
}
@@ -481,6 +499,107 @@ func analyzeParams(logf func(string, ...any), fset *token.FileSet, info *types.I
481499

482500
// -- callee helpers --
483501

502+
// isAssignableOperand reports whether the given outer-to-inner stack has an
503+
// innermost expression operand for which assignability rules apply, meaning it
504+
// is used in a position where its type need only be assignable to the type
505+
// implied by its surrounding syntax.
506+
//
507+
// As this function is intended to be used for inlining analysis, it reports
508+
// false in one case where the operand technically need only be assignable: if
509+
// the type being assigned to is a call argument and contains type parameters,
510+
// then passing a different (yet assignable) type may affect type inference,
511+
// and so isAssignableOperand reports false.
512+
func isAssignableOperand(info *types.Info, stack []ast.Node) bool {
513+
var (
514+
parent ast.Node // the parent node surrounding expr
515+
expr, _ = stack[len(stack)-1].(ast.Expr) // the relevant expr, after stripping parentheses
516+
)
517+
if expr == nil {
518+
return false
519+
}
520+
for i := len(stack) - 2; i >= 0; i-- {
521+
if pexpr, ok := stack[i].(*ast.ParenExpr); ok {
522+
expr = pexpr
523+
} else {
524+
parent = stack[i]
525+
break
526+
}
527+
}
528+
if parent == nil {
529+
return false
530+
}
531+
532+
// Types do not need to match for assignment to a variable.
533+
if assign, ok := parent.(*ast.AssignStmt); ok && assign.Tok == token.ASSIGN {
534+
for _, v := range assign.Rhs {
535+
if v == expr {
536+
return true
537+
}
538+
}
539+
}
540+
541+
// Types do not need to match for an initializer with known type.
542+
if spec, ok := parent.(*ast.ValueSpec); ok && spec.Type != nil {
543+
for _, v := range spec.Values {
544+
if v == expr {
545+
return true
546+
}
547+
}
548+
}
549+
550+
// Types do not need to match for composite literal keys, values, or
551+
// fields.
552+
if kv, ok := parent.(*ast.KeyValueExpr); ok {
553+
if kv.Key == expr || kv.Value == expr {
554+
return true
555+
}
556+
}
557+
if lit, ok := parent.(*ast.CompositeLit); ok {
558+
for _, v := range lit.Elts {
559+
if v == expr {
560+
return true
561+
}
562+
}
563+
}
564+
565+
// Types do not need to match for values sent to a channel.
566+
if send, ok := parent.(*ast.SendStmt); ok {
567+
if send.Value == expr {
568+
return true
569+
}
570+
}
571+
572+
// Types do not need to match for an argument to a call, unless the
573+
// corresponding parameter has type parameters, as in that case the
574+
// argument type may affect inference.
575+
if call, ok := parent.(*ast.CallExpr); ok {
576+
for i, arg := range call.Args {
577+
if arg == expr {
578+
if fn, _ := typeutil.Callee(info, call).(*types.Func); fn != nil { // Incidentally, this check rejects a conversion T(id).
579+
sig := fn.Type().(*types.Signature)
580+
581+
// Find the relevant parameter type, accounting for variadics.
582+
var paramType types.Type
583+
if plen := sig.Params().Len(); sig.Variadic() && i >= plen-1 && !call.Ellipsis.IsValid() {
584+
if s, ok := sig.Params().At(plen - 1).Type().(*types.Slice); ok {
585+
paramType = s.Elem()
586+
}
587+
} else if i < plen {
588+
paramType = sig.Params().At(i).Type()
589+
}
590+
591+
if paramType != nil && !new(typeparams.Free).Has(paramType) {
592+
return true
593+
}
594+
}
595+
break
596+
}
597+
}
598+
}
599+
600+
return false // In all other cases, preserve the parameter type.
601+
}
602+
484603
// addShadows returns the shadows set augmented by the set of names
485604
// locally shadowed at the location of the reference in the callee
486605
// (identified by the stack). The name of the reference itself is

internal/refactor/inline/inline.go

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,40 +1604,6 @@ next:
16041604
for i, param := range params {
16051605
if arg := args[i]; arg.substitutable {
16061606

1607-
// Wrap the argument in an explicit conversion if
1608-
// substitution might materially change its type.
1609-
// (We already did the necessary shadowing check
1610-
// on the parameter type syntax.)
1611-
//
1612-
// This is only needed for substituted arguments. All
1613-
// other arguments are given explicit types in either
1614-
// a binding decl or when using the literalization
1615-
// strategy.
1616-
1617-
// If the types are identical, we can eliminate
1618-
// redundant type conversions such as this:
1619-
//
1620-
// Callee:
1621-
// func f(i int32) { print(i) }
1622-
// Caller:
1623-
// func g() { f(int32(1)) }
1624-
// Inlined as:
1625-
// func g() { print(int32(int32(1)))
1626-
//
1627-
// Recall that non-trivial does not imply non-identical
1628-
// for constant conversions; however, at this point state.arguments
1629-
// has already re-typechecked the constant and set arg.type to
1630-
// its (possibly "untyped") inherent type, so
1631-
// the conversion from untyped 1 to int32 is non-trivial even
1632-
// though both arg and param have identical types (int32).
1633-
if len(param.info.Refs) > 0 &&
1634-
!types.Identical(arg.typ, param.obj.Type()) &&
1635-
!trivialConversion(arg.constant, arg.typ, param.obj.Type()) {
1636-
arg.expr = convert(param.fieldType, arg.expr)
1637-
logf("param %q: adding explicit %s -> %s conversion around argument",
1638-
param.info.Name, arg.typ, param.obj.Type())
1639-
}
1640-
16411607
// It is safe to substitute param and replace it with arg.
16421608
// The formatter introduces parens as needed for precedence.
16431609
//
@@ -1646,14 +1612,72 @@ next:
16461612
logf("replacing parameter %q by argument %q",
16471613
param.info.Name, debugFormatNode(caller.Fset, arg.expr))
16481614
for _, ref := range param.info.Refs {
1649-
replace(ref, internalastutil.CloneNode(arg.expr).(ast.Expr), arg.variadic)
1615+
// If the reference requires exact type agreement (as reported by
1616+
// param.info.NeedType), wrap the argument in an explicit conversion
1617+
// if substitution might materially change its type. (We already did
1618+
// the necessary shadowing check on the parameter type syntax.)
1619+
//
1620+
// This is only needed for substituted arguments. All other arguments
1621+
// are given explicit types in either a binding decl or when using the
1622+
// literalization strategy.
1623+
1624+
// If the types are identical, we can eliminate
1625+
// redundant type conversions such as this:
1626+
//
1627+
// Callee:
1628+
// func f(i int32) { print(i) }
1629+
// Caller:
1630+
// func g() { f(int32(1)) }
1631+
// Inlined as:
1632+
// func g() { print(int32(int32(1)))
1633+
//
1634+
// Recall that non-trivial does not imply non-identical
1635+
// for constant conversions; however, at this point state.arguments
1636+
// has already re-typechecked the constant and set arg.type to
1637+
// its (possibly "untyped") inherent type, so
1638+
// the conversion from untyped 1 to int32 is non-trivial even
1639+
// though both arg and param have identical types (int32).
1640+
argExpr := arg.expr
1641+
if ref.NeedType &&
1642+
!types.Identical(arg.typ, param.obj.Type()) &&
1643+
!trivialConversion(arg.constant, arg.typ, param.obj.Type()) {
1644+
1645+
// If arg.expr is already an interface call, strip it.
1646+
if call, ok := argExpr.(*ast.CallExpr); ok && len(call.Args) == 1 {
1647+
if typ, ok := isConversion(caller.Info, call); ok && isNonTypeParamInterface(typ) {
1648+
argExpr = call.Args[0]
1649+
}
1650+
}
1651+
1652+
argExpr = convert(param.fieldType, argExpr)
1653+
logf("param %q (offset %d): adding explicit %s -> %s conversion around argument",
1654+
param.info.Name, ref.Offset, arg.typ, param.obj.Type())
1655+
}
1656+
replace(ref.Offset, internalastutil.CloneNode(argExpr).(ast.Expr), arg.variadic)
16501657
}
16511658
params[i] = nil // substituted
16521659
args[i] = nil // substituted
16531660
}
16541661
}
16551662
}
16561663

1664+
// isConversion reports whether the given call is a type conversion, returning
1665+
// (operand, true) if so.
1666+
//
1667+
// If the call is not a conversion, it returns (nil, false).
1668+
func isConversion(info *types.Info, call *ast.CallExpr) (types.Type, bool) {
1669+
if tv, ok := info.Types[call.Fun]; ok && tv.IsType() {
1670+
return tv.Type, true
1671+
}
1672+
return nil, false
1673+
}
1674+
1675+
// isNonTypeParamInterface reports whether t is a non-type parameter interface
1676+
// type.
1677+
func isNonTypeParamInterface(t types.Type) bool {
1678+
return !typeparams.IsTypeParam(t) && types.IsInterface(t)
1679+
}
1680+
16571681
// isUsedOutsideCall reports whether v is used outside of caller.Call, within
16581682
// the body of caller.enclosingFunc.
16591683
func isUsedOutsideCall(caller *Caller, v *types.Var) bool {

internal/refactor/inline/inline_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,88 @@ func TestRedundantConversions(t *testing.T) {
15091509
`func _() { f(int32(1)) }`,
15101510
`func _() { print(int32(1)) }`,
15111511
},
1512+
{
1513+
"No type conversion for argument to interface parameter",
1514+
`type T int; func f(x any) { g(x) }; func g(any) {}`,
1515+
`func _() { f(T(1)) }`,
1516+
`func _() { g(T(1)) }`,
1517+
},
1518+
{
1519+
"No type conversion for parenthesized argument to interface parameter",
1520+
`type T int; func f(x any) { g((x)) }; func g(any) {}`,
1521+
`func _() { f(T(1)) }`,
1522+
`func _() { g((T(1))) }`,
1523+
},
1524+
{
1525+
"Type conversion for argument to type parameter",
1526+
`type T int; func f(x any) { g(x) }; func g[P any](P) {}`,
1527+
`func _() { f(T(1)) }`,
1528+
`func _() { g(any(T(1))) }`,
1529+
},
1530+
{
1531+
"Strip redundant interface conversions",
1532+
`type T interface{ M() }; func f(x any) { g(x) }; func g[P any](P) {}`,
1533+
`func _() { f(T(nil)) }`,
1534+
`func _() { g(any(nil)) }`,
1535+
},
1536+
{
1537+
"No type conversion for argument to variadic interface parameter",
1538+
`type T int; func f(x ...any) { g(x...) }; func g(...any) {}`,
1539+
`func _() { f(T(1)) }`,
1540+
`func _() { g(T(1)) }`,
1541+
},
1542+
{
1543+
"Type conversion for variadic argument",
1544+
`type T int; func f(x ...any) { g(x...) }; func g(...any) {}`,
1545+
`func _() { f([]any{T(1)}...) }`,
1546+
`func _() { g([]any{T(1)}...) }`,
1547+
},
1548+
{
1549+
"No type conversion for assignment to an explicit interface type",
1550+
`type T int; func f(x any) { var y any; y = x; _ = y }`,
1551+
`func _() { f(T(1)) }`,
1552+
`func _() {
1553+
var y any
1554+
y = T(1)
1555+
_ = y
1556+
}`,
1557+
},
1558+
{
1559+
"No type conversion for initializer of an explicit interface type",
1560+
`type T int; func f(x any) { var y any = x; _ = y }`,
1561+
`func _() { f(T(1)) }`,
1562+
`func _() {
1563+
var y any = T(1)
1564+
_ = y
1565+
}`,
1566+
},
1567+
{
1568+
"No type conversion for use as a composite literal key",
1569+
`type T int; func f(x any) { _ = map[any]any{x: 1} }`,
1570+
`func _() { f(T(1)) }`,
1571+
`func _() { _ = map[any]any{T(1): 1} }`,
1572+
},
1573+
{
1574+
"No type conversion for use as a composite literal value",
1575+
`type T int; func f(x any) { _ = []any{x} }`,
1576+
`func _() { f(T(1)) }`,
1577+
`func _() { _ = []any{T(1)} }`,
1578+
},
1579+
{
1580+
"No type conversion for use as a composite literal field",
1581+
`type T int; func f(x any) { _ = struct{ F any }{F: x} }`,
1582+
`func _() { f(T(1)) }`,
1583+
`func _() { _ = struct{ F any }{F: T(1)} }`,
1584+
},
1585+
{
1586+
"No type conversion for use in a send statement",
1587+
`type T int; func f(x any) { var c chan any; c <- x }`,
1588+
`func _() { f(T(1)) }`,
1589+
`func _() {
1590+
var c chan any
1591+
c <- T(1)
1592+
}`,
1593+
},
15121594
})
15131595
}
15141596

0 commit comments

Comments
 (0)