mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-26 19:42:23 +00:00
0de4c0fc17
Firstly introduced in 9871dc8f5a
. 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
```
397 lines
10 KiB
Go
397 lines
10 KiB
Go
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.
|
|
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 {
|
|
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 *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,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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[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
|
|
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
|
|
}
|
|
|
|
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") || 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
|
|
}
|
|
|
|
// 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")
|
|
}
|