0bc81aecf4
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>
388 lines
10 KiB
Go
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")
|
|
}
|