Skip to content
Closed
58 changes: 48 additions & 10 deletions internal/refactor/inline/inline.go
Original file line number Diff line number Diff line change
Expand Up @@ -1437,10 +1437,29 @@ next:
// other arguments are given explicit types in either
// a binding decl or when using the literalization
// strategy.
if len(param.info.Refs) > 0 && !trivialConversion(args[i].constant, args[i].typ, params[i].obj.Type()) {
arg.expr = convert(params[i].fieldType, arg.expr)

// If the types are identical, we can eliminate
// redundant type conversions such as this:
//
// Callee:
// func f(i int32) { print(i) }
// Caller:
// func g() { f(int32(1)) }
// Inlined as:
// func g() { print(int32(int32(1)))
//
// Recall that non-trivial does not imply non-identical
// for constant conversions; however, at this point state.arguments
// has already re-typechecked the constant and set arg.type to
// its (possibly "untyped") inherent type, so
// the conversion from untyped 1 to int32 is non-trivial even
// though both arg and param have identical types (int32).
if len(param.info.Refs) > 0 &&
!types.Identical(arg.typ, param.obj.Type()) &&
!trivialConversion(arg.constant, arg.typ, param.obj.Type()) {
arg.expr = convert(param.fieldType, arg.expr)
logf("param %q: adding explicit %s -> %s conversion around argument",
param.info.Name, args[i].typ, params[i].obj.Type())
param.info.Name, arg.typ, param.obj.Type())
}

