Skip to content

Commit 7c6d4c6

Browse files
committed
internal/refactor/inline: handle generic functions
This CL is a first step towards support for inlining all forms of generic functions. Its limitations include: - No support for methods on generic types. - Conservative shadowing. - Unnecessary type conversions (see generic.txtar, a1). - Conservative parenthesizing (see generic.txtar, file a1a). For golang/go#68236. Change-Id: Ib00b89cb61c611e8d1efd0e2f8b5d93032638b83 Reviewed-on: https://go-review.googlesource.com/c/tools/+/666716 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 6da8d2e commit 7c6d4c6

File tree

6 files changed

+267
-38
lines changed

6 files changed

+267
-38
lines changed

internal/refactor/inline/callee.go

+52-19
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type gobCallee struct {
4242
ValidForCallStmt bool // function body is "return expr" where expr is f() or <-ch
4343
NumResults int // number of results (according to type, not ast.FieldList)
4444
Params []*paramInfo // information about parameters (incl. receiver)
45+
TypeParams []*paramInfo // information about type parameters
4546
Results []*paramInfo // information about result variables
4647
Effects []int // order in which parameters are evaluated (see calleefx)
4748
HasDefer bool // uses defer
@@ -113,17 +114,6 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
113114
return nil, fmt.Errorf("cannot inline function %s as it has no body", name)
114115
}
115116

116-
// TODO(adonovan): support inlining of instantiated generic
117-
// functions by replacing each occurrence of a type parameter
118-
// T by its instantiating type argument (e.g. int). We'll need
119-
// to wrap the instantiating type in parens when it's not an
120-
// ident or qualified ident to prevent "if x == struct{}"
121-
// parsing ambiguity, or "T(x)" where T = "*int" or "func()"
122-
// from misparsing.
123-
if funcHasTypeParams(decl) {
124-
return nil, fmt.Errorf("cannot inline generic function %s: type parameters are not yet supported", name)
125-
}
126-
127117
// Record the location of all free references in the FuncDecl.
128118
// (Parameters are not free by this definition.)
129119
var (
@@ -347,6 +337,7 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
347337
}
348338

