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:
commit
f79672c4c3
15 changed files with 1156 additions and 62 deletions
|
@ -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] {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
2
pkg/compiler/testdata/foo/foo.go
vendored
2
pkg/compiler/testdata/foo/foo.go
vendored
|
@ -5,7 +5,7 @@ func NewBar() int {
|
|||
return 10
|
||||
}
|
||||
|
||||
// Dummy is dummy constant.
|
||||
// Dummy is dummy variable.
|
||||
var Dummy = 1
|
||||
|
||||
// Foo is a type.
|
||||
|
|
18
pkg/compiler/testdata/globalvar/funccall/main.go
vendored
Normal file
18
pkg/compiler/testdata/globalvar/funccall/main.go
vendored
Normal 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
14
pkg/compiler/testdata/globalvar/main.go
vendored
Normal 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
|
||||
}
|
29
pkg/compiler/testdata/globalvar/nested1/main.go
vendored
Normal file
29
pkg/compiler/testdata/globalvar/nested1/main.go
vendored
Normal 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
|
||||
}
|
20
pkg/compiler/testdata/globalvar/nested2/main.go
vendored
Normal file
20
pkg/compiler/testdata/globalvar/nested2/main.go
vendored
Normal 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
|
||||
}
|
18
pkg/compiler/testdata/globalvar/nested3/main.go
vendored
Normal file
18
pkg/compiler/testdata/globalvar/nested3/main.go
vendored
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue