Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/refactor/inline: allow duplicable type conversions #494

Closed
wants to merge 10 commits into from
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