349339
params, results, effects, falcon := analyzeParams(logf, fset, info, decl)
340+
tparams := analyzeTypeParams(logf, fset, info, decl)
350341
return &Callee{gobCallee{
351342
Content: content,
352343
PkgPath: pkg.Path(),
@@ -357,6 +348,7 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa
357348
ValidForCallStmt: validForCallStmt,
358349
NumResults: sig.Results().Len(),
359350
Params: params,
351+
TypeParams: tparams,
360352
Results: results,
361353
Effects: effects,
362354
HasDefer: hasDefer,
@@ -404,20 +396,15 @@ type refInfo struct {
404396
IsSelectionOperand bool
405397
}
406398

407-
// analyzeParams computes information about parameters of function fn,
399+
// analyzeParams computes information about parameters of the function declared by decl,
408400
// including a simple "address taken" escape analysis.
409401
//
410402
// It returns two new arrays, one of the receiver and parameters, and
411-
// the other of the result variables of function fn.
403+
// the other of the result variables of the function.
412404
//
413405
// The input must be well-typed.
414406
func analyzeParams(logf func(string, ...any), fset *token.FileSet, info *types.Info, decl *ast.FuncDecl) (params, results []*paramInfo, effects []int, _ falconResult) {
415-
fnobj, ok := info.Defs[decl.Name]
416-
if !ok {
417-
panic(fmt.Sprintf("%s: no func object for %q",
418-
fset.PositionFor(decl.Name.Pos(), false), decl.Name)) // ill-typed?
419-
}
420-
sig := fnobj.Type().(*types.Signature)
407+
sig := signature(fset, info, decl)
421408

422409
paramInfos := make(map[*types.Var]*paramInfo)
423410
{
@@ -504,6 +491,52 @@ func analyzeParams(logf func(string, ...any), fset *token.FileSet, info *types.I
504491
return params, results, effects, falcon
505492
}
506493

494+
// analyzeTypeParams computes information about the type parameters of the function declared by decl.
495+
func analyzeTypeParams(_ logger, fset *token.FileSet, info *types.Info, decl *ast.FuncDecl) []*paramInfo {
496+
sig := signature(fset, info, decl)
497+
paramInfos := make(map[*types.TypeName]*paramInfo)
498+
var params []*paramInfo
499+
collect := func(tpl *types.TypeParamList) {
500+
for i := range tpl.Len() {
501+
typeName := tpl.At(i).Obj()
502+
info := &paramInfo{Name: typeName.Name()}
503+
params = append(params, info)
504+
paramInfos[typeName] = info
505+
}
506+
}
507+
collect(sig.RecvTypeParams())
508+
collect(sig.TypeParams())
509+
510+
// Find references.
511+
// We don't care about most of the properties that matter for parameter references:
512+
// a type is immutable, cannot have its address taken, and does not undergo conversions.
513+
// TODO(jba): can we nevertheless combine this with the traversal in analyzeParams?
514+
var stack []ast.Node
515+
stack = append(stack, decl.Type) // for scope of function itself
516+
astutil.PreorderStack(decl.Body, stack, func(n ast.Node, stack []ast.Node) bool {
517+
if id, ok := n.(*ast.Ident); ok {
518+
if v, ok := info.Uses[id].(*types.TypeName); ok {
519+
if pinfo, ok := paramInfos[v]; ok {
520+
ref := refInfo{Offset: int(n.Pos() - decl.Pos())}
521+
pinfo.Refs = append(pinfo.Refs, ref)
522+
pinfo.Shadow = pinfo.Shadow.add(info, nil, pinfo.Name, stack)
523+
}
524+
}
525+
}
526+
return true
527+
})
528+
return params
529+
}
530+
531+
func signature(fset *token.FileSet, info *types.Info, decl *ast.FuncDecl) *types.Signature {
532+
fnobj, ok := info.Defs[decl.Name]
533+
if !ok {
534+
panic(fmt.Sprintf("%s: no func object for %q",
535+
fset.PositionFor(decl.Name.Pos(), false), decl.Name)) // ill-typed?
536+
}
537+
return fnobj.Type().(*types.Signature)
538+
}
539+
507540
// -- callee helpers --
508541

509542
// analyzeAssignment looks at the the given stack, and analyzes certain

internal/refactor/inline/inline.go

+84-2
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,14 @@ func (st *state) inlineCall() (*inlineCallResult, error) {
839839
}
840840
}
841841

842+
typeArgs := st.typeArguments(caller.Call)
843+
if len(typeArgs) != len(callee.TypeParams) {
844+
return nil, fmt.Errorf("cannot inline: type parameter inference is not yet supported")
845+
}
846+
if err := substituteTypeParams(logf, callee.TypeParams, typeArgs, params, replaceCalleeID); err != nil {
847+
return nil, err
848+
}
849+
842850
// Log effective arguments.
843851
for i, arg := range args {
844852
logf("arg #%d: %s pure=%t effects=%t duplicable=%t free=%v type=%v",
@@ -1378,6 +1386,35 @@ type argument struct {
13781386
desugaredRecv bool // is *recv or &recv, where operator was elided
13791387
}
13801388

1389+
// typeArguments returns the type arguments of the call.
1390+
// It only collects the arguments that are explicitly provided; it does
1391+
// not attempt type inference.
1392+
func (st *state) typeArguments(call *ast.CallExpr) []*argument {
1393+
var exprs []ast.Expr
1394+
switch d := ast.Unparen(call.Fun).(type) {
1395+
case *ast.IndexExpr:
1396+
exprs = []ast.Expr{d.Index}
1397+
case *ast.IndexListExpr:
1398+
exprs = d.Indices
1399+
default:
1400+
// No type arguments
1401+
return nil
1402+
}
1403+
var args []*argument
1404+
for _, e := range exprs {
1405+
arg := &argument{expr: e, freevars: freeVars(st.caller.Info, e)}
1406+
// Wrap the instantiating type in parens when it's not an
1407+
// ident or qualified ident to prevent "if x == struct{}"
1408+
// parsing ambiguity, or "T(x)" where T = "*int" or "func()"
1409+
// from misparsing.
1410+
if _, ok := arg.expr.(*ast.Ident); !ok {
1411+
arg.expr = &ast.ParenExpr{X: arg.expr}
1412+
}
1413+
args = append(args, arg)
1414+
}
1415+
return args
1416+
}
1417+
13811418
// arguments returns the effective arguments of the call.
13821419
//
13831420
// If the receiver argument and parameter have
@@ -1413,6 +1450,9 @@ func (st *state) arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 fun
14131450

14141451
callArgs := caller.Call.Args
14151452
if calleeDecl.Recv != nil {
1453+
if len(st.callee.impl.TypeParams) > 0 {
1454+
return nil, fmt.Errorf("cannot inline: generic methods not yet supported")
1455+
}
14161456
sel := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr)
14171457
seln := caller.Info.Selections[sel]
14181458
var recvArg ast.Expr
@@ -1536,9 +1576,52 @@ type parameter struct {
15361576
// A replacer replaces an identifier at the given offset in the callee.
15371577
// The replacement tree must not belong to the caller; use cloneNode as needed.
15381578
// If unpackVariadic is set, the replacement is a composite resulting from
1539-
// variadic elimination, and may be unpackeded into variadic calls.
1579+
// variadic elimination, and may be unpacked into variadic calls.
15401580
type replacer = func(offset int, repl ast.Expr, unpackVariadic bool)
15411581

1582+
// substituteTypeParams replaces type parameters in the callee with the corresponding type arguments
1583+
// from the call.
1584+
func substituteTypeParams(logf logger, typeParams []*paramInfo, typeArgs []*argument, params []*parameter, replace replacer) error {
1585+
assert(len(typeParams) == len(typeArgs), "mismatched number of type params/args")
1586+
for i, paramInfo := range typeParams {
1587+
arg := typeArgs[i]
1588+
// Perform a simplified, conservative shadow analysis: fail if there is any shadowing.
1589+
for free := range arg.freevars {
1590+
if paramInfo.Shadow[free] != 0 {
1591+
return fmt.Errorf("cannot inline: type argument #%d (type parameter %s) is shadowed", i, paramInfo.Name)
1592+
}
1593+
}
1594+
logf("replacing type param %s with %s", paramInfo.Name, debugFormatNode(token.NewFileSet(), arg.expr))
1595+
for _, ref := range paramInfo.Refs {
1596+
replace(ref.Offset, internalastutil.CloneNode(arg.expr), false)
1597+
}
1598+
// Also replace parameter field types.
1599+
// TODO(jba): find a way to do this that is not so slow and clumsy.
1600+
// Ideally, we'd walk each p.fieldType once, replacing all type params together.
1601+
for _, p := range params {
1602+
if id, ok := p.fieldType.(*ast.Ident); ok && id.Name == paramInfo.Name {
1603+
p.fieldType = arg.expr
1604+
} else {
1605+
for _, id := range identsNamed(p.fieldType, paramInfo.Name) {
1606+
replaceNode(p.fieldType, id, arg.expr)
1607+
}
1608+
}
1609+
}
1610+
}
1611+
return nil
1612+
}
1613+
1614+
func identsNamed(n ast.Node, name string) []*ast.Ident {
1615+
var ids []*ast.Ident
1616+
ast.Inspect(n, func(n ast.Node) bool {
1617+
if id, ok := n.(*ast.Ident); ok && id.Name == name {
1618+
ids = append(ids, id)
1619+
}
1620+
return true
1621+
})
1622+
return ids
1623+
}
1624+
15421625
// substitute implements parameter elimination by substitution.
15431626
//
15441627
// It considers each parameter and its corresponding argument in turn
@@ -1664,7 +1747,6 @@ next:
16641747
// parameter is also removed by substitution.
16651748

16661749
sg[arg] = nil // Absent shadowing, the arg is substitutable.
1667-
16681750
for free := range arg.freevars {
16691751
switch s := param.info.Shadow[free]; {
16701752
case s < 0:

internal/refactor/inline/inline_test.go

+30-8
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,22 @@ func doInlineNote(logf func(string, ...any), pkg *packages.Package, file *ast.Fi
308308
if want, ok := want.([]byte); ok {
309309
got = append(bytes.TrimSpace(got), '\n')
310310
want = append(bytes.TrimSpace(want), '\n')
311-
if diff := diff.Unified("want", "got", string(want), string(got)); diff != "" {
312-
return fmt.Errorf("Inline returned wrong output:\n%s\nWant:\n%s\nDiff:\n%s",
313-
got, want, diff)
311+
// If the "want" file begins "...", it need only be a substring of the "got" result,
312+
// rather than an exact match.
313+
if rest, ok := bytes.CutPrefix(want, []byte("...\n")); ok {
314+
want = rest
315+
if !bytes.Contains(got, want) {
316+
return fmt.Errorf("Inline returned wrong output:\n%s\nWant substring:\n%s", got, want)
317+
}
318+
} else {
319+
if diff := diff.Unified("want", "got", string(want), string(got)); diff != "" {
320+
return fmt.Errorf("Inline returned wrong output:\n%s\nWant:\n%s\nDiff:\n%s",
321+
got, want, diff)
322+
}
314323
}
315324
return nil
316325
}
317326
return fmt.Errorf("Inline succeeded unexpectedly: want error matching %q, got <<%s>>", want, got)
318-
319327
}
320328

321329
// findFuncByPosition returns the FuncDecl at the specified (package-agnostic) position.
@@ -364,16 +372,16 @@ type testcase struct {
364372
func TestErrors(t *testing.T) {
365373
runTests(t, []testcase{
366374
{
367-
"Generic functions are not yet supported.",
375+
"Inference of type parameters is not yet supported.",
368376
`func f[T any](x T) T { return x }`,
369377
`var _ = f(0)`,
370-
`error: type parameters are not yet supported`,
378+
`error: type parameter inference is not yet supported`,
371379
},
372380
{
373381
"Methods on generic types are not yet supported.",
374382
`type G[T any] struct{}; func (G[T]) f(x T) T { return x }`,
375383
`var _ = G[int]{}.f(0)`,
376-
`error: type parameters are not yet supported`,
384+
`error: generic methods not yet supported`,
377385
},
378386
})
379387
}
@@ -434,6 +442,13 @@ func TestBasics(t *testing.T) {
434442
}
435443
}`,
436444
},
445+
{
446+
"Explicit type parameters.",
447+
`func f[T any](x T) T { return x }`,
448+
`var _ = f[int](0)`,
449+
// TODO(jba): remove the unnecessary conversion.
450+
`var _ = int(0)`,
451+
},
437452
})
438453
}
439454

@@ -602,7 +617,6 @@ func f1(i int) int { return i + 1 }`,
602617
`func _() { print(F(f1), F(f1)) }`,
603618
},
604619
})
605-
606620
})
607621
}
608622

@@ -1832,6 +1846,14 @@ func runTests(t *testing.T, tests []testcase) {
18321846
if fun.Name == funcName {
18331847
call = n
18341848
}
1849+
case *ast.IndexExpr:
1850+
if id, ok := fun.X.(*ast.Ident); ok && id.Name == funcName {
1851+
call = n
1852+
}
1853+
case *ast.IndexListExpr:
1854+
if id, ok := fun.X.(*ast.Ident); ok && id.Name == funcName {
1855+
call = n
1856+
}
18351857
}
18361858
}
18371859
return call == nil

internal/refactor/inline/testdata/err-basic.txtar

-9
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@ it doesn't even reach the Indent function.
1111
module testdata
1212
go 1.12
1313

14-
-- a/generic.go --
15-
package a
16-
17-
func _() {
18-
f[int]() //@ inline(re"f", re"type parameters are not yet supported")
19-
}
20-
21-
func f[T any]() {}
22-
2314
-- a/nobody.go --
2415
package a
2516

internal/refactor/inline/testdata/err-shadow-builtin.txtar

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ func _() {
1414
}
1515

1616
func f() *int { return nil }
17+
-- a/nil-type-param.go --
18+
package a
19+
20+
func _[nil any]() {
21+
_ = f() //@ inline(re"f", re"nil.*shadowed.*by.*typename.*line 3")
22+
}
1723

1824
-- a/nil-typename.go --
1925
package a

0 commit comments

Comments
 (0)