neo-go/pkg/compiler/analysis.go
Evgeniy Stratonikov 0bc81aecf4 compiler: do not emit code for unused imported functions
Our current algorithm marks function as used if it is called
at least ones, even if the callee function is itself unused.
This commit implements more clever traversal to collect usage
information more precisely.

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
2021-10-13 15:56:07 +03:00

388 lines
10 KiB
Go

package compiler
import (
"errors"
"go/ast"
"go/token"
"go/types"
"strings"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"golang.org/x/tools/go/loader" //nolint:staticcheck // SA1019: package golang.org/x/tools/go/loader is deprecated
)
var (
// Go language builtin functions.
goBuiltins = []string{"len", "append", "panic", "make", "copy", "recover", "delete"}
// Custom builtin utility functions.
customBuiltins = []string{
"FromAddress", "Equals", "Remove",
"ToBool", "ToBytes", "ToString", "ToInteger",
}
)
// newGlobal creates new global variable.
func (c *codegen) newGlobal(pkg string, name string) {
name = c.getIdentName(pkg, name)
c.globals[name] = len(c.globals)
}
// getIdentName returns fully-qualified name for a variable.
func (c *codegen) getIdentName(pkg string, name string) string {
if fullName, ok := c.importMap[pkg]; ok {
pkg = fullName
}
return pkg + "." + name
}
// traverseGlobals visits and initializes global variables.
// It returns `true` if contract has `_deploy` function.
func (c *codegen) traverseGlobals() bool {
var hasDefer bool
var n, nConst int
var hasDeploy bool
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
nv, nc := countGlobals(f)
n += nv
nConst += nc
if !hasDeploy || !hasDefer {
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.FuncDecl:
hasDeploy = hasDeploy || isDeployFunc(n)
case *ast.DeferStmt:
hasDefer = true
return false
}
return true
})
}
})
if hasDefer {
n++
}
if n > 255 {
c.prog.BinWriter.Err = errors.New("too many global variables")
return hasDeploy
}
if n != 0 {
emit.Instruction(c.prog.BinWriter, opcode.INITSSLOT, []byte{byte(n)})
}
initOffset := c.prog.Len()
emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{0, 0})
lastCnt, maxCnt := -1, -1
c.ForEachPackage(func(pkg *loader.PackageInfo) {
if n+nConst > 0 {
for _, f := range pkg.Files {
c.fillImportMap(f, pkg.Pkg)
c.convertGlobals(f, pkg.Pkg)
}
}
for _, f := range pkg.Files {
c.fillImportMap(f, pkg.Pkg)
var currMax int
lastCnt, currMax = c.convertInitFuncs(f, pkg.Pkg, lastCnt)
if currMax > maxCnt {
maxCnt = currMax
}
}
// because we reuse `convertFuncDecl` for init funcs,
// we need to cleare scope, so that global variables
// encountered after will be recognized as globals.
c.scope = nil
})
if c.globalInlineCount > maxCnt {
maxCnt = c.globalInlineCount
}
// Here we remove `INITSLOT` if no code was emitted for `init` function.
// Note that the `INITSSLOT` must stay in place.
hasNoInit := initOffset+3 == c.prog.Len()
if hasNoInit {
buf := c.prog.Bytes()
c.prog.Reset()
c.prog.WriteBytes(buf[:initOffset])
}
if initOffset != 0 || !hasNoInit { // if there are some globals or `init()`.
c.initEndOffset = c.prog.Len()
emit.Opcodes(c.prog.BinWriter, opcode.RET)
if maxCnt >= 0 {
c.reverseOffsetMap[initOffset] = nameWithLocals{
name: "init",
count: maxCnt,
}
}
}
// store auxiliary variables after all others.
if hasDefer {
c.exceptionIndex = len(c.globals)
c.globals[exceptionVarName] = c.exceptionIndex
}
return hasDeploy
}
// countGlobals counts the global variables in the program to add
// them with the stack size of the function.
// Second returned argument contains amount of global constants.
func countGlobals(f ast.Node) (int, int) {
var numVar, numConst int
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
// Skip all function declarations if we have already encountered `defer`.
case *ast.FuncDecl:
return false
// After skipping all funcDecls we are sure that each value spec
// is a global declared variable or constant.
case *ast.GenDecl:
isVar := n.Tok == token.VAR
if isVar || n.Tok == token.CONST {
for _, s := range n.Specs {
for _, id := range s.(*ast.ValueSpec).Names {
if id.Name != "_" {
if isVar {
numVar++
} else {
numConst++
}
}
}
}
}
return false
}
return true
})
return numVar, numConst
}
// isExprNil looks if the given expression is a `nil`.
func isExprNil(e ast.Expr) bool {
v, ok := e.(*ast.Ident)
return ok && v.Name == "nil"
}
// indexOfStruct returns the index of the given field inside that struct.
// If the struct does not contain that field it will return -1.
func indexOfStruct(strct *types.Struct, fldName string) int {
for i := 0; i < strct.NumFields(); i++ {
if strct.Field(i).Name() == fldName {
return i
}
}
return -1
}
type funcUsage map[string]bool
func (f funcUsage) funcUsed(name string) bool {
_, ok := f[name]
return ok
}
// lastStmtIsReturn checks if last statement of the declaration was return statement..
func lastStmtIsReturn(body *ast.BlockStmt) (b bool) {
if l := len(body.List); l != 0 {
switch inner := body.List[l-1].(type) {
case *ast.BlockStmt:
return lastStmtIsReturn(inner)
case *ast.ReturnStmt:
return true
default:
return false
}
}
return false
}
// analyzePkgOrder sets the order in which packages should be processed.
// From Go spec:
// A package with no imports is initialized by assigning initial values to all its package-level variables
// followed by calling all init functions in the order they appear in the source, possibly in multiple files,
// as presented to the compiler. If a package has imports, the imported packages are initialized before
// initializing the package itself. If multiple packages import a package, the imported package
// will be initialized only once. The importing of packages, by construction, guarantees
// that there can be no cyclic initialization dependencies.
func (c *codegen) analyzePkgOrder() {
seen := make(map[string]bool)
info := c.buildInfo.program.Package(c.buildInfo.initialPackage)
c.visitPkg(info.Pkg, seen)
}
func (c *codegen) visitPkg(pkg *types.Package, seen map[string]bool) {
pkgPath := pkg.Path()
if seen[pkgPath] {
return
}
for _, imp := range pkg.Imports() {
c.visitPkg(imp, seen)
}
seen[pkgPath] = true
c.packages = append(c.packages, pkgPath)
}
func (c *codegen) fillDocumentInfo() {
fset := c.buildInfo.program.Fset
fset.Iterate(func(f *token.File) bool {
filePath := f.Position(f.Pos(0)).Filename
c.docIndex[filePath] = len(c.documents)
c.documents = append(c.documents, filePath)
return true
})
}
// analyzeFuncUsage traverses all code and returns map with functions
// which should be present in the emitted code.
// This is done using BFS starting from exported functions or
// function used in variable declarations (graph edge corresponds to
// function being called in declaration).
func (c *codegen) analyzeFuncUsage() funcUsage {
type declPair struct {
decl *ast.FuncDecl
importMap map[string]string
path string
}
// nodeCache contains top-level function declarations .
nodeCache := make(map[string]declPair)
diff := funcUsage{}
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
var pkgPath string
isMain := pkg == c.mainPkg.Pkg
if !isMain {
pkgPath = pkg.Path()
}
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.CallExpr:
// functions invoked in variable declarations in imported packages
// are marked as used.
var name string
switch t := n.Fun.(type) {
case *ast.Ident:
name = c.getIdentName(pkgPath, t.Name)
case *ast.SelectorExpr:
name, _ = c.getFuncNameFromSelector(t)
default:
return true
}
diff[name] = true
case *ast.FuncDecl:
name := c.getFuncNameFromDecl(pkgPath, n)
// exported functions are always assumed to be used
if isMain && n.Name.IsExported() || isInitFunc(n) || isDeployFunc(n) {
diff[name] = true
}
nodeCache[name] = declPair{n, c.importMap, pkgPath}
return false // will be processed in the next stage
}
return true
})
})
usage := funcUsage{}
for len(diff) != 0 {
nextDiff := funcUsage{}
for name := range diff {
fd, ok := nodeCache[name]
if !ok || usage[name] {
continue
}
usage[name] = true
old := c.importMap
c.importMap = fd.importMap
ast.Inspect(fd.decl, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.CallExpr:
switch t := n.Fun.(type) {
case *ast.Ident:
nextDiff[c.getIdentName(fd.path, t.Name)] = true
case *ast.SelectorExpr:
name, _ := c.getFuncNameFromSelector(t)
nextDiff[name] = true
}
}
return true
})
c.importMap = old
}
diff = nextDiff
}
return usage
}
func isGoBuiltin(name string) bool {
for i := range goBuiltins {
if name == goBuiltins[i] {
return true
}
}
return false
}
func isCustomBuiltin(f *funcScope) bool {
if !isInteropPath(f.pkg.Path()) {
return false
}
for _, n := range customBuiltins {
if f.name == n {
return true
}
}
return false
}
func isSyscall(fun *funcScope) bool {
if fun.selector == nil || fun.pkg == nil || !isInteropPath(fun.pkg.Path()) {
return false
}
return fun.pkg.Name() == "neogointernal" && (strings.HasPrefix(fun.name, "Syscall") ||
strings.HasPrefix(fun.name, "Opcode"))
}
const interopPrefix = "github.com/nspcc-dev/neo-go/pkg/interop"
func isInteropPath(s string) bool {
return strings.HasPrefix(s, interopPrefix)
}
// canConvert returns true if type doesn't need to be converted on type assertion.
func canConvert(s string) bool {
if len(s) != 0 && s[0] == '*' {
s = s[1:]
}
if isInteropPath(s) {
s = s[len(interopPrefix):]
return s != "/iterator.Iterator" && s != "/storage.Context" &&
s != "/native/ledger.Block" && s != "/native/ledger.Transaction" &&
s != "/native/management.Contract" && s != "/native/neo.AccountState"
}
return true
}
// canInline returns true if function is to be inlined.
// Currently there is a static list of function which are inlined,
// this may change in future.
func canInline(s string) bool {
if strings.HasPrefix(s, "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline") {
return true
}
if !isInteropPath(s) {
return false
}
return !strings.HasPrefix(s[len(interopPrefix):], "/neogointernal") &&
!strings.HasPrefix(s[len(interopPrefix):], "/util")
}