neo-go/pkg/compiler/analysis.go
Evgeniy Stratonikov 7160675ac1 compiler/interop: improve conversion to bytes interops
Allow to convert to both `Buffer` and `ByteString`
and explicitly mention VM types. Rename `ToByteArray` to `ToBytes`
to avoid ambiguity.
2021-03-10 13:38:44 +03:00

335 lines
8.9 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"
)
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.
// and returns number of variables initialized.
// Second return value is -1 if no `init()` functions were encountered
// and number of maximum amount of locals in any of init functions otherwise.
// Same for `_deploy()` functions (see docs/compiler.md).
func (c *codegen) traverseGlobals() (int, int, int) {
var hasDefer bool
var n, nConst int
initLocals := -1
deployLocals := -1
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
nv, nc := countGlobals(f)
n += nv
nConst += nc
if initLocals == -1 || deployLocals == -1 || !hasDefer {
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.GenDecl:
if n.Tok == token.VAR {
for i := range n.Specs {
for _, v := range n.Specs[i].(*ast.ValueSpec).Values {
num := c.countLocalsCall(v, pkg)
if num > initLocals {
initLocals = num
}
}
}
}
case *ast.FuncDecl:
if isInitFunc(n) {
num, _ := c.countLocals(n)
if num > initLocals {
initLocals = num
}
} else if isDeployFunc(n) {
num, _ := c.countLocals(n)
if num > deployLocals {
deployLocals = num
}
}
return !hasDefer
case *ast.DeferStmt:
hasDefer = true
return false
}
return true
})
}
})
if hasDefer {
n++
}
if n+nConst != 0 || initLocals > -1 {
if n > 255 {
c.prog.BinWriter.Err = errors.New("too many global variables")
return 0, initLocals, deployLocals
}
if n != 0 {
emit.Instruction(c.prog.BinWriter, opcode.INITSSLOT, []byte{byte(n)})
}
if initLocals > 0 {
emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(initLocals), 0})
}
seenBefore := false
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)
}
}
if initLocals > -1 {
for _, f := range pkg.Files {
c.fillImportMap(f, pkg.Pkg)
seenBefore = c.convertInitFuncs(f, pkg.Pkg, seenBefore) || seenBefore
}
}
// 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
})
// store auxiliary variables after all others.
if hasDefer {
c.exceptionIndex = len(c.globals)
c.globals["<exception>"] = c.exceptionIndex
}
}
return n, initLocals, deployLocals
}
// 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
})
}
func (c *codegen) analyzeFuncUsage() funcUsage {
usage := funcUsage{}
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
isMain := pkg == c.mainPkg.Pkg
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.CallExpr:
switch t := n.Fun.(type) {
case *ast.Ident:
var pkgPath string
if !isMain {
pkgPath = pkg.Path()
}
usage[c.getIdentName(pkgPath, t.Name)] = true
case *ast.SelectorExpr:
name, _ := c.getFuncNameFromSelector(t)
usage[name] = true
}
case *ast.FuncDecl:
// exported functions are always assumed to be used
if isMain && n.Name.IsExported() {
usage[c.getFuncNameFromDecl("", n)] = true
}
}
return true
})
})
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)
}
func isNativeHelpersPath(s string) bool {
return strings.HasPrefix(s, interopPrefix+"/native")
}
// 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"
}
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")
}