// It is safe to substitute param and replace it with arg.
Expand Down Expand Up @@ -2206,16 +2225,35 @@ func duplicable(info *types.Info, e ast.Expr) bool {
return false

case *ast.CallExpr:
// Don't treat a conversion T(x) as duplicable even
// if x is duplicable because it could duplicate
// allocations.
// Treat type conversions as duplicable if they do not observably allocate.
// The only cases of observable allocations are
// the `[]byte(string)` and `[]rune(string)` conversions.
//
// TODO(adonovan): there are cases to tease apart here:
// duplicating string([]byte) conversions increases
// Duplicating string([]byte) conversions increases
// allocation but doesn't change behavior, but the
// reverse, []byte(string), allocates a distinct array,
// which is observable
return false
// which is observable.

if !info.Types[e.Fun].IsType() { // check whether e.Fun is a type conversion
return false
}

fun := info.TypeOf(e.Fun)
arg := info.TypeOf(e.Args[0])

switch fun := fun.Underlying().(type) {
case *types.Slice:
// Do not mark []byte(string) and []rune(string) as duplicable.
elem, ok := fun.Elem().Underlying().(*types.Basic)
if ok && (elem.Kind() == types.Rune || elem.Kind() == types.Byte) {
from, ok := arg.Underlying().(*types.Basic)
isString := ok && from.Info()&types.IsString != 0
return !isString
}
case *types.TypeParam:
return false // be conservative
}
return true

case *ast.SelectorExpr:
if seln, ok := info.Selections[e]; ok {
Expand Down
210 changes: 173 additions & 37 deletions internal/refactor/inline/inline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,52 +414,171 @@ func TestBasics(t *testing.T) {
}

func TestDuplicable(t *testing.T) {
runTests(t, []testcase{
{
"Empty strings are duplicable.",
`func f(s string) { print(s, s) }`,
`func _() { f("") }`,
`func _() { print("", "") }`,
},
{
"Non-empty string literals are not duplicable.",
`func f(s string) { print(s, s) }`,
`func _() { f("hi") }`,
`func _() {
t.Run("basic", func(t *testing.T) {
runTests(t, []testcase{
{
"Empty strings are duplicable.",
`func f(s string) { print(s, s) }`,
`func _() { f("") }`,
`func _() { print("", "") }`,
},
{
"Non-empty string literals are not duplicable.",
`func f(s string) { print(s, s) }`,
`func _() { f("hi") }`,
`func _() {
var s string = "hi"
print(s, s)
}`,
},
{
"Empty array literals are duplicable.",
`func f(a [2]int) { print(a, a) }`,
`func _() { f([2]int{}) }`,
`func _() { print([2]int{}, [2]int{}) }`,
},
{
"Non-empty array literals are not duplicable.",
`func f(a [2]int) { print(a, a) }`,
`func _() { f([2]int{1, 2}) }`,
`func _() {
},
{
"Empty array literals are duplicable.",
`func f(a [2]int) { print(a, a) }`,
`func _() { f([2]int{}) }`,
`func _() { print([2]int{}, [2]int{}) }`,
},
{
"Non-empty array literals are not duplicable.",
`func f(a [2]int) { print(a, a) }`,
`func _() { f([2]int{1, 2}) }`,
`func _() {
var a [2]int = [2]int{1, 2}
print(a, a)
}`,
},
{
"Empty struct literals are duplicable.",
`func f(s S) { print(s, s) }; type S struct { x int }`,
`func _() { f(S{}) }`,
`func _() { print(S{}, S{}) }`,
},
{
"Non-empty struct literals are not duplicable.",
`func f(s S) { print(s, s) }; type S struct { x int }`,
`func _() { f(S{x: 1}) }`,
`func _() {
},
{
"Empty struct literals are duplicable.",
`func f(s S) { print(s, s) }; type S struct { x int }`,
`func _() { f(S{}) }`,
`func _() { print(S{}, S{}) }`,
},
{
"Non-empty struct literals are not duplicable.",
`func f(s S) { print(s, s) }; type S struct { x int }`,
`func _() { f(S{x: 1}) }`,
`func _() {
var s S = S{x: 1}
print(s, s)
}`,
},
},
})
})

t.Run("conversions", func(t *testing.T) {
runTests(t, []testcase{
{
"Conversions to integer are duplicable.",
`func f(i int) { print(i, i) }`,
`func _() { var i int8 = 1; f(int(i)) }`,
`func _() { var i int8 = 1; print(int(i), int(i)) }`,
},
{
"Implicit conversions from underlying types are duplicable.",
`func f(i I) { print(i, i) }; type I int`,
`func _() { f(1) }`,
`func _() { print(I(1), I(1)) }`,
},
{
"Conversions to array are duplicable.",
`func f(a [2]int) { print(a, a) }; type A [2]int`,
`func _() { var a A; f([2]int(a)) }`,
`func _() { var a A; print([2]int(a), [2]int(a)) }`,
},
{
"Conversions from array are duplicable.",
`func f(a A) { print(a, a) }; type A [2]int`,
`func _() { var a [2]int; f(A(a)) }`,
`func _() { var a [2]int; print(A(a), A(a)) }`,
},
{
"Conversions from byte slice to string are duplicable.",
`func f(s string) { print(s, s) }`,
`func _() { var b []byte; f(string(b)) }`,
`func _() { var b []byte; print(string(b), string(b)) }`,
},
{
"Conversions from string to byte slice are not duplicable.",
`func f(b []byte) { print(b, b) }`,
`func _() { var s string; f([]byte(s)) }`,
`func _() {
var s string
var b []byte = []byte(s)
print(b, b)
}`,
},
{
"Conversions from string to uint8 slice are not duplicable.",
`func f(b []uint8) { print(b, b) }`,
`func _() { var s string; f([]uint8(s)) }`,
`func _() {
var s string
var b []uint8 = []uint8(s)
print(b, b)
}`,
},
{
"Conversions from string to rune slice are not duplicable.",
`func f(r []rune) { print(r, r) }`,
`func _() { var s string; f([]rune(s)) }`,
`func _() {
var s string
var r []rune = []rune(s)
print(r, r)
}`,
},
{
"Conversions from string to named type with underlying byte slice are not duplicable.",
`func f(b B) { print(b, b) }; type B []byte`,
`func _() { var s string; f(B(s)) }`,
`func _() {
var s string
var b B = B(s)
print(b, b)
}`,
},
{
"Conversions from string to named type of string are duplicable.",
`func f(s S) { print(s, s) }; type S string`,
`func _() { var s string; f(S(s)) }`,
`func _() { var s string; print(S(s), S(s)) }`,
},
{
"Built-in function calls are not duplicable.",
`func f(i int) { print(i, i) }`,
`func _() { f(len("")) }`,
`func _() {
var i int = len("")
print(i, i)
}`,
},
{
"Built-in function calls are not duplicable.",
`func f(c complex128) { print(c, c) }`,
`func _() { f(complex(1.0, 2.0)) }`,
`func _() {
var c complex128 = complex(1.0, 2.0)
print(c, c)
}`,
},
{
"Non built-in function calls are not duplicable.",
`func f(i int) { print(i, i) }
//go:noinline
func f1(i int) int { return i + 1 }`,
`func _() { f(f1(1)) }`,
`func _() {
var i int = f1(1)
print(i, i)
}`,
},
{
"Conversions between function types are duplicable.",
`func f(f F) { print(f, f) }; type F func(); func f1() {}`,
`func _() { f(F(f1)) }`,
`func _() { print(F(f1), F(f1)) }`,
},
})

})
}

Expand Down Expand Up @@ -1358,6 +1477,23 @@ func TestSubstitutionPreservesParameterType(t *testing.T) {
})
}

func TestRedundantConversions(t *testing.T) {
runTests(t, []testcase{
{
"Type conversion must be added if the constant is untyped.",
`func f(i int32) { print(i) }`,
`func _() { f(1) }`,
`func _() { print(int32(1)) }`,
},
{
"Type conversion must not be added if the constant is typed.",
`func f(i int32) { print(i) }`,
`func _() { f(int32(1)) }`,
`func _() { print(int32(1)) }`,
},
})
}

func runTests(t *testing.T, tests []testcase) {
for _, test := range tests {
test := test
Expand Down
11 changes: 8 additions & 3 deletions internal/refactor/inline/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,19 @@ func within(pos token.Pos, n ast.Node) bool {
//
// trivialConversion under-approximates trivial conversions, as unfortunately
// go/types does not record the type of an expression *before* it is implicitly
// converted, and therefore it cannot distinguish typed constant constant
// converted, and therefore it cannot distinguish typed constant
// expressions from untyped constant expressions. For example, in the
// expression `c + 2`, where c is a uint32 constant, trivialConversion does not
// detect that the default type of this express is actually uint32, not untyped
// detect that the default type of this expression is actually uint32, not untyped
// int.
//
// We could, of course, do better here by reverse engineering some of go/types'
// constant handling. That may or may not be worthwhile..
// constant handling. That may or may not be worthwhile.
//
// Example: in func f() int32 { return 0 },
// the type recorded for 0 is int32, not untyped int;
// although it is Identical to the result var,
// the conversion is non-trivial.
func trivialConversion(fromValue constant.Value, from, to types.Type) bool {
if fromValue != nil {
var defaultType types.Type
Expand Down