Merge pull request #2624 from nspcc-dev/optimize-unused-globals

compiler: do not emit initialisation code for unused global vars
This commit is contained in:
Roman Khimov 2022-09-02 14:28:44 +03:00 committed by GitHub
commit f79672c4c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1156 additions and 62 deletions

View file

@ -49,11 +49,15 @@ func (c *codegen) getIdentName(pkg string, name string) string {
func (c *codegen) traverseGlobals() bool {
var hasDefer bool
var n, nConst int
var hasUnusedCall bool
var hasDeploy bool
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
nv, nc := countGlobals(f)
nv, nc, huc := countGlobals(f, !hasUnusedCall)
n += nv
nConst += nc
if huc {
hasUnusedCall = true
}
if !hasDeploy || !hasDefer {
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
@ -85,10 +89,10 @@ func (c *codegen) traverseGlobals() bool {
lastCnt, maxCnt := -1, -1
c.ForEachPackage(func(pkg *packages.Package) {
if n+nConst > 0 {
if n+nConst > 0 || hasUnusedCall {
for _, f := range pkg.Syntax {
c.fillImportMap(f, pkg)
c.convertGlobals(f, pkg.Types)
c.convertGlobals(f)
}
}
for _, f := range pkg.Syntax {
@ -143,26 +147,37 @@ func (c *codegen) traverseGlobals() bool {
// countGlobals counts the global variables in the program to add
// them with the stack size of the function.
// Second returned argument contains the amount of global constants.
func countGlobals(f ast.Node) (int, int) {
// If checkUnusedCalls set to true then unnamed global variables containing call
// will be searched for and their presence is returned as the last argument.
func countGlobals(f ast.Node, checkUnusedCalls bool) (int, int, bool) {
var numVar, numConst int
var hasUnusedCall bool
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.
// is a globally 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 != "_" {
valueSpec := s.(*ast.ValueSpec)
multiRet := len(valueSpec.Values) != 0 && len(valueSpec.Names) != len(valueSpec.Values) // e.g. var A, B = f() where func f() (int, int)
for j, id := range valueSpec.Names {
if id.Name != "_" { // If variable has name, then it's treated as used - that's countGlobals' caller responsibility to guarantee that.
if isVar {
numVar++
} else {
numConst++
}
} else if isVar && len(valueSpec.Values) != 0 && checkUnusedCalls && !hasUnusedCall {
indexToCheck := j
if multiRet {
indexToCheck = 0
}
hasUnusedCall = containsCall(valueSpec.Values[indexToCheck])
}
}
}
@ -171,7 +186,23 @@ func countGlobals(f ast.Node) (int, int) {
}
return true
})
return numVar, numConst
return numVar, numConst, hasUnusedCall
}
// containsCall traverses node and looks if it contains a function or method call.
func containsCall(n ast.Node) bool {
var hasCall bool
ast.Inspect(n, func(node ast.Node) bool {
switch node.(type) {
case *ast.CallExpr:
hasCall = true
case *ast.Ident:
// Can safely skip idents immediately, we're interested at function calls only.
return false
}
return !hasCall
})
return hasCall
}
// isExprNil looks if the given expression is a `nil`.
@ -250,21 +281,45 @@ func (c *codegen) fillDocumentInfo() {
})
}
// analyzeFuncUsage traverses all code and returns a map with functions
// analyzeFuncAndGlobalVarUsage traverses all code and returns a map with functions
// which should be present in the emitted code.
// This is done using BFS starting from exported functions or
// the function used in variable declarations (graph edge corresponds to
// the function being called in declaration).
func (c *codegen) analyzeFuncUsage() funcUsage {
// the function being called in declaration). It also analyzes global variables
// usage preserving the same traversal strategy and rules. Unused global variables
// are renamed to "_" in the end. Global variable is treated as "used" iff:
// 1. It belongs either to main or to exported package AND is used directly from the exported (or _init\_deploy) method of the main package.
// 2. It belongs either to main or to exported package AND is used non-directly from the exported (or _init\_deploy) method of the main package
// (e.g. via series of function calls or in some expression that is "used").
// 3. It belongs either to main or to exported package AND contains function call inside its value definition.
func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
type declPair struct {
decl *ast.FuncDecl
importMap map[string]string
path string
}
// nodeCache contains top-level function declarations .
// globalVar represents a global variable declaration node with the corresponding package context.
type globalVar struct {
decl *ast.GenDecl // decl contains global variables declaration node (there can be multiple declarations in a single node).
specIdx int // specIdx is the index of variable specification in the list of GenDecl specifications.
varIdx int // varIdx is the index of variable name in the specification names.
ident *ast.Ident // ident is a named global variable identifier got from the specified node.
importMap map[string]string
path string
}
// nodeCache contains top-level function declarations.
nodeCache := make(map[string]declPair)
// globalVarsCache contains both used and unused declared named global vars.
globalVarsCache := make(map[string]globalVar)
// diff contains used functions that are not yet marked as "used" and those definition
// requires traversal in the subsequent stages.
diff := funcUsage{}
// globalVarsDiff contains used named global variables that are not yet marked as "used"
// and those declaration requires traversal in the subsequent stages.
globalVarsDiff := funcUsage{}
// usedExpressions contains a set of ast.Nodes that are used in the program and need to be evaluated
// (either they are used from the used functions OR belong to global variable declaration and surrounded by a function call)
var usedExpressions []nodeContext
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
var pkgPath string
isMain := pkg == c.mainPkg.Types
@ -316,6 +371,44 @@ func (c *codegen) analyzeFuncUsage() funcUsage {
}
nodeCache[name] = declPair{n, c.importMap, pkgPath}
return false // will be processed in the next stage
case *ast.GenDecl:
// After skipping all funcDecls, we are sure that each value spec
// is a globally declared variable or constant. We need to gather global
// vars from both main and imported packages.
if n.Tok == token.VAR {
for i, s := range n.Specs {
valSpec := s.(*ast.ValueSpec)
for j, id := range valSpec.Names {
if id.Name != "_" {
name := c.getIdentName(pkgPath, id.Name)
globalVarsCache[name] = globalVar{
decl: n,
specIdx: i,
varIdx: j,
ident: id,
importMap: c.importMap,
path: pkgPath,
}
}
// Traverse both named/unnamed global variables, check whether function/method call
// is present inside variable value and if so, mark all its children as "used" for
// further traversal and evaluation.
if len(valSpec.Values) == 0 {
continue
}
multiRet := len(valSpec.Values) != len(valSpec.Names)
if (j == 0 || !multiRet) && containsCall(valSpec.Values[j]) {
usedExpressions = append(usedExpressions, nodeContext{
node: valSpec.Values[j],
path: pkgPath,
importMap: c.importMap,
typeInfo: c.typeInfo,
currPkg: c.currPkg,
})
}
}
}
}
}
return true
})
@ -324,9 +417,24 @@ func (c *codegen) analyzeFuncUsage() funcUsage {
return nil
}
// Handle nodes that contain (or surrounded by) function calls and are a part
// of global variable declaration.
c.pickVarsFromNodes(usedExpressions, func(name string) {
if _, gOK := globalVarsCache[name]; gOK {
globalVarsDiff[name] = true
}
})
// Traverse the set of upper-layered used functions and construct the functions' usage map.
// At the same time, go through the whole set of used functions and mark global vars used
// from these functions as "used". Also mark the global variables from the previous step
// and their children as "used".
usage := funcUsage{}
for len(diff) != 0 {
globalVarsUsage := funcUsage{}
for len(diff) != 0 || len(globalVarsDiff) != 0 {
nextDiff := funcUsage{}
nextGlobalVarsDiff := funcUsage{}
usedExpressions = usedExpressions[:0]
for name := range diff {
fd, ok := nodeCache[name]
if !ok || usage[name] {
@ -354,12 +462,157 @@ func (c *codegen) analyzeFuncUsage() funcUsage {
}
return true
})
usedExpressions = append(usedExpressions, nodeContext{
node: fd.decl.Body,
path: fd.path,
importMap: c.importMap,
typeInfo: c.typeInfo,
currPkg: c.currPkg,
})
}
// Traverse used global vars in a separate cycle so that we're sure there's no other unrelated vars.
// Mark their children as "used".
for name := range globalVarsDiff {
fd, ok := globalVarsCache[name]
if !ok || globalVarsUsage[name] {
continue
}
globalVarsUsage[name] = true
pkg := c.mainPkg
if fd.path != "" {
pkg = c.packageCache[fd.path]
}
valSpec := fd.decl.Specs[fd.specIdx].(*ast.ValueSpec)
if len(valSpec.Values) == 0 {
continue
}
multiRet := len(valSpec.Values) != len(valSpec.Names)
if fd.varIdx == 0 || !multiRet {
usedExpressions = append(usedExpressions, nodeContext{
node: valSpec.Values[fd.varIdx],
path: fd.path,
importMap: fd.importMap,
typeInfo: pkg.TypesInfo,
currPkg: pkg,
})
}
}
c.pickVarsFromNodes(usedExpressions, func(name string) {
if _, gOK := globalVarsCache[name]; gOK {
nextGlobalVarsDiff[name] = true
}
})
diff = nextDiff
globalVarsDiff = nextGlobalVarsDiff
}
// Tiny hack: rename all remaining unused global vars. After that these unused
// vars will be handled as any other unnamed unused variables, i.e.
// c.traverseGlobals() won't take them into account during static slot creation
// and the code won't be emitted for them.
for name, node := range globalVarsCache {
if _, ok := globalVarsUsage[name]; !ok {
node.ident.Name = "_"
}
}
return usage
}
// nodeContext contains ast node with the corresponding import map, type info and package information
// required to retrieve fully qualified node name (if so).
type nodeContext struct {
node ast.Node
path string
importMap map[string]string
typeInfo *types.Info
currPkg *packages.Package
}
// derive returns provided node with the parent's context.
func (c nodeContext) derive(n ast.Node) nodeContext {
return nodeContext{
node: n,
path: c.path,
importMap: c.importMap,
typeInfo: c.typeInfo,
currPkg: c.currPkg,
}
}
// pickVarsFromNodes searches for variables used in the given set of nodes
// calling markAsUsed for each variable. Be careful while using codegen after
// pickVarsFromNodes, it changes importMap, currPkg and typeInfo.
func (c *codegen) pickVarsFromNodes(nodes []nodeContext, markAsUsed func(name string)) {
for len(nodes) != 0 {
var nextExprToCheck []nodeContext
for _, val := range nodes {
// Set variable context for proper name extraction.
c.importMap = val.importMap
c.currPkg = val.currPkg
c.typeInfo = val.typeInfo
ast.Inspect(val.node, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.KeyValueExpr: // var _ = f() + CustomInt{Int: Unused}.Int + 3 => mark Unused as "used".
nextExprToCheck = append(nextExprToCheck, val.derive(n.Value))
return false
case *ast.CallExpr:
switch t := n.Fun.(type) {
case *ast.Ident:
// Do nothing, used functions are handled in a separate cycle.
case *ast.SelectorExpr:
nextExprToCheck = append(nextExprToCheck, val.derive(t))
}
for _, arg := range n.Args {
switch arg.(type) {
case *ast.BasicLit:
default:
nextExprToCheck = append(nextExprToCheck, val.derive(arg))
}
}
return false
case *ast.SelectorExpr:
if c.typeInfo.Selections[n] != nil {
switch t := n.X.(type) {
case *ast.Ident:
nextExprToCheck = append(nextExprToCheck, val.derive(t))
case *ast.CompositeLit:
nextExprToCheck = append(nextExprToCheck, val.derive(t))
case *ast.SelectorExpr: // imp_pkg.Anna.GetAge() => mark Anna (exported global struct) as used.
nextExprToCheck = append(nextExprToCheck, val.derive(t))
}
} else {
ident := n.X.(*ast.Ident)
name := c.getIdentName(ident.Name, n.Sel.Name)
markAsUsed(name)
}
return false
case *ast.CompositeLit: // var _ = f(1) + []int{1, Unused, 3}[1] => mark Unused as "used".
for _, e := range n.Elts {
switch e.(type) {
case *ast.BasicLit:
default:
nextExprToCheck = append(nextExprToCheck, val.derive(e))
}
}
return false
case *ast.Ident:
name := c.getIdentName(val.path, n.Name)
markAsUsed(name)
return false
case *ast.DeferStmt:
nextExprToCheck = append(nextExprToCheck, val.derive(n.Call.Fun))
return false
case *ast.BasicLit:
return false
}
return true
})
}
nodes = nextExprToCheck
}
}
func isGoBuiltin(name string) bool {
for i := range goBuiltins {
if name == goBuiltins[i] {

View file

@ -350,7 +350,7 @@ func (c *codegen) emitDefault(t types.Type) {
// convertGlobals traverses the AST and only converts global declarations.
// If we call this in convertFuncDecl, it will load all global variables
// into the scope of the function.
func (c *codegen) convertGlobals(f *ast.File, _ *types.Package) {
func (c *codegen) convertGlobals(f *ast.File) {
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.FuncDecl:
@ -596,15 +596,32 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
}
}
}
for i := range t.Names {
if len(t.Values) != 0 {
if i == 0 || !multiRet {
ast.Walk(c, t.Values[i])
for i, id := range t.Names {
if id.Name != "_" {
if len(t.Values) != 0 {
if i == 0 || !multiRet {
ast.Walk(c, t.Values[i])
}
} else {
c.emitDefault(c.typeOf(t.Type))
}
} else {
c.emitDefault(c.typeOf(t.Type))
c.emitStoreVar("", t.Names[i].Name)
continue
}
// If var decl contains call then the code should be emitted for it, otherwise - do not evaluate.
if len(t.Values) == 0 {
continue
}
var hasCall bool
if i == 0 || !multiRet {
hasCall = containsCall(t.Values[i])
}
if hasCall {
ast.Walk(c, t.Values[i])
}
if hasCall || i != 0 && multiRet {
c.emitStoreVar("", "_") // drop unused after walk
}
c.emitStoreVar("", t.Names[i].Name)
}
}
}
@ -2119,7 +2136,7 @@ func (c *codegen) compile(info *buildInfo, pkg *packages.Package) error {
c.mainPkg = pkg
c.analyzePkgOrder()
c.fillDocumentInfo()
funUsage := c.analyzeFuncUsage()
funUsage := c.analyzeFuncAndGlobalVarUsage()
if c.prog.Err != nil {
return c.prog.Err
}

View file

@ -9,34 +9,652 @@ import (
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/require"
)
func TestUnusedGlobal(t *testing.T) {
src := `package foo
const (
_ int = iota
a
)
func Main() int {
return 1
}`
eval(t, src, big.NewInt(1))
t.Run("simple unused", func(t *testing.T) {
src := `package foo
const (
_ int = iota
a
)
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // PUSH1 + RET
})
t.Run("unused with function call inside", func(t *testing.T) {
t.Run("specification names count matches values count", func(t *testing.T) {
src := `package foo
var control int
var _ = f()
func Main() int {
return control
}
func f() int {
control = 1
return 5
}`
eval(t, src, big.NewInt(1))
})
t.Run("specification names count differs from values count", func(t *testing.T) {
src := `package foo
var control int
var _, _ = f()
func Main() int {
return control
}
func f() (int, int) {
control = 1
return 5, 6
}`
eval(t, src, big.NewInt(1))
})
t.Run("used", func(t *testing.T) {
src := `package foo
var _, A = f()
func Main() int {
return A
}
func f() (int, int) {
return 5, 6
}`
eval(t, src, big.NewInt(6))
checkInstrCount(t, src, 1, 1, 0, 0) // sslot for A, single call to f
})
})
t.Run("unused without function call", func(t *testing.T) {
src := `package foo
var _ = 1
var (
_ = 2 + 3
_, _ = 3 + 4, 5
)
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // PUSH1 + RET
})
}
func TestUnusedOptimizedGlobalVar(t *testing.T) {
t.Run("unused, no initialization", func(t *testing.T) {
src := `package foo
var A int
var (
B int
C, D, E int
)
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("used, no initialization", func(t *testing.T) {
src := `package foo
var A int
func Main() int {
return A
}`
eval(t, src, big.NewInt(0))
checkInstrCount(t, src, 1, 0, 0, 0) // sslot for A
})
t.Run("used by unused var, no initialization", func(t *testing.T) {
src := `package foo
var Unused int
var Unused2 = Unused + 1
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("unused, with initialization", func(t *testing.T) {
src := `package foo
var Unused = 1
func Main() int {
return 2
}`
prog := eval(t, src, big.NewInt(2))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("unused, with initialization by used var", func(t *testing.T) {
src := `package foo
var (
A = 1
B, Unused, C = f(), A + 2, 3 // the code for Unused initialization won't be emitted as it's a pure expression without function calls
Unused2 = 4
)
var Unused3 = 5
func Main() int {
return A + C
}
func f() int {
return 4
}`
eval(t, src, big.NewInt(4), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A and C
opcode.PUSH1, opcode.STSFLD0, // store A
[]interface{}{opcode.CALL, []byte{10}}, opcode.DROP, // evaluate B and drop
opcode.PUSH3, opcode.STSFLD1, opcode.RET, // store C
opcode.LDSFLD0, opcode.LDSFLD1, opcode.ADD, opcode.RET, // Main
opcode.PUSH4, opcode.RET) // f
})
t.Run("used by unused var, with initialization", func(t *testing.T) {
src := `package foo
var (
Unused1 = 1
Unused2 = Unused1 + 1
)
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("used with combination of nested unused", func(t *testing.T) {
src := `package foo
var (
A = 1
Unused1 = 2
Unused2 = Unused1 + 1
)
func Main() int {
return A
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, opcode.RET, // store A
opcode.LDSFLD0, opcode.RET) // Main
})
t.Run("single var stmt with both used and unused vars", func(t *testing.T) {
src := `package foo
var A, Unused1, B, Unused2 = 1, 2, 3, 4
func Main() int {
return A + B
}`
eval(t, src, big.NewInt(4), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A and B
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.PUSH3, opcode.STSFLD1, opcode.RET, // store B
opcode.LDSFLD0, opcode.LDSFLD1, opcode.ADD, opcode.RET) // Main
})
t.Run("single var decl token with multiple var specifications", func(t *testing.T) {
src := `package foo
var (
A, Unused1, B, Unused2 = 1, 2, 3, 4
C, Unused3 int
)
func Main() int {
return A + B + C
}`
eval(t, src, big.NewInt(4), []interface{}{opcode.INITSSLOT, []byte{3}}, // sslot for A, B, C
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.PUSH3, opcode.STSFLD1, // store B
opcode.PUSH0, opcode.STSFLD2, opcode.RET, // store C
opcode.LDSFLD0, opcode.LDSFLD1, opcode.ADD, opcode.LDSFLD2, opcode.ADD, opcode.RET) // Main
})
t.Run("function as unused var value", func(t *testing.T) {
src := `package foo
var A, Unused1 = 1, f()
func Main() int {
return A
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, // store A
[]interface{}{opcode.CALL, []byte{6}}, opcode.DROP, opcode.RET, // evaluate Unused1 (call to f) and drop its value
opcode.LDSFLD0, opcode.RET, // Main
opcode.PUSH2, opcode.RET) // f
})
t.Run("function as unused struct field", func(t *testing.T) {
src := `package foo
type Str struct { Int int }
var _ = Str{Int: f()}
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.CALL, []byte{8}}, opcode.PUSH1, opcode.PACKSTRUCT, opcode.DROP, opcode.RET, // evaluate struct val
opcode.PUSH1, opcode.RET, // Main
opcode.PUSH2, opcode.RET) // f
})
t.Run("used in unused function", func(t *testing.T) {
src := `package foo
var Unused1, Unused2, Unused3 = 1, 2, 3
func Main() int {
return 1
}
func unused1() int {
return Unused1
}
func unused2() int {
return Unused1 + unused1()
}
func unused3() int {
return Unused2 + unused2()
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("used in used function", func(t *testing.T) {
src := `package foo
var A = 1
func Main() int {
return f()
}
func f() int {
return A
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 1, 1, 0, 0)
})
t.Run("unused, initialized via init", func(t *testing.T) {
src := `package foo
var A int
func Main() int {
return 2
}
func init() {
A = 1 // Although A is unused from exported functions, it's used from init(), so it should be mark as "used" and stored.
}`
eval(t, src, big.NewInt(2))
checkInstrCount(t, src, 1, 0, 0, 0)
})
t.Run("used, initialized via init", func(t *testing.T) {
src := `package foo
var A int
func Main() int {
return A
}
func init() {
A = 1
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 1, 0, 0, 0)
})
t.Run("unused, initialized by function call", func(t *testing.T) {
t.Run("unnamed", func(t *testing.T) {
src := `package foo
var _ = f()
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 0, 1, 0, 0)
})
t.Run("named", func(t *testing.T) {
src := `package foo
var A = f()
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 0, 1, 0, 0)
})
t.Run("named, with dependency on unused var", func(t *testing.T) {
src := `package foo
var (
A = 1
B = A + 1 // To check nested ident values.
C = 3
D = B + f() + C // To check that both idents (before and after the call to f) will be marked as "used".
E = C + 1 // Unused, no code expected.
)
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{3}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.LDSFLD0, opcode.PUSH1, opcode.ADD, opcode.STSFLD1, // store B
opcode.PUSH3, opcode.STSFLD2, // store C
opcode.LDSFLD1, []interface{}{opcode.CALL, []byte{9}}, opcode.ADD, opcode.LDSFLD2, opcode.ADD, opcode.DROP, opcode.RET, // evaluate D and drop
opcode.PUSH1, opcode.RET, // Main
opcode.PUSH2, opcode.RET) // f
})
t.Run("named, with dependency on unused var ident inside function call", func(t *testing.T) {
src := `package foo
var A = 1
var B = f(A)
func Main() int {
return 1
}
func f(a int) int {
return a
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.LDSFLD0, []interface{}{opcode.CALL, []byte{6}}, opcode.DROP, opcode.RET, // evaluate B and drop
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.RET) // f
})
t.Run("named, inside multi-specs and multi-vals var declaration", func(t *testing.T) {
src := `package foo
var (
Unused = 1
Unused1, A, Unused2 = 2, 3 + f(), 4
)
func Main() int {
return 1
}
func f() int {
return 5
}`
eval(t, src, big.NewInt(1), opcode.PUSH3, []interface{}{opcode.CALL, []byte{7}}, opcode.ADD, opcode.DROP, opcode.RET, // evaluate and drop A
opcode.PUSH1, opcode.RET, // Main
opcode.PUSH5, opcode.RET) // f
})
t.Run("unnamed + unused", func(t *testing.T) {
src := `package foo
var A = 1 // At least one global variable is used, thus, the whole set of package variables will be walked.
var B = 2
var _ = B + 1 // This variable is unnamed and doesn't contain call, thus its children won't be marked as "used".
func Main() int {
return A
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, opcode.RET, // store A
opcode.LDSFLD0, opcode.RET) // Main
})
t.Run("mixed value", func(t *testing.T) {
src := `package foo
var control int // At least one global variable is used, thus the whole set of package variables will be walked.
var B = 2
var _ = 1 + f() + B // This variable is unnamed but contains call, thus its children will be marked as "used".
func Main() int {
return control
}
func f() int {
control = 1
return 3
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 2 /* control + B */, 1, 0, 0)
})
t.Run("multiple function return values", func(t *testing.T) {
src := `package foo
var A, B = f()
func Main() int {
return A
}
func f() (int, int) {
return 3, 4
}`
eval(t, src, big.NewInt(3), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
[]interface{}{opcode.CALL, []byte{7}}, opcode.STSFLD0, opcode.DROP, opcode.RET, // evaluate and store A, drop B
opcode.LDSFLD0, opcode.RET, // Main
opcode.PUSH4, opcode.PUSH3, opcode.RET) // f
})
t.Run("constant in declaration", func(t *testing.T) {
src := `package foo
const A = 5
var Unused = 1 + A
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("mixed expression", func(t *testing.T) {
src := `package foo
type CustomInt struct {
Int int
}
var A = CustomInt{Int: 2}
var B = f(3) + A.f(1)
func Main() int {
return 1
}
func f(a int) int {
return a
}
func (i CustomInt) f(a int) int { // has the same name as f
return i.Int + a
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 1 /* A */, 2, 2, 0)
})
})
t.Run("mixed nested expressions", func(t *testing.T) {
src := `package foo
type CustomInt struct { Int int} // has the same field name as Int variable, important for test
var A = CustomInt{Int: 2}
var B = f(A.Int)
var Unused = 4
var Int = 5 // unused and MUST NOT be treated as "used"
var C = CustomInt{Int: Unused}.Int + f(1) // uses Unused => Unused should be marked as "used"
func Main() int {
return 1
}
func f(a int) int {
return a
}
func (i CustomInt) f(a int) int { // has the same name as f
return i.Int + a
}`
eval(t, src, big.NewInt(1))
})
t.Run("composite literal", func(t *testing.T) {
src := `package foo
var A = 2
var B = []int{1, A, 3}[1]
var C = f(1) + B
func Main() int {
return 1
}
func f(a int) int {
return a
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A, B
opcode.PUSH2, opcode.STSFLD0, // store A
opcode.PUSH3, opcode.LDSFLD0, opcode.PUSH1, opcode.PUSH3, opcode.PACK, opcode.PUSH1, opcode.PICKITEM, opcode.STSFLD1, // evaluate B
opcode.PUSH1, []interface{}{opcode.CALL, []byte{8}}, opcode.LDSFLD1, opcode.ADD, opcode.DROP, opcode.RET, // evalute C and drop
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.RET) // f
})
t.Run("index expression", func(t *testing.T) {
src := `package foo
var Unused = 2
var A = f(1) + []int{1, 2, 3}[Unused] // index expression
func Main() int {
return 1
}
func f(a int) int {
return a
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for Unused
opcode.PUSH2, opcode.STSFLD0, // store Unused
opcode.PUSH1, []interface{}{opcode.CALL, []byte{14}}, // call f(1)
opcode.PUSH3, opcode.PUSH2, opcode.PUSH1, opcode.PUSH3, opcode.PACK, opcode.LDSFLD0, opcode.PICKITEM, // eval index expression
opcode.ADD, opcode.DROP, opcode.RET, // eval and drop A
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.RET) // f(a)
})
t.Run("used via nested function calls", func(t *testing.T) {
src := `package foo
var A = 1
func Main() int {
return f()
}
func f() int {
return g()
}
func g() int {
return A
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, opcode.RET, // store A
[]interface{}{opcode.CALL, []byte{3}}, opcode.RET, // Main
[]interface{}{opcode.CALL, []byte{3}}, opcode.RET, // f
opcode.LDSFLD0, opcode.RET) // g
})
t.Run("struct field name matches global var name", func(t *testing.T) {
src := `package foo
type CustomStr struct { Int int }
var str = CustomStr{Int: 2}
var Int = 5 // Unused and the code must not be emitted.
func Main() int {
return str.Int
}`
eval(t, src, big.NewInt(2), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for str
opcode.PUSH2, opcode.PUSH1, opcode.PACKSTRUCT, opcode.STSFLD0, opcode.RET, // store str
opcode.LDSFLD0, opcode.PUSH0, opcode.PICKITEM, opcode.RET) // Main
})
t.Run("var as a struct field initializer", func(t *testing.T) {
src := `package foo
type CustomStr struct { Int int }
var A = 5
var Int = 6 // Unused
func Main() int {
return CustomStr{Int: A}.Int
}`
eval(t, src, big.NewInt(5))
})
t.Run("argument of globally called function", func(t *testing.T) {
src := `package foo
var Unused = 5
var control int
var _, A = f(Unused)
func Main() int {
return control
}
func f(int) (int, int) {
control = 5
return 1, 2
}`
eval(t, src, big.NewInt(5))
})
t.Run("argument of locally called function", func(t *testing.T) {
src := `package foo
var Unused = 5
func Main() int {
var _, a = f(Unused)
return a
}
func f(i int) (int, int) {
return i, i
}`
eval(t, src, big.NewInt(5))
})
t.Run("used in globally called defer", func(t *testing.T) {
src := `package foo
var control1, control2 int
var Unused = 5
var _ = f()
func Main() int {
return control1 + control2
}
func f() int {
control1 = 1
defer func(){
control2 = Unused
}()
return 2
}`
eval(t, src, big.NewInt(6))
})
t.Run("used in locally called defer", func(t *testing.T) {
src := `package foo
var control1, control2 int
var Unused = 5
func Main() int {
_ = f()
return control1 + control2
}
func f() int {
control1 = 1
defer func(){
control2 = Unused
}()
return 2
}`
eval(t, src, big.NewInt(6))
})
t.Run("imported", func(t *testing.T) {
t.Run("init by func call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar"
func Main() int {
return globalvar.Default
}`
eval(t, src, big.NewInt(0))
checkInstrCount(t, src, 1 /* Default */, 1 /* f */, 0, 0)
})
t.Run("nested var call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested1"
func Main() int {
return nested1.C
}`
eval(t, src, big.NewInt(81))
checkInstrCount(t, src, 6 /* dependant vars of nested1.C */, 3, 1, 1)
})
t.Run("nested func call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/funccall"
func Main() int {
return funccall.F()
}`
eval(t, src, big.NewInt(56))
checkInstrCount(t, src, 2 /* nested2.Argument + nested1.Argument */, -1, -1, -1)
})
t.Run("nested method call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/funccall"
func Main() int {
return funccall.GetAge()
}`
eval(t, src, big.NewInt(24))
checkInstrCount(t, src, 3, /* nested3.Anna + nested2.Argument + nested3.Argument */
5, /* funccall.GetAge() + Anna.GetAge() + nested1.f + nested1.f + nested2.f */
2 /* nested1.f + nested2.f */, 0)
})
})
}
func TestChangeGlobal(t *testing.T) {
src := `package foo
var a int
func Main() int {
setLocal()
set42()
setLocal()
return a
}
func set42() { a = 42 }
func setLocal() { a := 10; _ = a }`
eval(t, src, big.NewInt(42))
t.Run("from Main", func(t *testing.T) {
src := `package foo
var a int
func Main() int {
setLocal()
set42()
setLocal()
return a
}
func set42() { a = 42 }
func setLocal() { a := 10; _ = a }`
eval(t, src, big.NewInt(42))
})
t.Run("from other global", func(t *testing.T) {
t.Skip("see https://github.com/nspcc-dev/neo-go/issues/2661")
src := `package foo
var A = f()
var B int
func Main() int {
return B
}
func f() int {
B = 3
return B
}`
eval(t, src, big.NewInt(3))
})
}
func TestMultiDeclaration(t *testing.T) {
@ -293,3 +911,29 @@ func TestUnderscoreVarsDontUseSlots(t *testing.T) {
src := buf.String()
eval(t, src, big.NewInt(count))
}
func TestUnderscoreGlobalVarDontEmitCode(t *testing.T) {
src := `package foo
var _ int
var _ = 1
var (
A = 2
_ = A + 3
_, B, _ = 4, 5, 6
_, C, _ = f(A, B)
)
var D = 7 // named unused, after global codegen optimisation no code expected
func Main() int {
return 1
}
func f(a, b int) (int, int, int) {
return 8, 9, 10
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A, B
opcode.PUSH2, opcode.STSFLD0, // store A
opcode.PUSH5, opcode.STSFLD1, // store B
opcode.LDSFLD0, opcode.LDSFLD1, opcode.SWAP, []interface{}{opcode.CALL, []byte{8}}, // evaluate f(A,B)
opcode.DROP, opcode.DROP, opcode.DROP, opcode.RET, // drop result of f(A,B)
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 2}}, opcode.PUSH10, opcode.PUSH9, opcode.PUSH8, opcode.RET) // f
}

View file

@ -100,7 +100,7 @@ func TestInitWithNoGlobals(t *testing.T) {
func Main() int {
return 42
}`
v, s := vmAndCompileInterop(t, src)
v, s, _ := vmAndCompileInterop(t, src)
require.NoError(t, v.Run())
assertResult(t, v, big.NewInt(42))
require.True(t, len(s.events) == 1)

View file

@ -3,6 +3,7 @@ package compiler_test
import (
"fmt"
"math/big"
"os"
"strings"
"testing"
@ -12,8 +13,12 @@ import (
)
func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, expectedLocalsMain int) {
v, sp := vmAndCompileInterop(t, src)
checkInstrCount(t, src, -1, expectedCall, expectedInitSlot, expectedLocalsMain)
}
func checkInstrCount(t *testing.T, src string, expectedSSlotCount, expectedCall, expectedInitSlot, expectedLocalsMain int) {
v, sp, _ := vmAndCompileInterop(t, src)
v.PrintOps(os.Stdout)
mainStart := -1
for _, m := range sp.info.Methods {
if m.Name.Name == "main" {
@ -29,6 +34,14 @@ func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, ex
for op, param, err := ctx.Next(); ; op, param, err = ctx.Next() {
require.NoError(t, err)
switch op {
case opcode.INITSSLOT:
if expectedSSlotCount == -1 {
continue
}
if expectedSSlotCount == 0 {
t.Fatalf("no INITSSLOT expected, found at %d with %d cells", ctx.IP(), param[0])
}
require.Equal(t, expectedSSlotCount, int(param[0]))
case opcode.CALL, opcode.CALLL:
actualCall++
case opcode.INITSLOT:
@ -41,8 +54,12 @@ func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, ex
break
}
}
require.Equal(t, expectedCall, actualCall)
require.True(t, expectedInitSlot == actualInitSlot)
if expectedCall != -1 {
require.Equal(t, expectedCall, actualCall)
}
if expectedInitSlot != -1 {
require.Equal(t, expectedInitSlot, actualInitSlot)
}
}
func TestInline(t *testing.T) {
@ -55,13 +72,13 @@ func TestInline(t *testing.T) {
a int
b pair
}
// local alias
func sum(a, b int) int {
return 42
}
var Num = 1
func Main() int {
%s
}
// local alias
func sum(a, b int) int {
return 42
}`
t.Run("no return", func(t *testing.T) {
src := fmt.Sprintf(srcTmpl, `inline.NoArgsNoReturn()

View file

@ -193,7 +193,7 @@ func TestNotify(t *testing.T) {
runtime.Notify("single")
}`
v, s := vmAndCompileInterop(t, src)
v, s, _ := vmAndCompileInterop(t, src)
v.Estack().PushVal(11)
require.NoError(t, v.Run())
@ -224,7 +224,7 @@ func TestSyscallInGlobalInit(t *testing.T) {
func Main() bool {
return a
}`
v, s := vmAndCompileInterop(t, src)
v, s, _ := vmAndCompileInterop(t, src)
s.interops[interopnames.ToID([]byte(interopnames.SystemRuntimeCheckWitness))] = func(v *vm.VM) error {
s := v.Estack().Pop().Value().([]byte)
require.Equal(t, "5T", string(s))
@ -479,7 +479,7 @@ func TestInteropTypesComparison(t *testing.T) {
b := struct{}{}
return a.Equals(b)
}`
vm, _ := vmAndCompileInterop(t, src)
vm, _, _ := vmAndCompileInterop(t, src)
err := vm.Run()
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "invalid conversion: Struct/ByteString"), err)

View file

@ -5,7 +5,7 @@ func NewBar() int {
return 10
}
// Dummy is dummy constant.
// Dummy is dummy variable.
var Dummy = 1
// Foo is a type.

View file

@ -0,0 +1,18 @@
package funccall
import (
"github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested1"
"github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested2"
alias "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested3"
)
// F should be called from the main package to check usage analyzer against
// nested constructions handling.
func F() int {
return nested1.F(nested2.Argument + alias.Argument)
}
// GetAge calls method on the global struct.
func GetAge() int {
return alias.Anna.GetAge()
}

14
pkg/compiler/testdata/globalvar/main.go vendored Normal file
View file

@ -0,0 +1,14 @@
package globalvar
// Unused shouldn't produce any initialization code if it's not used anywhere.
var Unused = 3
// Default is initialized by default value.
var Default int
// A initialized by function call, thus the initialization code should always be emitted.
var A = f()
func f() int {
return 5
}

View file

@ -0,0 +1,29 @@
package nested1
import (
"github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested2"
alias "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested3"
)
// Unused shouldn't produce any code if unused.
var Unused = 11
// A should produce call to f and should not be DROPped if C is used. It uses
// aliased package var as an argument to check analizator.
var A = f(alias.Argument)
// B should produce call to f and be DROPped if unused. It uses foreign package var as an argument
// to check analizator.
var B = f(nested2.Argument)
// C shouldn't produce any code if unused. It uses
var C = A + nested2.A + nested2.Unique
func f(i int) int {
return i
}
// F is used for nested calls check.
func F(i int) int {
return i
}

View file

@ -0,0 +1,20 @@
package nested2
// Unused shouldn't produce any code if unused.
var Unused = 21
// Argument is an argument used from external package to call nested1.f.
var Argument = 22
// A has the same name as nested1.A.
var A = 23
// B should produce call to f and be DROPped if unused.
var B = f()
// Unique has unique name.
var Unique = 24
func f() int {
return 25
}

View file

@ -0,0 +1,18 @@
package nested3
// Argument is used as a function argument.
var Argument = 34
// Anna is used to check struct-related usage analyzer logic (calls to methods
// and fields).
var Anna = Person{Age: 24}
// Person is an auxiliary structure containing simple field.
type Person struct {
Age int
}
// GetAge is used to check method calls inside usage analyzer.
func (p Person) GetAge() int {
return p.Age
}

View file

@ -3,6 +3,8 @@ package compiler_test
import (
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
)
func TestGenDeclWithMultiRet(t *testing.T) {
@ -29,3 +31,46 @@ func TestGenDeclWithMultiRet(t *testing.T) {
eval(t, src, big.NewInt(3))
})
}
func TestUnderscoreLocalVarDontEmitCode(t *testing.T) {
src := `package foo
type Foo struct { Int int }
func Main() int {
var _ int
var _ = 1
var (
A = 2
_ = A + 3
_, B, _ = 4, 5, 6
_, _, _ = f(A, B) // unused, but has function call, so the code is expected
_, C, _ = f(A, B)
)
var D = 7 // unused but named, so the code is expected
_ = D
var _ = Foo{ Int: 5 }
var fo = Foo{ Int: 3 }
var _ = 1 + A + fo.Int
var _ = fo.GetInt() // unused, but has method call, so the code is expected
return C
}
func f(a, b int) (int, int, int) {
return 8, 9, 10
}
func (fo Foo) GetInt() int {
return fo.Int
}`
eval(t, src, big.NewInt(9), []interface{}{opcode.INITSLOT, []byte{5, 0}}, // local slot for A, B, C, D, fo
opcode.PUSH2, opcode.STLOC0, // store A
opcode.PUSH5, opcode.STLOC1, // store B
opcode.LDLOC0, opcode.LDLOC1, opcode.SWAP, []interface{}{opcode.CALL, []byte{27}}, // evaluate f() first time
opcode.DROP, opcode.DROP, opcode.DROP, // drop all values from f
opcode.LDLOC0, opcode.LDLOC1, opcode.SWAP, []interface{}{opcode.CALL, []byte{19}}, // evaluate f() second time
opcode.DROP, opcode.STLOC2, opcode.DROP, // store C
opcode.PUSH7, opcode.STLOC3, // store D
opcode.LDLOC3, opcode.DROP, // empty assignment
opcode.PUSH3, opcode.PUSH1, opcode.PACKSTRUCT, opcode.STLOC4, // fo decl
opcode.LDLOC4, []interface{}{opcode.CALL, []byte{12}}, opcode.DROP, // fo.GetInt()
opcode.LDLOC2, opcode.RET, // return C
[]interface{}{opcode.INITSLOT, []byte{0, 2}}, opcode.PUSH10, opcode.PUSH9, opcode.PUSH8, opcode.RET, // f
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.PUSH0, opcode.PICKITEM, opcode.RET) // (fo Foo) GetInt() int
}

View file

@ -18,7 +18,7 @@ func TestVerifyGood(t *testing.T) {
pub, sig := signMessage(t, msg)
src := getVerifyProg(pub, sig)
v, p := vmAndCompileInterop(t, src)
v, p, _ := vmAndCompileInterop(t, src)
p.interops[interopnames.ToID([]byte(interopnames.SystemCryptoCheckSig))] = func(v *vm.VM) error {
assert.Equal(t, pub, v.Estack().Pop().Bytes())
assert.Equal(t, sig, v.Estack().Pop().Bytes())

View file

@ -9,9 +9,12 @@ import (
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -32,9 +35,25 @@ func runTestCases(t *testing.T, tcases []testCase) {
}
}
func eval(t *testing.T, src string, result interface{}) {
vm, _ := vmAndCompileInterop(t, src)
func eval(t *testing.T, src string, result interface{}, expectedOps ...interface{}) []byte {
vm, _, script := vmAndCompileInterop(t, src)
if len(expectedOps) != 0 {
expected := io.NewBufBinWriter()
for _, op := range expectedOps {
switch typ := op.(type) {
case opcode.Opcode:
emit.Opcodes(expected.BinWriter, typ)
case []interface{}:
emit.Instruction(expected.BinWriter, typ[0].(opcode.Opcode), typ[1].([]byte))
default:
t.Fatalf("unexpected evaluation operation: %v", typ)
}
}
require.Equal(t, expected.Bytes(), script)
}
runAndCheck(t, vm, result)
return script
}
func runAndCheck(t *testing.T, v *vm.VM, result interface{}) {
@ -61,11 +80,11 @@ func assertResult(t *testing.T, vm *vm.VM, result interface{}) {
}
func vmAndCompile(t *testing.T, src string) *vm.VM {
v, _ := vmAndCompileInterop(t, src)
v, _, _ := vmAndCompileInterop(t, src)
return v
}
func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) {
func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin, []byte) {
vm := vm.New()
storePlugin := newStoragePlugin()
@ -77,7 +96,7 @@ func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) {
storePlugin.info = di
invokeMethod(t, testMainIdent, b.Script, vm, di)
return vm, storePlugin
return vm, storePlugin, b.Script
}
func invokeMethod(t *testing.T, method string, script []byte, v *vm.VM, di *compiler.DebugInfo) {