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 {
|
func (c *codegen) traverseGlobals() bool {
|
||||||
var hasDefer bool
|
var hasDefer bool
|
||||||
var n, nConst int
|
var n, nConst int
|
||||||
|
var hasUnusedCall bool
|
||||||
var hasDeploy bool
|
var hasDeploy bool
|
||||||
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
|
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
|
||||||
nv, nc := countGlobals(f)
|
nv, nc, huc := countGlobals(f, !hasUnusedCall)
|
||||||
n += nv
|
n += nv
|
||||||
nConst += nc
|
nConst += nc
|
||||||
|
if huc {
|
||||||
|
hasUnusedCall = true
|
||||||
|
}
|
||||||
if !hasDeploy || !hasDefer {
|
if !hasDeploy || !hasDefer {
|
||||||
ast.Inspect(f, func(node ast.Node) bool {
|
ast.Inspect(f, func(node ast.Node) bool {
|
||||||
switch n := node.(type) {
|
switch n := node.(type) {
|
||||||
|
@ -85,10 +89,10 @@ func (c *codegen) traverseGlobals() bool {
|
||||||
|
|
||||||
lastCnt, maxCnt := -1, -1
|
lastCnt, maxCnt := -1, -1
|
||||||
c.ForEachPackage(func(pkg *packages.Package) {
|
c.ForEachPackage(func(pkg *packages.Package) {
|
||||||
if n+nConst > 0 {
|
if n+nConst > 0 || hasUnusedCall {
|
||||||
for _, f := range pkg.Syntax {
|
for _, f := range pkg.Syntax {
|
||||||
c.fillImportMap(f, pkg)
|
c.fillImportMap(f, pkg)
|
||||||
c.convertGlobals(f, pkg.Types)
|
c.convertGlobals(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, f := range pkg.Syntax {
|
for _, f := range pkg.Syntax {
|
||||||
|
@ -143,26 +147,37 @@ func (c *codegen) traverseGlobals() bool {
|
||||||
// countGlobals counts the global variables in the program to add
|
// countGlobals counts the global variables in the program to add
|
||||||
// them with the stack size of the function.
|
// them with the stack size of the function.
|
||||||
// Second returned argument contains the amount of global constants.
|
// 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 numVar, numConst int
|
||||||
|
var hasUnusedCall bool
|
||||||
ast.Inspect(f, func(node ast.Node) bool {
|
ast.Inspect(f, func(node ast.Node) bool {
|
||||||
switch n := node.(type) {
|
switch n := node.(type) {
|
||||||
// Skip all function declarations if we have already encountered `defer`.
|
// Skip all function declarations if we have already encountered `defer`.
|
||||||
case *ast.FuncDecl:
|
case *ast.FuncDecl:
|
||||||
return false
|
return false
|
||||||
// After skipping all funcDecls, we are sure that each value spec
|
// 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:
|
case *ast.GenDecl:
|
||||||
isVar := n.Tok == token.VAR
|
isVar := n.Tok == token.VAR
|
||||||
if isVar || n.Tok == token.CONST {
|
if isVar || n.Tok == token.CONST {
|
||||||
for _, s := range n.Specs {
|
for _, s := range n.Specs {
|
||||||
for _, id := range s.(*ast.ValueSpec).Names {
|
valueSpec := s.(*ast.ValueSpec)
|
||||||
if id.Name != "_" {
|
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 {
|
if isVar {
|
||||||
numVar++
|
numVar++
|
||||||
} else {
|
} else {
|
||||||
numConst++
|
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 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`.
|
// 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.
|
// which should be present in the emitted code.
|
||||||
// This is done using BFS starting from exported functions or
|
// This is done using BFS starting from exported functions or
|
||||||
// the function used in variable declarations (graph edge corresponds to
|
// the function used in variable declarations (graph edge corresponds to
|
||||||
// the function being called in declaration).
|
// the function being called in declaration). It also analyzes global variables
|
||||||
func (c *codegen) analyzeFuncUsage() funcUsage {
|
// 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 {
|
type declPair struct {
|
||||||
decl *ast.FuncDecl
|
decl *ast.FuncDecl
|
||||||
importMap map[string]string
|
importMap map[string]string
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
// 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 contains top-level function declarations.
|
||||||
nodeCache := make(map[string]declPair)
|
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{}
|
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) {
|
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
|
||||||
var pkgPath string
|
var pkgPath string
|
||||||
isMain := pkg == c.mainPkg.Types
|
isMain := pkg == c.mainPkg.Types
|
||||||
|
@ -316,6 +371,44 @@ func (c *codegen) analyzeFuncUsage() funcUsage {
|
||||||
}
|
}
|
||||||
nodeCache[name] = declPair{n, c.importMap, pkgPath}
|
nodeCache[name] = declPair{n, c.importMap, pkgPath}
|
||||||
return false // will be processed in the next stage
|
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
|
return true
|
||||||
})
|
})
|
||||||
|
@ -324,9 +417,24 @@ func (c *codegen) analyzeFuncUsage() funcUsage {
|
||||||
return nil
|
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{}
|
usage := funcUsage{}
|
||||||
for len(diff) != 0 {
|
globalVarsUsage := funcUsage{}
|
||||||
|
for len(diff) != 0 || len(globalVarsDiff) != 0 {
|
||||||
nextDiff := funcUsage{}
|
nextDiff := funcUsage{}
|
||||||
|
nextGlobalVarsDiff := funcUsage{}
|
||||||
|
usedExpressions = usedExpressions[:0]
|
||||||
for name := range diff {
|
for name := range diff {
|
||||||
fd, ok := nodeCache[name]
|
fd, ok := nodeCache[name]
|
||||||
if !ok || usage[name] {
|
if !ok || usage[name] {
|
||||||
|
@ -354,12 +462,157 @@ func (c *codegen) analyzeFuncUsage() funcUsage {
|
||||||
}
|
}
|
||||||
return true
|
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
|
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
|
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 {
|
func isGoBuiltin(name string) bool {
|
||||||
for i := range goBuiltins {
|
for i := range goBuiltins {
|
||||||
if name == goBuiltins[i] {
|
if name == goBuiltins[i] {
|
||||||
|
|
|
@ -350,7 +350,7 @@ func (c *codegen) emitDefault(t types.Type) {
|
||||||
// convertGlobals traverses the AST and only converts global declarations.
|
// convertGlobals traverses the AST and only converts global declarations.
|
||||||
// If we call this in convertFuncDecl, it will load all global variables
|
// If we call this in convertFuncDecl, it will load all global variables
|
||||||
// into the scope of the function.
|
// 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 {
|
ast.Inspect(f, func(node ast.Node) bool {
|
||||||
switch n := node.(type) {
|
switch n := node.(type) {
|
||||||
case *ast.FuncDecl:
|
case *ast.FuncDecl:
|
||||||
|
@ -596,7 +596,8 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := range t.Names {
|
for i, id := range t.Names {
|
||||||
|
if id.Name != "_" {
|
||||||
if len(t.Values) != 0 {
|
if len(t.Values) != 0 {
|
||||||
if i == 0 || !multiRet {
|
if i == 0 || !multiRet {
|
||||||
ast.Walk(c, t.Values[i])
|
ast.Walk(c, t.Values[i])
|
||||||
|
@ -605,6 +606,22 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
c.emitDefault(c.typeOf(t.Type))
|
c.emitDefault(c.typeOf(t.Type))
|
||||||
}
|
}
|
||||||
c.emitStoreVar("", t.Names[i].Name)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2119,7 +2136,7 @@ func (c *codegen) compile(info *buildInfo, pkg *packages.Package) error {
|
||||||
c.mainPkg = pkg
|
c.mainPkg = pkg
|
||||||
c.analyzePkgOrder()
|
c.analyzePkgOrder()
|
||||||
c.fillDocumentInfo()
|
c.fillDocumentInfo()
|
||||||
funUsage := c.analyzeFuncUsage()
|
funUsage := c.analyzeFuncAndGlobalVarUsage()
|
||||||
if c.prog.Err != nil {
|
if c.prog.Err != nil {
|
||||||
return c.prog.Err
|
return c.prog.Err
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,12 @@ import (
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnusedGlobal(t *testing.T) {
|
func TestUnusedGlobal(t *testing.T) {
|
||||||
|
t.Run("simple unused", func(t *testing.T) {
|
||||||
src := `package foo
|
src := `package foo
|
||||||
const (
|
const (
|
||||||
_ int = iota
|
_ int = iota
|
||||||
|
@ -21,10 +23,612 @@ func TestUnusedGlobal(t *testing.T) {
|
||||||
func Main() int {
|
func Main() int {
|
||||||
return 1
|
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))
|
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) {
|
func TestChangeGlobal(t *testing.T) {
|
||||||
|
t.Run("from Main", func(t *testing.T) {
|
||||||
src := `package foo
|
src := `package foo
|
||||||
var a int
|
var a int
|
||||||
func Main() int {
|
func Main() int {
|
||||||
|
@ -35,8 +639,22 @@ func TestChangeGlobal(t *testing.T) {
|
||||||
}
|
}
|
||||||
func set42() { a = 42 }
|
func set42() { a = 42 }
|
||||||
func setLocal() { a := 10; _ = a }`
|
func setLocal() { a := 10; _ = a }`
|
||||||
|
|
||||||
eval(t, src, big.NewInt(42))
|
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) {
|
func TestMultiDeclaration(t *testing.T) {
|
||||||
|
@ -293,3 +911,29 @@ func TestUnderscoreVarsDontUseSlots(t *testing.T) {
|
||||||
src := buf.String()
|
src := buf.String()
|
||||||
eval(t, src, big.NewInt(count))
|
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 {
|
func Main() int {
|
||||||
return 42
|
return 42
|
||||||
}`
|
}`
|
||||||
v, s := vmAndCompileInterop(t, src)
|
v, s, _ := vmAndCompileInterop(t, src)
|
||||||
require.NoError(t, v.Run())
|
require.NoError(t, v.Run())
|
||||||
assertResult(t, v, big.NewInt(42))
|
assertResult(t, v, big.NewInt(42))
|
||||||
require.True(t, len(s.events) == 1)
|
require.True(t, len(s.events) == 1)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package compiler_test
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -12,8 +13,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, expectedLocalsMain int) {
|
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
|
mainStart := -1
|
||||||
for _, m := range sp.info.Methods {
|
for _, m := range sp.info.Methods {
|
||||||
if m.Name.Name == "main" {
|
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() {
|
for op, param, err := ctx.Next(); ; op, param, err = ctx.Next() {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
switch op {
|
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:
|
case opcode.CALL, opcode.CALLL:
|
||||||
actualCall++
|
actualCall++
|
||||||
case opcode.INITSLOT:
|
case opcode.INITSLOT:
|
||||||
|
@ -41,8 +54,12 @@ func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, ex
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if expectedCall != -1 {
|
||||||
require.Equal(t, expectedCall, actualCall)
|
require.Equal(t, expectedCall, actualCall)
|
||||||
require.True(t, expectedInitSlot == actualInitSlot)
|
}
|
||||||
|
if expectedInitSlot != -1 {
|
||||||
|
require.Equal(t, expectedInitSlot, actualInitSlot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInline(t *testing.T) {
|
func TestInline(t *testing.T) {
|
||||||
|
@ -55,13 +72,13 @@ func TestInline(t *testing.T) {
|
||||||
a int
|
a int
|
||||||
b pair
|
b pair
|
||||||
}
|
}
|
||||||
// local alias
|
|
||||||
func sum(a, b int) int {
|
|
||||||
return 42
|
|
||||||
}
|
|
||||||
var Num = 1
|
var Num = 1
|
||||||
func Main() int {
|
func Main() int {
|
||||||
%s
|
%s
|
||||||
|
}
|
||||||
|
// local alias
|
||||||
|
func sum(a, b int) int {
|
||||||
|
return 42
|
||||||
}`
|
}`
|
||||||
t.Run("no return", func(t *testing.T) {
|
t.Run("no return", func(t *testing.T) {
|
||||||
src := fmt.Sprintf(srcTmpl, `inline.NoArgsNoReturn()
|
src := fmt.Sprintf(srcTmpl, `inline.NoArgsNoReturn()
|
||||||
|
|
|
@ -193,7 +193,7 @@ func TestNotify(t *testing.T) {
|
||||||
runtime.Notify("single")
|
runtime.Notify("single")
|
||||||
}`
|
}`
|
||||||
|
|
||||||
v, s := vmAndCompileInterop(t, src)
|
v, s, _ := vmAndCompileInterop(t, src)
|
||||||
v.Estack().PushVal(11)
|
v.Estack().PushVal(11)
|
||||||
|
|
||||||
require.NoError(t, v.Run())
|
require.NoError(t, v.Run())
|
||||||
|
@ -224,7 +224,7 @@ func TestSyscallInGlobalInit(t *testing.T) {
|
||||||
func Main() bool {
|
func Main() bool {
|
||||||
return a
|
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.interops[interopnames.ToID([]byte(interopnames.SystemRuntimeCheckWitness))] = func(v *vm.VM) error {
|
||||||
s := v.Estack().Pop().Value().([]byte)
|
s := v.Estack().Pop().Value().([]byte)
|
||||||
require.Equal(t, "5T", string(s))
|
require.Equal(t, "5T", string(s))
|
||||||
|
@ -479,7 +479,7 @@ func TestInteropTypesComparison(t *testing.T) {
|
||||||
b := struct{}{}
|
b := struct{}{}
|
||||||
return a.Equals(b)
|
return a.Equals(b)
|
||||||
}`
|
}`
|
||||||
vm, _ := vmAndCompileInterop(t, src)
|
vm, _, _ := vmAndCompileInterop(t, src)
|
||||||
err := vm.Run()
|
err := vm.Run()
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.True(t, strings.Contains(err.Error(), "invalid conversion: Struct/ByteString"), 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
|
return 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy is dummy constant.
|
// Dummy is dummy variable.
|
||||||
var Dummy = 1
|
var Dummy = 1
|
||||||
|
|
||||||
// Foo is a type.
|
// 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 (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenDeclWithMultiRet(t *testing.T) {
|
func TestGenDeclWithMultiRet(t *testing.T) {
|
||||||
|
@ -29,3 +31,46 @@ func TestGenDeclWithMultiRet(t *testing.T) {
|
||||||
eval(t, src, big.NewInt(3))
|
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)
|
pub, sig := signMessage(t, msg)
|
||||||
src := getVerifyProg(pub, sig)
|
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 {
|
p.interops[interopnames.ToID([]byte(interopnames.SystemCryptoCheckSig))] = func(v *vm.VM) error {
|
||||||
assert.Equal(t, pub, v.Estack().Pop().Bytes())
|
assert.Equal(t, pub, v.Estack().Pop().Bytes())
|
||||||
assert.Equal(t, sig, 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/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
"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/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/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
"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"
|
||||||
|
"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/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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{}) {
|
func eval(t *testing.T, src string, result interface{}, expectedOps ...interface{}) []byte {
|
||||||
vm, _ := vmAndCompileInterop(t, src)
|
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)
|
runAndCheck(t, vm, result)
|
||||||
|
return script
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAndCheck(t *testing.T, v *vm.VM, result interface{}) {
|
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 {
|
func vmAndCompile(t *testing.T, src string) *vm.VM {
|
||||||
v, _ := vmAndCompileInterop(t, src)
|
v, _, _ := vmAndCompileInterop(t, src)
|
||||||
return v
|
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()
|
vm := vm.New()
|
||||||
|
|
||||||
storePlugin := newStoragePlugin()
|
storePlugin := newStoragePlugin()
|
||||||
|
@ -77,7 +96,7 @@ func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) {
|
||||||
|
|
||||||
storePlugin.info = di
|
storePlugin.info = di
|
||||||
invokeMethod(t, testMainIdent, b.Script, vm, 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) {
|
func invokeMethod(t *testing.T, method string, script []byte, v *vm.VM, di *compiler.DebugInfo) {
|
||||||
|
|
Loading…
Reference in a new issue