neo-go/pkg/compiler/analysis.go

398 lines
10 KiB
Go
Raw Normal View History

package compiler
import (
"errors"
"go/ast"
"go/token"
"go/types"
"path/filepath"
"strings"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"golang.org/x/tools/go/packages"
)
var (
// Go language builtin functions.
2020-09-06 12:49:41 +00:00
goBuiltins = []string{"len", "append", "panic", "make", "copy", "recover", "delete"}
// Custom builtin utility functions.
customBuiltins = []string{
"FromAddress",
}
)
// 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 {
2020-08-21 12:37:46 +00:00
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 {
2020-08-21 12:37:46 +00:00
switch n := node.(type) {
case *ast.FuncDecl:
hasDeploy = hasDeploy || isDeployFunc(n)
2020-08-21 12:37:46 +00:00
case *ast.DeferStmt:
hasDefer = true
return false
}
return true
})
}
})
2020-08-21 12:37:46 +00:00
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 *packages.Package) {
if n+nConst > 0 {
for _, f := range pkg.Syntax {
c.fillImportMap(f, pkg)
c.convertGlobals(f, pkg.Types)
}
}
for _, f := range pkg.Syntax {
c.fillImportMap(f, pkg)
var currMax int
lastCnt, currMax = c.convertInitFuncs(f, pkg.Types, 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,
}
2020-08-21 12:37:46 +00:00
}
}
// 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) {
2020-08-21 12:37:46 +00:00
// 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"
}
2019-10-22 14:56:03 +00:00
// 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[0]
c.visitPkg(info, seen)
}
func (c *codegen) visitPkg(pkg *packages.Package, seen map[string]bool) {
if seen[pkg.PkgPath] {
return
}
for _, imp := range pkg.Types.Imports() {
c.visitPkg(pkg.Imports[imp.Path()], seen)
}
seen[pkg.PkgPath] = true
c.packages = append(c.packages, pkg.PkgPath)
c.packageCache[pkg.PkgPath] = pkg
}
func (c *codegen) fillDocumentInfo() {
fset := c.buildInfo.config.Fset
fset.Iterate(func(f *token.File) bool {
filePath := f.Position(f.Pos(0)).Filename
compiler: use full paths for debug info files if relative can't be constructed Firstly introduced in 9871dc8f5aa33c17651848df637ed1185f43aa18. It's OK if relative path can't be constructed e.g. for interop dependencies. Otherwice leads to failing tests on Windows with the following error: ``` 2022-02-09T09:39:31.3307626Z panic: Rel: can't make D:\a\neo-go\neo-go\pkg\interop\neogointernal\syscall.go relative to C:\Users\RUNNER~1\AppData\Local\Temp\TestContractInitAndCompile1998984267\001\testcontract [recovered] 2022-02-09T09:39:31.3308830Z panic: Rel: can't make D:\a\neo-go\neo-go\pkg\interop\neogointernal\syscall.go relative to C:\Users\RUNNER~1\AppData\Local\Temp\TestContractInitAndCompile1998984267\001\testcontract 2022-02-09T09:39:31.3309285Z 2022-02-09T09:39:31.3309390Z goroutine 302 [running]: 2022-02-09T09:39:31.3309725Z testing.tRunner.func1.2({0x14dbfe0, 0xc00051bdb0}) 2022-02-09T09:39:31.3310187Z C:/hostedtoolcache/windows/go/1.17.6/x64/src/testing/testing.go:1209 +0x36c 2022-02-09T09:39:31.3310538Z testing.tRunner.func1() 2022-02-09T09:39:31.3310937Z C:/hostedtoolcache/windows/go/1.17.6/x64/src/testing/testing.go:1212 +0x3b6 2022-02-09T09:39:31.3311285Z panic({0x14dbfe0, 0xc00051bdb0}) 2022-02-09T09:39:31.3311689Z C:/hostedtoolcache/windows/go/1.17.6/x64/src/runtime/panic.go:1047 +0x266 2022-02-09T09:39:31.3312319Z github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).fillDocumentInfo.func1(0xc0002d0b40) 2022-02-09T09:39:31.3312885Z D:/a/neo-go/neo-go/pkg/compiler/analysis.go:240 +0x34d 2022-02-09T09:39:31.3313397Z go/token.(*FileSet).Iterate(0xc0000a69c0, 0xc0004898b0) 2022-02-09T09:39:31.3313839Z C:/hostedtoolcache/windows/go/1.17.6/x64/src/go/token/position.go:463 +0xde 2022-02-09T09:39:31.3314404Z github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).fillDocumentInfo(0xc0002e4d20) 2022-02-09T09:39:31.3314947Z D:/a/neo-go/neo-go/pkg/compiler/analysis.go:236 +0x97 2022-02-09T09:39:31.3315530Z github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).compile(0xc0002e4d20, 0xc00009a690, 0xc000101540) 2022-02-09T09:39:31.3316064Z D:/a/neo-go/neo-go/pkg/compiler/codegen.go:2088 +0x192 2022-02-09T09:39:31.3316563Z github.com/nspcc-dev/neo-go/pkg/compiler.codeGen(0xc00009a690) 2022-02-09T09:39:31.3317066Z D:/a/neo-go/neo-go/pkg/compiler/codegen.go:2167 +0x7cd 2022-02-09T09:39:31.3317679Z github.com/nspcc-dev/neo-go/pkg/compiler.CompileWithOptions({0xc0000435e0, 0x62}, {0x0, 0x0}, 0xc0004bcf70) 2022-02-09T09:39:31.3318233Z D:/a/neo-go/neo-go/pkg/compiler/compiler.go:225 +0xde 2022-02-09T09:39:31.3318804Z github.com/nspcc-dev/neo-go/pkg/compiler.CompileAndSave({0xc0000435e0, 0x62}, 0xc0004bcf70) 2022-02-09T09:39:31.3319353Z D:/a/neo-go/neo-go/pkg/compiler/compiler.go:241 +0x3a5 2022-02-09T09:39:31.3319889Z github.com/nspcc-dev/neo-go/cli/smartcontract.contractCompile(0xc0004e42c0) 2022-02-09T09:39:31.3320456Z D:/a/neo-go/neo-go/cli/smartcontract/smart_contract.go:520 +0xc2d 2022-02-09T09:39:31.3320873Z github.com/urfave/cli.HandleAction({0x14b6fa0, 0x17b8d60}, 0x7) 2022-02-09T09:39:31.3321344Z C:/Users/runneradmin/go/pkg/mod/github.com/urfave/cli@v1.22.5/app.go:524 +0xf4 2022-02-09T09:39:31.3321931Z github.com/urfave/cli.Command.Run({{0x16460d2, 0x7}, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x166fe9e, 0x27}, {0x0, ...}, ...}, ...) 2022-02-09T09:39:31.3322443Z C:/Users/runneradmin/go/pkg/mod/github.com/urfave/cli@v1.22.5/command.go:173 +0xc09 2022-02-09T09:39:31.3322894Z github.com/urfave/cli.(*App).RunAsSubcommand(0xc0000b7340, 0xc0004e4000) 2022-02-09T09:39:31.3323375Z C:/Users/runneradmin/go/pkg/mod/github.com/urfave/cli@v1.22.5/app.go:405 +0x106c 2022-02-09T09:39:31.3323852Z github.com/urfave/cli.Command.startApp({{0x1647536, 0x8}, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x1671070, 0x28}, {0x0, ...}, ...}, ...) 2022-02-09T09:39:31.3324365Z C:/Users/runneradmin/go/pkg/mod/github.com/urfave/cli@v1.22.5/command.go:372 +0x108c 2022-02-09T09:39:31.3324838Z github.com/urfave/cli.Command.Run({{0x1647536, 0x8}, {0x0, 0x0}, {0x0, 0x0, 0x0}, {0x1671070, 0x28}, {0x0, ...}, ...}, ...) 2022-02-09T09:39:31.3325327Z C:/Users/runneradmin/go/pkg/mod/github.com/urfave/cli@v1.22.5/command.go:102 +0xe8c 2022-02-09T09:39:31.3325744Z github.com/urfave/cli.(*App).Run(0xc000323880, {0xc0004237a0, 0xb, 0x12}) 2022-02-09T09:39:31.3326198Z C:/Users/runneradmin/go/pkg/mod/github.com/urfave/cli@v1.22.5/app.go:277 +0xa8c 2022-02-09T09:39:31.3326770Z github.com/nspcc-dev/neo-go/cli.(*executor).run(0xc0000709c0, {0xc0004237a0, 0xb, 0x12}) 2022-02-09T09:39:31.3327275Z D:/a/neo-go/neo-go/cli/executor_test.go:273 +0x3a5 2022-02-09T09:39:31.3327812Z github.com/nspcc-dev/neo-go/cli.(*executor).Run(0x14aa020, 0xc000493950, {0xc0004237a0, 0xb, 0x12}) 2022-02-09T09:39:31.3328324Z D:/a/neo-go/neo-go/cli/executor_test.go:263 +0x10f 2022-02-09T09:39:31.3328844Z github.com/nspcc-dev/neo-go/cli.TestContractInitAndCompile(0xc0001d69c0) 2022-02-09T09:39:31.3329377Z D:/a/neo-go/neo-go/cli/contract_test.go:155 +0x147f 2022-02-09T09:39:31.3329694Z testing.tRunner(0xc0001d69c0, 0x17b8be8) 2022-02-09T09:39:31.3330123Z C:/hostedtoolcache/windows/go/1.17.6/x64/src/testing/testing.go:1259 +0x230 2022-02-09T09:39:31.3330470Z created by testing.(*T).Run 2022-02-09T09:39:31.3330862Z C:/hostedtoolcache/windows/go/1.17.6/x64/src/testing/testing.go:1306 +0x727 2022-02-09T09:39:31.3331325Z FAIL github.com/nspcc-dev/neo-go/cli 5.552s ```
2022-02-09 15:17:47 +00:00
rel, err := filepath.Rel(c.buildInfo.config.Dir, filePath)
// It's OK if we can't construct relative path, e.g. for interop dependencies.
if err == nil {
filePath = rel
}
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.Types
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
pkg := c.mainPkg
if fd.path != "" {
pkg = c.packageCache[fd.path]
}
c.typeInfo = pkg.TypesInfo
c.currPkg = pkg
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
})
}
diff = nextDiff
}
return usage
}
func isGoBuiltin(name string) bool {
for i := range goBuiltins {
if name == goBuiltins[i] {
return true
}
}
return false
}
2018-04-22 18:11:37 +00:00
func isCustomBuiltin(f *funcScope) bool {
if !isInteropPath(f.pkg.Path()) {
return false
}
for _, n := range customBuiltins {
if f.name == n {
2018-04-22 18:11:37 +00:00
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") || strings.HasPrefix(fun.name, "CallWithToken"))
}
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
}
2021-02-04 12:41:00 +00:00
// 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, name 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") && name == "FromAddress")
2021-02-04 12:41:00 +00:00
}