inline

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2023 License: BSD-3-Clause Imports: 16 Imported by: 0

Documentation

Overview

Package inline implements inlining of Go function calls.

The client provides information about the caller and callee, including the source text, syntax tree, and type information, and the inliner returns the modified source file for the caller, or an error if the inlining operation is invalid (for example because the function body refers to names that are inaccessible to the caller).

Although this interface demands more information from the client than might seem necessary, it enables smoother integration with existing batch and interactive tools that have their own ways of managing the processes of reading, parsing, and type-checking packages. In particular, this package does not assume that the caller and callee belong to the same token.FileSet or types.Importer realms.

In general, inlining consists of modifying a function or method call expression f(a1, ..., an) so that the name of the function f is replaced ("literalized") by a literal copy of the function declaration, with free identifiers suitably modified to use the locally appropriate identifiers or perhaps constant argument values.

Inlining must not change the semantics of the call. Semantics preservation is crucial for clients such as codebase maintenance tools that automatically inline all calls to designated functions on a large scale. Such tools must not introduce subtle behavior changes. (Fully inlining a call is dynamically observable using reflection over the call stack, but this exception to the rule is explicitly allowed.)

In some special cases it is possible to entirely replace ("reduce") the call by a copy of the function's body in which parameters have been replaced by arguments, but this is surprisingly tricky for a number of reasons, some of which are listed here for illustration:

  • Any effects of the call argument expressions must be preserved, even if the corresponding parameters are never referenced, or are referenced multiple times, or are referenced in a different order from the arguments.

  • Even an argument expression as simple as ptr.x may not be referentially transparent, because another argument may have the effect of changing the value of ptr.

  • Although constants are referentially transparent, as a matter of style we do not wish to duplicate literals that are referenced multiple times in the body because this undoes proper factoring. Also, string literals may be arbitrarily large.

  • If the function body consists of statements other than just "return expr", in some contexts it may be syntactically impossible to replace the call expression by the body statements. Consider "} else if x := f(); cond { ... }". (Go has no equivalent to Lisp's progn or Rust's blocks.)

  • Similarly, without the equivalent of Rust-style blocks and first-class tuples, there is no general way to reduce a call to a function such as > func(params)(args)(results) { stmts; return body } to an expression such as > { var params = args; stmts; body } or even a statement such as > results = { var params = args; stmts; body } Consequently the declaration and scope of the result variables, and the assignment and control-flow implications of the return statement, must be dealt with by cases.

  • A standalone call statement that calls a function whose body is "return expr" cannot be simply replaced by the body expression if it is not itself a call or channel receive expression; it is necessary to explicitly discard the result using "_ = expr".

    Similarly, if the body is a call expression, only calls to some built-in functions with no result (such as copy or panic) are permitted as statements, whereas others (such as append) return a result that must be used, even if just by discarding.

  • If a parameter or result variable is updated by an assignment within the function body, it cannot always be safely replaced by a variable in the caller. For example, given > func f(a int) int { a++; return a } The call y = f(x) cannot be replaced by { x++; y = x } because this would change the value of the caller's variable x. Only if the caller is finished with x is this safe.

    A similar argument applies to parameter or result variables that escape: by eliminating a variable, inlining would change the identity of the variable that escapes.

  • If the function body uses 'defer' and the inlined call is not a tail-call, inlining may delay the deferred effects.

  • Each control label that is used by both caller and callee must be α-renamed.

  • Given > func f() uint8 { return 0 } > var x any = f() reducing the call to var x any = 0 is unsound because it discards the implicit conversion. We may need to make each argument->parameter and return->result assignment conversion implicit if the types differ. Assignments to variadic parameters may need to explicitly construct a slice.

More complex callee functions are inlinable with more elaborate and invasive changes to the statements surrounding the call expression.

TODO(adonovan): future work:

  • Handle more of the above special cases by careful analysis, thoughtful factoring of the large design space, and thorough test coverage.

  • Write a fuzz-like test that selects function calls at random in the corpus, inlines them, and checks that the result is either a sensible error or a valid transformation.

  • Eliminate parameters that are unreferenced in the callee and whose argument expression is side-effect free.

  • Afford the client more control such as a limit on the total increase in line count, or a refusal to inline using the general approach (replacing name by function literal). This could be achieved by returning metadata alongside the result and having the client conditionally discard the change.

  • Is it acceptable to skip effects that are limited to runtime panics? Can we avoid evaluating an argument x.f or a[i] when the corresponding parameter is unused?

  • When caller syntax permits a block, replace argument-to-parameter assignment by a set of local var decls, e.g. f(1, 2) would become { var x, y = 1, 2; body... }.

    But even this is complicated: a single var decl initializer cannot declare all the parameters and initialize them to their arguments in one go if they have varied types. Instead, one must use multiple specs such as: > { var x int = 1; var y int32 = 2; body ...} but this means that the initializer expression for y is within the scope of x, so it may require α-renaming.

    It is tempting to use a short var decl { x, y := 1, 2; body ...} as it permits simultaneous declaration and initialization of many variables of varied type. However, one must take care to convert each argument expression to the correct parameter variable type, perhaps explicitly. (Consider "x := 1 << 64".)

    Also, as a matter of style, having all parameter declarations and argument expressions in a single statement is potentially unwieldy.

  • Support inlining of generic functions, replacing type parameters by their instantiations.

  • Support inlining of calls to function literals such as: > f := func(...) { ...} > f() including recursive ones: > var f func(...) > f = func(...) { ...f...} > f() But note that the existing algorithm makes widespread assumptions that the callee is a package-level function or method.

  • Eliminate parens inserted conservatively when they are redundant.

  • Allow non-'go' build systems such as Bazel/Blaze a chance to decide whether an import is accessible using logic other than "/internal/" path segments. This could be achieved by returning the list of added import paths.

  • Inlining a function from another module may change the effective version of the Go language spec that governs it. We should probably make the client responsible for rejecting attempts to inline from newer callees to older callers, since there's no way for this package to access module versions.

  • Use an alternative implementation of the import-organizing operation that doesn't require operating on a complete file (and reformatting). Then return the results in a higher-level form as a set of import additions and deletions plus a single diff that encloses the call expression. This interface could perhaps be implemented atop imports.Process by post-processing its result to obtain the abstract import changes and discarding its formatted output.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Inline

func Inline(caller *Caller, callee_ *Callee) ([]byte, error)

Inline inlines the called function (callee) into the function call (caller) and returns the updated, formatted content of the caller source file.

Types

type Callee

type Callee struct {
	// contains filtered or unexported fields
}

A Callee holds information about an inlinable function. Gob-serializable.

func AnalyzeCallee

func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, decl *ast.FuncDecl, content []byte) (*Callee, error)

AnalyzeCallee analyzes a function that is a candidate for inlining and returns a Callee that describes it. The Callee object, which is serializable, can be passed to one or more subsequent calls to Inline, each with a different Caller.

This design allows separate analysis of callers and callees in the golang.org/x/tools/go/analysis framework: the inlining information about a callee can be recorded as a "fact".

func (*Callee) GobDecode

func (callee *Callee) GobDecode(data []byte) error

func (*Callee) GobEncode

func (callee *Callee) GobEncode() ([]byte, error)

func (*Callee) String

func (callee *Callee) String() string

type Caller

type Caller struct {
	Fset    *token.FileSet
	Types   *types.Package
	Info    *types.Info
	File    *ast.File
	Call    *ast.CallExpr
	Content []byte
}

A Caller describes the function call and its enclosing context.

The client is responsible for populating this struct and passing it to Inline.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL