package compiler

import (
	"cmp"
	"encoding/binary"
	"errors"
	"fmt"
	"go/ast"
	"go/constant"
	"go/token"
	"go/types"
	"math"
	"math/big"
	"slices"
	"strings"

	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/address"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/nef"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/scparser"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util/bitfield"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/emit"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"golang.org/x/tools/go/packages"
)

type codegen struct {
	// Information about the program with all its dependencies.
	buildInfo *buildInfo

	// prog holds the output buffer.
	prog *io.BufBinWriter

	// Type information.
	typeInfo *types.Info
	// pkgInfoInline is a stack of type information for packages containing inline functions.
	pkgInfoInline []*packages.Package

	// A mapping of func identifiers with their scope.
	funcs map[string]*funcScope

	// A mapping of lambda functions into their scope.
	lambda map[string]*funcScope

	// reverseOffsetMap maps function offsets to a local variable count.
	reverseOffsetMap map[int]nameWithLocals

	// Current funcScope being converted.
	scope *funcScope

	globals map[string]int
	// staticVariables contains global (static in NDX-DN11) variable names and types.
	staticVariables []string
	// initVariables contains variables local to `_initialize` method.
	initVariables []string
	// deployVariables contains variables local to `_initialize` method.
	deployVariables []string

	// A mapping from label's names to their ids.
	labels map[labelWithType]uint16
	// A list of nested label names together with evaluation stack depth.
	labelList []labelWithStackSize
	// inlineContext contains info about inlined function calls.
	inlineContext []inlineContextSingle
	// globalInlineCount contains the amount of auxiliary variables introduced by
	// function inlining during global variables initialization.
	globalInlineCount int

	// A label for the for-loop being currently visited.
	currentFor string
	// A label for the switch statement being visited.
	currentSwitch string
	// A label to be used in the next statement.
	nextLabel string

	// sequencePoints is a mapping from the method name to a slice
	// containing info about mapping from opcode's offset
	// to a text span in the source file.
	sequencePoints map[string][]DebugSeqPoint

	// initEndOffset specifies the end of the initialization method.
	initEndOffset int
	// deployEndOffset specifies the end of the deployment method.
	deployEndOffset int

	// importMap contains mapping from package aliases to full package names for the current file.
	importMap map[string]string

	// constMap contains constants from foreign packages.
	constMap map[string]types.TypeAndValue

	// currPkg is the current package being processed.
	currPkg *packages.Package

	// mainPkg is the main package metadata.
	mainPkg *packages.Package

	// packages contains packages in the order they were loaded.
	packages     []string
	packageCache map[string]*packages.Package

	// exceptionIndex is the index of the static slot where the exception is stored.
	exceptionIndex int

	// documents contains paths to all files used by the program.
	documents []string
	// docIndex maps the file path to the index in the documents array.
	docIndex map[string]int

	// emittedEvents contains all events emitted by the contract.
	emittedEvents map[string][]EmittedEventInfo

	// invokedContracts contains invoked methods of other contracts.
	invokedContracts map[util.Uint160][]string

	// Label table for recording jump destinations.
	l []int

	// Tokens for CALLT instruction
	callTokens []nef.MethodToken
}

type labelOffsetType byte

const (
	labelStart labelOffsetType = iota // labelStart is a default label type
	labelEnd                          // labelEnd is a type for labels that are targets for break
	labelPost                         // labelPost is a type for labels that are targets for continue
)

type labelWithType struct {
	name string
	typ  labelOffsetType
}

type labelWithStackSize struct {
	name string
	sz   int
}

type nameWithLocals struct {
	name  string
	count int
}

type inlineContextSingle struct {
	// labelOffset contains size of labelList at the start of inline call processing.
	// For such calls, we need to drop only the newly created part of stack.
	labelOffset int
	// returnLabel contains label ID pointing to the first instruction right after the call.
	returnLabel uint16
}

type varType int

const (
	varGlobal varType = iota
	varLocal
	varArgument
)

// ErrUnsupportedTypeAssertion is returned when type assertion statement is not supported by the compiler.
var ErrUnsupportedTypeAssertion = errors.New("type assertion with two return values is not supported")

// newLabel creates a new label to jump to.
func (c *codegen) newLabel() (l uint16) {
	li := len(c.l)
	if li > math.MaxUint16 {
		c.prog.Err = errors.New("label number is too big")
		return
	}
	l = uint16(li)
	c.l = append(c.l, -1)
	return
}

// newNamedLabel creates a new label with the specified name.
func (c *codegen) newNamedLabel(typ labelOffsetType, name string) (l uint16) {
	l = c.newLabel()
	lt := labelWithType{name: name, typ: typ}
	c.labels[lt] = l
	return
}

func (c *codegen) setLabel(l uint16) {
	c.l[l] = c.pc() + 1
}

// pc returns the program offset off the last instruction.
func (c *codegen) pc() int {
	return c.prog.Len() - 1
}

func (c *codegen) emitLoadConst(t types.TypeAndValue) {
	if c.prog.Err != nil {
		return
	}

	typ, ok := t.Type.Underlying().(*types.Basic)
	if !ok {
		c.prog.Err = fmt.Errorf("compiler doesn't know how to convert this constant: %v", t)
		return
	}

	switch typ.Kind() {
	case types.Int, types.UntypedInt, types.Uint,
		types.Int8, types.Uint8,
		types.Int16, types.Uint16,
		types.Int32, types.Uint32, types.Int64:
		val, _ := constant.Int64Val(t.Value)
		emit.Int(c.prog.BinWriter, val)
	case types.Uint64:
		val, _ := constant.Int64Val(t.Value)
		emit.BigInt(c.prog.BinWriter, new(big.Int).SetUint64(uint64(val)))
	case types.String, types.UntypedString:
		val := constant.StringVal(t.Value)
		emit.String(c.prog.BinWriter, val)
	case types.Bool, types.UntypedBool:
		val := constant.BoolVal(t.Value)
		emit.Bool(c.prog.BinWriter, val)
	default:
		c.prog.Err = fmt.Errorf("compiler doesn't know how to convert this basic type: %v", t)
		return
	}
}

func (c *codegen) emitLoadField(i int) {
	emit.Int(c.prog.BinWriter, int64(i))
	emit.Opcodes(c.prog.BinWriter, opcode.PICKITEM)
}

func (c *codegen) emitStoreStructField(i int) {
	emit.Int(c.prog.BinWriter, int64(i))
	emit.Opcodes(c.prog.BinWriter, opcode.ROT, opcode.SETITEM)
}

// emitStoreSelectorExpr stores RHS into a selector expression (x.f).
// Assumes the RHS is already on the stack before this call.
func (c *codegen) emitStoreSelectorExpr(n *ast.SelectorExpr) {
	typ := c.typeOf(n.X)
	if c.isInvalidType(typ) {
		// Store to other package global variable.
		c.emitStoreVar(n.X.(*ast.Ident).Name, n.Sel.Name)
		return
	}
	strct, ok := c.getStruct(typ)
	if !ok {
		c.prog.Err = fmt.Errorf("nested selector assigns not supported yet")
		return
	}
	ast.Walk(c, n.X)                      // load the struct
	i := indexOfStruct(strct, n.Sel.Name) // get the index of the field
	c.emitStoreStructField(i)             // store the field
}

// emitStoreIndexExpr emits code to store into an index expression (container[index]).
// Assumes the RHS is already on the stack before calling.
func (c *codegen) emitStoreIndexExpr(n *ast.IndexExpr) {
	ast.Walk(c, n.X)
	ast.Walk(c, n.Index)
	emit.Opcodes(c.prog.BinWriter, opcode.ROT, opcode.SETITEM)
}

// getVarIndex returns variable type and position in the corresponding slot,
// according to the current scope.
func (c *codegen) getVarIndex(pkg string, name string) *varInfo {
	if pkg == "" {
		if c.scope != nil {
			vi := c.scope.vars.getVarInfo(name)
			if vi != nil {
				return vi
			}
		}
	}
	if i, ok := c.globals[c.getIdentName(pkg, name)]; ok {
		return &varInfo{refType: varGlobal, index: i}
	}

	c.scope.newVariable(varLocal, name)
	return c.scope.vars.getVarInfo(name)
}

func getBaseOpcode(t varType) (opcode.Opcode, opcode.Opcode) {
	switch t {
	case varGlobal:
		return opcode.LDSFLD0, opcode.STSFLD0
	case varLocal:
		return opcode.LDLOC0, opcode.STLOC0
	case varArgument:
		return opcode.LDARG0, opcode.STARG0
	default:
		panic("invalid type")
	}
}

// emitLoadVar loads the specified variable to the evaluation stack.
func (c *codegen) emitLoadVar(pkg string, name string) {
	vi := c.getVarIndex(pkg, name)
	if vi.ctx != nil && c.typeAndValueOf(vi.ctx.expr).Value != nil {
		c.emitLoadConst(c.typeAndValueOf(vi.ctx.expr))
		return
	} else if vi.ctx != nil {
		var oldScope []map[string]varInfo
		oldMap := c.importMap
		c.importMap = vi.ctx.importMap
		if c.scope != nil {
			oldScope = c.scope.vars.locals
			c.scope.vars.locals = vi.ctx.scope
		}

		ast.Walk(c, vi.ctx.expr)

		if c.scope != nil {
			c.scope.vars.locals = oldScope
		}
		c.importMap = oldMap
		return
	} else if vi.index == unspecifiedVarIndex {
		emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL)
		return
	}
	c.emitLoadByIndex(vi.refType, vi.index)
}

// emitLoadByIndex loads the specified variable type with index i.
func (c *codegen) emitLoadByIndex(t varType, i int) {
	base, _ := getBaseOpcode(t)
	if i < 7 {
		emit.Opcodes(c.prog.BinWriter, base+opcode.Opcode(i))
	} else {
		emit.Instruction(c.prog.BinWriter, base+7, []byte{byte(i)})
	}
}

// emitStoreVar stores top value from the evaluation stack in the specified variable.
func (c *codegen) emitStoreVar(pkg string, name string) {
	if name == "_" {
		emit.Opcodes(c.prog.BinWriter, opcode.DROP)
		return
	}
	vi := c.getVarIndex(pkg, name)
	if vi.index == unspecifiedVarIndex {
		// Creating a local variable if it was not created earlier during inline calls.
		c.scope.newLocal(name)
		vi = c.getVarIndex(pkg, name)
	}
	c.emitStoreByIndex(vi.refType, vi.index)
}

// emitStoreByIndex stores top value in the specified variable type with index i.
func (c *codegen) emitStoreByIndex(t varType, i int) {
	_, base := getBaseOpcode(t)
	if i < 7 {
		emit.Opcodes(c.prog.BinWriter, base+opcode.Opcode(i))
	} else {
		emit.Instruction(c.prog.BinWriter, base+7, []byte{byte(i)})
	}
}

func (c *codegen) emitDefault(t types.Type) {
	switch t := t.Underlying().(type) {
	case *types.Basic:
		info := t.Info()
		switch {
		case info&types.IsInteger != 0:
			emit.Int(c.prog.BinWriter, 0)
		case info&types.IsString != 0:
			emit.Bytes(c.prog.BinWriter, []byte{})
		case info&types.IsBoolean != 0:
			emit.Bool(c.prog.BinWriter, false)
		default:
			emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL)
		}
	case *types.Struct:
		num := t.NumFields()
		for i := num - 1; i >= 0; i-- {
			c.emitDefault(t.Field(i).Type())
		}
		emit.Int(c.prog.BinWriter, int64(num))
		emit.Opcodes(c.prog.BinWriter, opcode.PACKSTRUCT)
	default:
		emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL)
	}
}

// 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) {
	ast.Inspect(f, func(node ast.Node) bool {
		switch n := node.(type) {
		case *ast.FuncDecl:
			return false
		case *ast.GenDecl:
			ast.Walk(c, n)
		}
		return true
	})
}

func isInitFunc(decl *ast.FuncDecl) bool {
	return decl.Name.Name == "init" && decl.Recv == nil &&
		decl.Type.Params.NumFields() == 0 &&
		decl.Type.Results.NumFields() == 0
}

func (c *codegen) isVerifyFunc(decl *ast.FuncDecl) bool {
	return decl.Name.Name == "Verify" && decl.Recv == nil &&
		decl.Type.Results.NumFields() == 1 &&
		isBool(c.typeOf(decl.Type.Results.List[0].Type))
}

func (c *codegen) clearSlots(n int) {
	for i := range n {
		emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL)
		c.emitStoreByIndex(varLocal, i)
	}
}

// convertInitFuncs converts `init()` functions in file f and returns
// the number of locals in the last processed definition as well as maximum locals number encountered.
func (c *codegen) convertInitFuncs(f *ast.File, pkg *types.Package, lastCount int) (int, int) {
	maxCount := -1
	ast.Inspect(f, func(node ast.Node) bool {
		switch n := node.(type) {
		case *ast.FuncDecl:
			if isInitFunc(n) {
				if lastCount != -1 {
					c.clearSlots(lastCount)
				}

				f := c.convertFuncDecl(f, n, pkg)
				lastCount = f.vars.localsCnt
				maxCount = max(lastCount, maxCount)
			}
		case *ast.GenDecl:
			return false
		}
		return true
	})
	return lastCount, maxCount
}

func isDeployFunc(decl *ast.FuncDecl) bool {
	if decl.Name.Name != "_deploy" || decl.Recv != nil ||
		decl.Type.Params.NumFields() != 2 ||
		decl.Type.Results.NumFields() != 0 {
		return false
	}
	typ, ok := decl.Type.Params.List[1].Type.(*ast.Ident)
	return ok && typ.Name == "bool"
}

func (c *codegen) convertDeployFuncs() int {
	maxCount, lastCount := 0, -1
	c.ForEachFile(func(f *ast.File, pkg *types.Package) {
		ast.Inspect(f, func(node ast.Node) bool {
			switch n := node.(type) {
			case *ast.FuncDecl:
				if isDeployFunc(n) {
					if lastCount != -1 {
						c.clearSlots(lastCount)
					}

					f := c.convertFuncDecl(f, n, pkg)
					lastCount = f.vars.localsCnt
					maxCount = max(lastCount, maxCount)
				}
			case *ast.GenDecl:
				return false
			}
			return true
		})
	})
	return maxCount
}

func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl, pkg *types.Package) *funcScope {
	var (
		f            *funcScope
		ok, isLambda bool
	)
	isInit := isInitFunc(decl)
	isDeploy := isDeployFunc(decl)
	if isInit || isDeploy {
		f = c.newFuncScope(decl, c.newLabel())
	} else {
		f, ok = c.funcs[c.getFuncNameFromDecl("", decl)]
		if ok {
			// If this function is a syscall we will not convert it to bytecode.
			// If it's a potential custom builtin then it needs more specific usages research,
			// thus let's emit the code for it.
			if isSyscall(f) {
				return f
			}
			c.setLabel(f.label)
		} else if f, ok = c.lambda[c.getIdentName("", decl.Name.Name)]; ok {
			isLambda = ok
			c.setLabel(f.label)
		} else {
			f = c.newFunc(decl)
		}
	}

	f.rng.Start = uint16(c.prog.Len())
	c.scope = f
	ast.Inspect(decl, c.scope.analyzeVoidCalls) // @OPTIMIZE

	// All globals copied into the scope of the function need to be added
	// to the stack size of the function.
	if !isInit && !isDeploy {
		sizeArg := f.countArgs()
		if sizeArg > 255 {
			c.prog.Err = errors.New("maximum of 255 local variables is allowed")
		}
		emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(0), byte(sizeArg)})
	}

	f.vars.newScope()
	defer f.vars.dropScope()

	// We need to handle methods, which in Go, is just syntactic sugar.
	// The method receiver will be passed in as the first argument.
	// We check if this declaration has a receiver and load it into the scope.
	//
	// FIXME: For now, we will hard cast this to a struct. We can later fine tune this
	// to support other types.
	if decl.Recv != nil {
		for _, arg := range decl.Recv.List {
			// Use underscore instead of unnamed receiver name, e.g.:
			// func (MyCustomStruct) DoSmth(arg1 int) {...}
			// Unnamed receiver will never be referenced, thus we can use the same approach as for multiple unnamed parameters handling, see #2204.
			recvName := "_"
			if len(arg.Names) != 0 {
				recvName = arg.Names[0].Name
			}
			// only create an argument here, it will be stored via INITSLOT
			c.scope.newVariable(varArgument, recvName)
		}
	}

	// Load the arguments in scope.
	for _, arg := range decl.Type.Params.List {
		for _, id := range arg.Names {
			// only create an argument here, it will be stored via INITSLOT
			c.scope.newVariable(varArgument, id.Name)
		}
	}

	// Emit defaults for named returns.
	if decl.Type.Results.NumFields() != 0 {
		for _, arg := range decl.Type.Results.List {
			for _, id := range arg.Names {
				if id.Name != "_" {
					i := c.scope.newLocal(id.Name)
					c.emitDefault(c.typeOf(arg.Type))
					c.emitStoreByIndex(varLocal, i)
				}
			}
		}
	}

	ast.Walk(c, decl.Body)

	// If we have reached the end of the function without encountering `return` statement,
	// we should clean alt.stack manually.
	// This can be the case with void and named-return functions.
	if !isInit && !isDeploy && !lastStmtIsReturn(decl.Body) {
		c.processDefers()
		c.saveSequencePoint(decl.Body)
		emit.Opcodes(c.prog.BinWriter, opcode.RET)
	}

	if isInit {
		c.initVariables = append(c.initVariables, f.variables...)
	} else if isDeploy {
		c.deployVariables = append(c.deployVariables, f.variables...)
	}

	f.rng.End = uint16(c.prog.Len() - 1)

	if !isLambda {
		for _, f := range c.lambda {
			if _, ok := c.lambda[c.getIdentName("", f.decl.Name.Name)]; !ok {
				panic("ICE: lambda name doesn't match map key")
			}
			c.convertFuncDecl(file, f.decl, pkg)
		}
		c.lambda = make(map[string]*funcScope)
	}

	if !isInit && !isDeploy {
		c.reverseOffsetMap[int(f.rng.Start)] = nameWithLocals{
			name:  f.name,
			count: f.vars.localsCnt,
		}
	}
	return f
}

func (c *codegen) Visit(node ast.Node) ast.Visitor {
	if c.prog.Err != nil {
		return nil
	}
	switch n := node.(type) {
	// General declarations.
	// var (
	//     x = 2
	// )
	case *ast.GenDecl:
		// Filter out generics usage.
		err := c.checkGenericsGenDecl(n, c.currPkg.PkgPath)
		if err != nil {
			c.prog.Err = err
			return nil // Program is invalid.
		}

		if n.Tok == token.VAR || n.Tok == token.CONST {
			c.saveSequencePoint(n)
		}
		if n.Tok == token.CONST {
			for _, spec := range n.Specs {
				vs := spec.(*ast.ValueSpec)
				for i := range vs.Names {
					obj := c.currPkg.Types.Scope().Lookup(vs.Names[i].Name)
					if obj != nil { // can be nil if unused
						c.constMap[c.getIdentName("", vs.Names[i].Name)] = types.TypeAndValue{
							Type:  obj.Type(),
							Value: obj.(*types.Const).Val(),
						}
					}
				}
			}
			return nil
		}
		for _, spec := range n.Specs {
			switch t := spec.(type) {
			case *ast.ValueSpec:
				var isMapKeyCheck bool
				// Filter out type assertion with two return values: var i, ok = v.(int)
				// and map's index expression: var v, ok = myMap["key"]
				if len(t.Names) == 2 && len(t.Values) == 1 && n.Tok == token.VAR {
					err := checkTypeAssertWithOK(t.Values[0])
					if err != nil {
						c.prog.Err = err
						return nil
					}
					if isMapKeyCheck = c.checkGetMapValueWithOKFlag(t.Values[0]); isMapKeyCheck {
						c.emitGetMapValueWithOKFlag(t.Values[0])
					}
				}
				multiRet := n.Tok == token.VAR && len(t.Values) != 0 && len(t.Names) != len(t.Values)
				for _, id := range t.Names {
					if id.Name != "_" {
						if c.scope == nil {
							// it is a global declaration
							c.newGlobal("", id.Name)
						} else {
							c.scope.newLocal(id.Name)
						}
						if !multiRet {
							c.registerDebugVariable(id.Name, t.Type)
						}
					}
				}
				for i, id := range t.Names {
					if id.Name != "_" {
						if !isMapKeyCheck {
							if len(t.Values) == 0 {
								c.emitDefault(c.typeOf(t.Type))
							} else if i == 0 || !multiRet {
								ast.Walk(c, t.Values[i])
							}
						}
						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 || isMapKeyCheck || i != 0 && multiRet {
						c.emitStoreVar("", "_") // drop unused after walk
					}
				}
			}
		}
		return nil

	case *ast.AssignStmt:
		var isMapKeyCheck bool
		// Filter out type assertion with two return values: i, ok = v.(int)
		// and map's index expression: v, ok = myMap["key"]
		if len(n.Lhs) == 2 && len(n.Rhs) == 1 && (n.Tok == token.DEFINE || n.Tok == token.ASSIGN) {
			err := checkTypeAssertWithOK(n.Rhs[0])
			if err != nil {
				c.prog.Err = err
				return nil
			}
			isMapKeyCheck = c.checkGetMapValueWithOKFlag(n.Rhs[0])
		}
		multiRet := len(n.Rhs) != len(n.Lhs)
		c.saveSequencePoint(n)
		// Assign operations are grouped https://github.com/golang/go/blob/master/src/go/types/stmt.go#L160
		isAssignOp := token.ADD_ASSIGN <= n.Tok && n.Tok <= token.AND_NOT_ASSIGN
		if isAssignOp {
			// RHS can contain exactly one expression, thus there is no need to iterate.
			ast.Walk(c, n.Lhs[0])
			ast.Walk(c, n.Rhs[0])
			c.emitToken(n.Tok, c.typeOf(n.Rhs[0]))
		}
		if isMapKeyCheck {
			c.emitGetMapValueWithOKFlag(n.Rhs[0])
		}
		if !isAssignOp && !isMapKeyCheck {
			for i := range n.Rhs {
				ast.Walk(c, n.Rhs[i])
			}
		}
		if multiRet {
			emit.Int(c.prog.BinWriter, int64(len(n.Lhs)))
			emit.Opcodes(c.prog.BinWriter, opcode.REVERSEN)
		}
		for i := len(n.Lhs) - 1; i >= 0; i-- {
			switch t := n.Lhs[i].(type) {
			case *ast.Ident:
				if n.Tok == token.DEFINE {
					if !multiRet {
						c.registerDebugVariable(t.Name, n.Rhs[i])
					}
					if t.Name != "_" {
						c.scope.newLocal(t.Name)
					}
				}
				c.emitStoreVar("", t.Name)

			case *ast.SelectorExpr:
				c.emitStoreSelectorExpr(t) // store the field

			// Assignments to index expressions.
			// slice[0] = 10
			case *ast.IndexExpr:
				c.emitStoreIndexExpr(t)
			}
		}
		return nil

	case *ast.SliceExpr:
		if isCompoundSlice(c.typeOf(n.X).Underlying()) {
			c.prog.Err = errors.New("subslices are supported only for []byte and string")
			return nil
		}

		ast.Walk(c, n.X)

		if n.Low != nil {
			ast.Walk(c, n.Low)
		} else {
			emit.Opcodes(c.prog.BinWriter, opcode.PUSH0)
		}

		if n.High != nil {
			ast.Walk(c, n.High)
		} else {
			emit.Opcodes(c.prog.BinWriter, opcode.OVER, opcode.SIZE)
		}

		emit.Opcodes(c.prog.BinWriter, opcode.OVER, opcode.SUB, opcode.SUBSTR)
		if isString(c.typeOf(n.X)) {
			c.emitConvert(stackitem.ByteArrayT)
		}

		return nil

	case *ast.ReturnStmt:
		l := c.newLabel()
		c.setLabel(l)

		cnt := 0
		start := 0
		if len(c.inlineContext) > 0 {
			start = c.inlineContext[len(c.inlineContext)-1].labelOffset
		}
		for i := start; i < len(c.labelList); i++ {
			cnt += c.labelList[i].sz
		}
		c.dropItems(cnt)

		if len(n.Results) == 0 {
			results := c.scope.decl.Type.Results
			if results.NumFields() != 0 {
				// function with named returns
				for i := len(results.List) - 1; i >= 0; i-- {
					names := results.List[i].Names
					for j := len(names) - 1; j >= 0; j-- {
						if names[j].Name == "_" {
							c.emitDefault(c.typeOf(results.List[i].Type))
						} else {
							c.emitLoadVar("", names[j].Name)
						}
					}
				}
			}
		} else {
			// first result should be on top of the stack
			for i := len(n.Results) - 1; i >= 0; i-- {
				ast.Walk(c, n.Results[i])
			}
		}

		c.processDefers()

		c.saveSequencePoint(n)
		if len(c.pkgInfoInline) == 0 {
			emit.Opcodes(c.prog.BinWriter, opcode.RET)
		} else {
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, c.inlineContext[len(c.inlineContext)-1].returnLabel)
		}
		return nil

	case *ast.IfStmt:
		c.scope.vars.newScope()
		defer c.scope.vars.dropScope()

		lIf := c.newLabel()
		lElse := c.newLabel()
		lElseEnd := c.newLabel()

		if n.Init != nil {
			ast.Walk(c, n.Init)
		}
		if n.Cond != nil {
			c.emitBoolExpr(n.Cond, true, false, lElse)
		}

		c.setLabel(lIf)
		ast.Walk(c, n.Body)
		if n.Else != nil {
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, lElseEnd)
		}

		c.setLabel(lElse)
		if n.Else != nil {
			ast.Walk(c, n.Else)
		}
		c.setLabel(lElseEnd)
		return nil

	case *ast.SwitchStmt:
		c.scope.vars.newScope()
		defer c.scope.vars.dropScope()

		if n.Init != nil {
			ast.Walk(c, n.Init)
		}

		eqOpcode := opcode.EQUAL
		if n.Tag != nil {
			ast.Walk(c, n.Tag)
			eqOpcode, _ = convertToken(token.EQL, c.typeOf(n.Tag))
		} else {
			emit.Bool(c.prog.BinWriter, true)
		}
		switchEnd, label := c.generateLabel(labelEnd)

		lastSwitch := c.currentSwitch
		c.currentSwitch = label
		c.pushStackLabel(label, 1)

		startLabels := make([]uint16, len(n.Body.List))
		for i := range startLabels {
			startLabels[i] = c.newLabel()
		}
		for i := range n.Body.List {
			lEnd := c.newLabel()
			lStart := startLabels[i]
			cc := n.Body.List[i].(*ast.CaseClause)

			if l := len(cc.List); l != 0 { // if not `default`
				for j := range cc.List {
					emit.Opcodes(c.prog.BinWriter, opcode.DUP)
					ast.Walk(c, cc.List[j])
					emit.Opcodes(c.prog.BinWriter, eqOpcode)
					if j == l-1 {
						emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, lEnd)
					} else {
						emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, lStart)
					}
				}
			}

			c.scope.vars.newScope()

			c.setLabel(lStart)
			last := len(cc.Body) - 1
			for j, stmt := range cc.Body {
				if j == last && isFallthroughStmt(stmt) {
					emit.Jmp(c.prog.BinWriter, opcode.JMPL, startLabels[i+1])
					break
				}
				ast.Walk(c, stmt)
			}
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, switchEnd)
			c.setLabel(lEnd)

			c.scope.vars.dropScope()
		}

		c.setLabel(switchEnd)
		c.dropStackLabel()

		c.currentSwitch = lastSwitch

		return nil

	case *ast.FuncLit:
		var found bool
		var l uint16
		for _, fs := range c.lambda {
			if fs.decl.Body == n.Body {
				found = true
				l = fs.label
				break
			}
		}
		if !found {
			l = c.newLabel()
			c.newLambda(l, n)
		}

		buf := make([]byte, 4)
		binary.LittleEndian.PutUint16(buf, l)
		emit.Instruction(c.prog.BinWriter, opcode.PUSHA, buf)
		return nil

	case *ast.BasicLit:
		c.emitLoadConst(c.typeAndValueOf(n))
		return nil

	case *ast.StarExpr:
		_, ok := c.getStruct(c.typeOf(n.X))
		if !ok {
			c.prog.Err = errors.New("dereferencing is only supported on structs")
			return nil
		}
		ast.Walk(c, n.X)
		c.emitConvert(stackitem.StructT)
		return nil

	case *ast.Ident:
		if tv := c.typeAndValueOf(n); tv.Value != nil {
			c.emitLoadConst(tv)
		} else if n.Name == "nil" {
			emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL)
		} else {
			c.emitLoadVar("", n.Name)
		}
		return nil

	case *ast.CompositeLit:
		t := c.typeOf(n)
		switch typ := t.Underlying().(type) {
		case *types.Struct:
			c.convertStruct(n, false)
		case *types.Map:
			c.convertMap(n)
		default:
			if tn, ok := t.(*types.Named); ok && isInteropPath(tn.String()) {
				st, _, _, _ := scAndVMInteropTypeFromExpr(tn, false)
				expectedLen := -1
				switch st {
				case smartcontract.Hash160Type:
					expectedLen = 20
				case smartcontract.Hash256Type:
					expectedLen = 32
				default:
				}
				if expectedLen != -1 && expectedLen != len(n.Elts) {
					c.prog.Err = fmt.Errorf("%s type must have size %d", tn.Obj().Name(), expectedLen)
					return nil
				}
			}
			ln := len(n.Elts)
			// ByteArrays needs a different approach than normal arrays.
			if isByteSlice(typ) {
				c.convertByteArray(n.Elts)
				return nil
			}
			for i := ln - 1; i >= 0; i-- {
				ast.Walk(c, n.Elts[i])
			}
			emit.Int(c.prog.BinWriter, int64(ln))
			emit.Opcodes(c.prog.BinWriter, opcode.PACK)
		}

		return nil

	case *ast.BinaryExpr:
		c.emitBinaryExpr(n, false, false, 0)
		return nil

	case *ast.CallExpr:
		var (
			f         *funcScope
			ok        bool
			name      string
			numArgs   = len(n.Args)
			isBuiltin bool
			isFunc    bool
			isLiteral bool
		)

		switch fun := n.Fun.(type) {
		case *ast.Ident:
			f, ok = c.getFuncFromIdent(fun)
			isBuiltin = isGoBuiltin(fun.Name)
			if !ok && !isBuiltin {
				name = fun.Name
			}
			// distinguish lambda invocations from type conversions
			if fun.Obj != nil && fun.Obj.Kind == ast.Var {
				isFunc = true
			}
			if ok && canInline(f.pkg.Path(), f.decl.Name.Name, false) {
				c.inlineCall(f, n)
				return nil
			}
		case *ast.SelectorExpr:
			name, isMethod := c.getFuncNameFromSelector(fun)

			f, ok = c.funcs[name]
			if ok {
				f.selector = fun.X
				isBuiltin = isPotentialCustomBuiltin(f, n)
				if canInline(f.pkg.Path(), f.decl.Name.Name, isBuiltin) {
					c.inlineCall(f, n)
					return nil
				}
			} else {
				typ := c.typeOf(fun)
				if typ == nil {
					c.prog.Err = fmt.Errorf("can't find function %q in package %s", name, c.currPkg.PkgPath)
					return nil
				}
				ast.Walk(c, n.Args[0])
				c.emitExplicitConvert(c.typeOf(n.Args[0]), typ)
				return nil
			}
			if isMethod {
				// If this is a method call we need to walk the AST to load the struct locally.
				// Otherwise, this is a function call from an imported package and we can call it
				// directly.
				ast.Walk(c, fun.X)
				// Don't forget to add 1 extra argument when it's a method.
				numArgs++
			}
		case *ast.ArrayType:
			// For now we will assume that there are only byte slice conversions.
			// E.g. []byte("foobar") or []byte(scriptHash).
			ast.Walk(c, n.Args[0])
			c.emitConvert(stackitem.BufferT)
			return nil
		case *ast.InterfaceType:
			// It's a type conversion into some interface. Programmer is responsible
			// for the conversion to be appropriate, just load the arg.
			ast.Walk(c, n.Args[0])
			return nil
		case *ast.FuncLit:
			isLiteral = true
		}

		c.saveSequencePoint(n)

		args := transformArgs(f, n.Fun, isBuiltin, n.Args)

		// Handle the arguments
		for _, arg := range args {
			ast.Walk(c, arg)
			typ := c.typeOf(arg)
			_, ok := typ.Underlying().(*types.Struct)
			if ok && !isInteropPath(typ.String()) {
				// To clone struct fields we create a new array and append struct to it.
				// This way even non-pointer struct fields will be copied.
				emit.Opcodes(c.prog.BinWriter, opcode.NEWARRAY0,
					opcode.DUP, opcode.ROT, opcode.APPEND,
					opcode.POPITEM)
			}
		}
		// Do not swap for builtin functions.
		if !isBuiltin && (f != nil && !isSyscall(f)) {
			typ, ok := c.typeOf(n.Fun).(*types.Signature)
			if ok && typ.Variadic() && !n.Ellipsis.IsValid() {
				// pack variadic args into an array only if last argument is not of form `...`
				varSize := c.packVarArgs(n, typ)
				numArgs -= varSize - 1
			}
			c.emitReverse(numArgs)
		}

		// Check builtin first to avoid nil pointer on funcScope!
		switch {
		case isBuiltin:
			// Use the ident to check, builtins are not in func scopes.
			// We can be sure builtins are of type *ast.Ident.
			c.convertBuiltin(n)
		case name != "":
			// Function was not found, thus it can only be an invocation of a func-typed variable or type conversion.
			// We care only about string conversions because all others are effectively no-op in NeoVM.
			// E.g. one cannot write `bool(int(a))`, only `int32(int(a))`.
			var typ = c.typeOf(n.Fun)
			if typ == nil {
				c.prog.Err = fmt.Errorf("can't find function %q in package %s", name, c.currPkg.PkgPath)
				return nil
			}
			if isString(typ) {
				c.emitConvert(stackitem.ByteArrayT)
			} else if isFunc {
				c.emitLoadVar("", name)
				emit.Opcodes(c.prog.BinWriter, opcode.CALLA)
			}
		case isLiteral:
			ast.Walk(c, n.Fun)
			emit.Opcodes(c.prog.BinWriter, opcode.CALLA)
		case isSyscall(f):
			c.convertSyscall(f, n)
		default:
			emit.Call(c.prog.BinWriter, opcode.CALLL, f.label)
		}

		if c.scope != nil && c.scope.voidCalls[n] {
			var sz int
			if f != nil {
				sz = f.decl.Type.Results.NumFields()
			} else if !isBuiltin {
				// lambda invocation
				f := c.typeOf(n.Fun).Underlying().(*types.Signature)
				sz = f.Results().Len()
			}
			for range sz {
				emit.Opcodes(c.prog.BinWriter, opcode.DROP)
			}
		}

		return nil

	case *ast.DeferStmt:
		catch := c.newLabel()
		finally := c.newLabel()
		param := make([]byte, 8)
		binary.LittleEndian.PutUint16(param[0:], catch)
		binary.LittleEndian.PutUint16(param[4:], finally)
		emit.Instruction(c.prog.BinWriter, opcode.TRYL, param)
		index := c.scope.newLocal(fmt.Sprintf("defer@%d", n.Call.Pos()))
		emit.Opcodes(c.prog.BinWriter, opcode.PUSH1)
		c.emitStoreByIndex(varLocal, index)
		c.scope.deferStack = append(c.scope.deferStack, deferInfo{
			catchLabel:   catch,
			finallyLabel: finally,
			expr:         n.Call,
			localIndex:   index,
		})
		return nil

	case *ast.SelectorExpr:
		typ := c.typeOf(n.X)
		if c.isInvalidType(typ) {
			// This is a global variable from a package.
			pkgAlias := n.X.(*ast.Ident).Name
			name := c.getIdentName(pkgAlias, n.Sel.Name)
			if tv, ok := c.constMap[name]; ok {
				c.emitLoadConst(tv)
			} else {
				c.emitLoadVar(pkgAlias, n.Sel.Name)
			}
			return nil
		}
		strct, ok := c.getStruct(typ)
		if !ok {
			c.prog.Err = fmt.Errorf("selectors are supported only on structs")
			return nil
		}
		ast.Walk(c, n.X) // load the struct
		i := indexOfStruct(strct, n.Sel.Name)
		c.emitLoadField(i) // load the field
		return nil

	case *ast.UnaryExpr:
		if n.Op == token.AND {
			// We support only taking address from struct literals.
			// For identifiers we can't support "taking address" in a general way
			// because both struct and array are reference types.
			lit, ok := n.X.(*ast.CompositeLit)
			if ok {
				c.convertStruct(lit, true)
				return nil
			}
			c.prog.Err = fmt.Errorf("'&' can be used only with struct literals")
			return nil
		}

		ast.Walk(c, n.X)
		// From https://golang.org/ref/spec#Operators
		// there can be only following unary operators
		// "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
		// of which last three are not used in SC
		switch n.Op {
		case token.ADD:
			// +10 == 10, no need to do anything in this case
		case token.SUB:
			emit.Opcodes(c.prog.BinWriter, opcode.NEGATE)
		case token.NOT:
			emit.Opcodes(c.prog.BinWriter, opcode.NOT)
		case token.XOR:
			emit.Opcodes(c.prog.BinWriter, opcode.INVERT)
		default:
			c.prog.Err = fmt.Errorf("invalid unary operator: %s", n.Op)
			return nil
		}
		return nil

	case *ast.IncDecStmt:
		ast.Walk(c, n.X)
		c.emitToken(n.Tok, c.typeOf(n.X))

		switch t := n.X.(type) {
		case *ast.Ident:
			// Identifiers are supported for (post) for stmts.
			// for i := 0; i < 10; i++ {}
			// Where the post stmt is ( i++ )
			c.emitStoreVar("", t.Name)

		case *ast.SelectorExpr:
			// Struct field selectors are supported, e.g. myStruct.t++ or p.field++.
			c.emitStoreSelectorExpr(t)

		case *ast.IndexExpr:
			// Index expressions are supported for arrays/slices/maps, e.g. s[0]++, m["k"]++.
			c.emitStoreIndexExpr(t)
		}
		return nil

	case *ast.IndexExpr:
		// Walk the expression, this could be either an Ident or SelectorExpr.
		// This will load local whatever X is.
		ast.Walk(c, n.X)
		ast.Walk(c, n.Index)
		emit.Opcodes(c.prog.BinWriter, opcode.PICKITEM) // just pickitem here

		return nil

	case *ast.BranchStmt:
		var label string
		if n.Label != nil {
			label = n.Label.Name
		} else if n.Tok == token.BREAK {
			label = c.currentSwitch
		} else if n.Tok == token.CONTINUE {
			label = c.currentFor
		}

		cnt := 0
		for i := len(c.labelList) - 1; i >= 0 && c.labelList[i].name != label; i-- {
			cnt += c.labelList[i].sz
		}
		c.dropItems(cnt)

		switch n.Tok {
		case token.BREAK:
			end := c.getLabelOffset(labelEnd, label)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, end)
		case token.CONTINUE:
			post := c.getLabelOffset(labelPost, label)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, post)
		default:
			return nil
		}

	case *ast.LabeledStmt:
		c.nextLabel = n.Label.Name

		ast.Walk(c, n.Stmt)

		return nil

	case *ast.BlockStmt:
		c.scope.vars.newScope()
		defer c.scope.vars.dropScope()

		for i := range n.List {
			ast.Walk(c, n.List[i])
		}

		return nil

	case *ast.ForStmt:
		c.scope.vars.newScope()
		defer c.scope.vars.dropScope()

		fstart, label := c.generateLabel(labelStart)
		fend := c.newNamedLabel(labelEnd, label)
		fpost := c.newNamedLabel(labelPost, label)

		lastLabel := c.currentFor
		lastSwitch := c.currentSwitch
		c.currentFor = label
		c.currentSwitch = label

		// Walk the initializer and condition.
		if n.Init != nil {
			ast.Walk(c, n.Init)
		}

		// Set label and walk the condition.
		c.pushStackLabel(label, 0)
		c.setLabel(fstart)
		if n.Cond != nil {
			ast.Walk(c, n.Cond)

			// Jump if the condition is false
			emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, fend)
		}

		// Walk body followed by the iterator (post stmt).
		ast.Walk(c, n.Body)
		c.setLabel(fpost)
		if n.Post != nil {
			ast.Walk(c, n.Post)
		}

		// Jump back to condition.
		emit.Jmp(c.prog.BinWriter, opcode.JMPL, fstart)
		c.setLabel(fend)
		c.dropStackLabel()

		c.currentFor = lastLabel
		c.currentSwitch = lastSwitch

		return nil

	case *ast.RangeStmt:
		c.scope.vars.newScope()
		defer c.scope.vars.dropScope()

		labelAfterRangeLoop := c.newLabel()
		start, label := c.generateLabel(labelStart)
		end := c.newNamedLabel(labelEnd, label)
		post := c.newNamedLabel(labelPost, label)

		lastFor := c.currentFor
		lastSwitch := c.currentSwitch
		c.currentFor = label
		c.currentSwitch = label

		ast.Walk(c, n.X)

		// Implementation is a bit different for integers, slices and maps:
		// For integers, we iterate through indices from 0 to len-1, storing integer and index on stack.
		// For slices, we iterate through indices from 0 to len-1, storing array, len and index on stack.
		// For maps, we iterate through indices from 0 to len-1, storing map, keyarray, size and index on stack.
		var (
			isMap     bool
			isInteger bool
		)
		switch typ := c.typeOf(n.X).Underlying().(type) {
		case *types.Map:
			isMap = true
		case *types.Basic:
			isInteger = typ.Info()&types.IsInteger != 0
		}
		if !isInteger {
			emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.ISNULL)
			emit.Instruction(c.prog.BinWriter, opcode.JMPIFNOT, []byte{2 + 1 + 1 + 4})
			emit.Opcodes(c.prog.BinWriter, opcode.DROP)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, labelAfterRangeLoop)
			emit.Opcodes(c.prog.BinWriter, opcode.DUP)
			if isMap {
				emit.Opcodes(c.prog.BinWriter, opcode.KEYS, opcode.DUP)
			}
			emit.Opcodes(c.prog.BinWriter, opcode.SIZE)
		}
		emit.Opcodes(c.prog.BinWriter, opcode.PUSH0)

		stackSize := 3 // slice, len(slice), index
		if isMap {
			stackSize++ // map, keys, len(keys), index in keys
		}
		if isInteger {
			stackSize-- // integer, index
		}
		c.pushStackLabel(label, stackSize)
		c.setLabel(start)

		emit.Opcodes(c.prog.BinWriter, opcode.OVER, opcode.OVER)
		emit.Jmp(c.prog.BinWriter, opcode.JMPLEL, end)

		var (
			haveKey   bool
			haveVal   bool
			keyIdent  *ast.Ident
			keyLoaded bool
			valIdent  *ast.Ident
		)
		if n.Key != nil {
			keyIdent, haveKey = n.Key.(*ast.Ident)
			if !haveKey {
				c.prog.Err = errors.New("only simple identifiers can be used for range loop keys (see #2870)")
				return nil
			}
			haveKey = (keyIdent.Name != "_")
		}
		if n.Value != nil {
			valIdent, haveVal = n.Value.(*ast.Ident)
			if !haveVal {
				c.prog.Err = errors.New("only simple identifiers can be used for range loop values (see #2870)")
				return nil
			}
			haveVal = (valIdent.Name != "_")
		}
		if haveKey {
			if isMap {
				c.rangeLoadKey()
				if haveVal {
					emit.Opcodes(c.prog.BinWriter, opcode.DUP)
					keyLoaded = true
				}
			} else {
				emit.Opcodes(c.prog.BinWriter, opcode.DUP)
			}
			if n.Tok == token.DEFINE {
				c.scope.newLocal(keyIdent.Name)
			}
			c.emitStoreVar("", keyIdent.Name)
		}
		if haveVal {
			if !isMap || !keyLoaded {
				c.rangeLoadKey()
			}
			if isMap {
				// we have loaded only key from key array, now load value
				emit.Int(c.prog.BinWriter, 4)
				emit.Opcodes(c.prog.BinWriter,
					opcode.PICK, // load map itself (+1 because key was pushed)
					opcode.SWAP, // key should be on top
					opcode.PICKITEM)
			}
			if n.Tok == token.DEFINE {
				c.scope.newLocal(valIdent.Name)
			}
			c.emitStoreVar("", valIdent.Name)
		}

		ast.Walk(c, n.Body)

		c.setLabel(post)

		emit.Opcodes(c.prog.BinWriter, opcode.INC)
		emit.Jmp(c.prog.BinWriter, opcode.JMPL, start)

		c.setLabel(end)
		c.dropStackLabel()
		c.setLabel(labelAfterRangeLoop)

		c.currentFor = lastFor
		c.currentSwitch = lastSwitch

		return nil

	// We don't really care about assertions for the core logic.
	// The only thing we need is to please the compiler type checking.
	// For this to work properly, we only need to walk the expression
	// which is not the assertion type.
	case *ast.TypeAssertExpr:
		ast.Walk(c, n.X)
		if c.isCallExprSyscall(n.X) {
			return nil
		}

		goTyp := c.typeOf(n.Type)
		if canConvert(goTyp.String()) {
			typ := toNeoType(goTyp)
			c.emitConvert(typ)
		}
		return nil
	}
	return c
}

func checkTypeAssertWithOK(n ast.Node) error {
	if t, ok := n.(*ast.TypeAssertExpr); ok &&
		t.Type != nil { // not a type switch
		return ErrUnsupportedTypeAssertion
	}
	return nil
}

// packVarArgs packs variadic arguments into an array
// and returns the amount of arguments packed.
func (c *codegen) packVarArgs(n *ast.CallExpr, typ *types.Signature) int {
	varSize := len(n.Args) - typ.Params().Len() + 1
	c.emitReverse(varSize)
	emit.Int(c.prog.BinWriter, int64(varSize))
	emit.Opcodes(c.prog.BinWriter, opcode.PACK)
	return varSize
}

func (c *codegen) isCallExprSyscall(e ast.Expr) bool {
	ce, ok := e.(*ast.CallExpr)
	if !ok {
		return false
	}
	sel, ok := ce.Fun.(*ast.SelectorExpr)
	if !ok {
		return false
	}
	name, _ := c.getFuncNameFromSelector(sel)
	f, ok := c.funcs[name]
	return ok && isSyscall(f)
}

// processDefers emits code for `defer` statements.
// TRY-related opcodes handle exception as follows:
//  1. CATCH block is executed only if exception has occurred.
//  2. FINALLY block is always executed, but after catch block.
//
// Go `defer` statements are a bit different:
//  1. `defer` is always executed irregardless of whether an exception has occurred.
//  2. `recover` can or can not handle a possible exception.
//
// Thus, we use the following approach:
//  1. Throwed exception is saved in a static field X, static fields Y and it is set to true.
//  2. For each defer local there is a dedicated local variable which is set to 1 if `defer` statement
//     is encountered during an actual execution.
//  3. CATCH and FINALLY blocks are the same, and both contain the same CALLs.
//  4. Right before the CATCH block, check a variable from (2). If it is null, jump to the end of CATCH+FINALLY block.
//  5. In CATCH block we set Y to true and emit default return values if it is the last defer.
//  6. Execute FINALLY block only if Y is false.
func (c *codegen) processDefers() {
	for i := len(c.scope.deferStack) - 1; i >= 0; i-- {
		stmt := c.scope.deferStack[i]
		after := c.newLabel()

		c.emitLoadByIndex(varLocal, c.scope.deferStack[i].localIndex)
		emit.Opcodes(c.prog.BinWriter, opcode.ISNULL)
		emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, after)

		emit.Jmp(c.prog.BinWriter, opcode.ENDTRYL, after)

		c.setLabel(stmt.catchLabel)
		c.emitStoreByIndex(varGlobal, c.exceptionIndex)
		emit.Int(c.prog.BinWriter, 1)

		finalIndex := c.getVarIndex("", finallyVarName).index
		c.emitStoreByIndex(varLocal, finalIndex)
		ast.Walk(c, stmt.expr)
		if i == 0 {
			results := c.scope.decl.Type.Results
			if results.NumFields() != 0 {
				// After panic, default values must be returns, except for named returns,
				// which we don't support here for now.
				for i := len(results.List) - 1; i >= 0; i-- {
					c.emitDefault(c.typeOf(results.List[i].Type))
				}
			}
		}
		emit.Jmp(c.prog.BinWriter, opcode.ENDTRYL, after)

		c.setLabel(stmt.finallyLabel)
		before := c.newLabel()
		c.emitLoadByIndex(varLocal, finalIndex)
		emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, before)
		ast.Walk(c, stmt.expr)
		c.setLabel(before)
		emit.Int(c.prog.BinWriter, 0)
		c.emitStoreByIndex(varLocal, finalIndex)
		emit.Opcodes(c.prog.BinWriter, opcode.ENDFINALLY)
		c.setLabel(after)
	}
}

// emitExplicitConvert handles `someType(someValue)` conversions between string/[]byte.
// Rules for conversion:
//  1. interop.* types are converted to ByteArray if not already.
//  2. Otherwise, convert between ByteArray/Buffer.
//  3. Rules for types which are not string/[]byte should already
//     be enforced by go parser.
func (c *codegen) emitExplicitConvert(from, to types.Type) {
	if isInteropPath(to.String()) {
		if isByteSlice(from) && !isString(from) {
			c.emitConvert(stackitem.ByteArrayT)
		}
	} else if isByteSlice(to) && !isByteSlice(from) {
		c.emitConvert(stackitem.BufferT)
	} else if isString(to) && !isString(from) {
		c.emitConvert(stackitem.ByteArrayT)
	}
}

func (c *codegen) isInvalidType(typ types.Type) bool {
	tb, ok := typ.(*types.Basic)
	return typ == nil || ok && tb.Kind() == types.Invalid
}

func (c *codegen) rangeLoadKey() {
	emit.Int(c.prog.BinWriter, 2)
	emit.Opcodes(c.prog.BinWriter,
		opcode.PICK, // load keys
		opcode.OVER, // load index in key array
		opcode.PICKITEM)
}

func isFallthroughStmt(c ast.Node) bool {
	s, ok := c.(*ast.BranchStmt)
	return ok && s.Tok == token.FALLTHROUGH
}

func (c *codegen) getCompareWithNilArg(n *ast.BinaryExpr) ast.Expr {
	if isExprNil(n.X) {
		return n.Y
	} else if isExprNil(n.Y) {
		return n.X
	}
	return nil
}

func (c *codegen) emitJumpOnCondition(cond bool, jmpLabel uint16) {
	if cond {
		emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, jmpLabel)
	} else {
		emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, jmpLabel)
	}
}

// emitBoolExpr emits boolean expression. If needJump is true and expression evaluates to `cond`,
// jump to jmpLabel is performed and no item is left on stack.
func (c *codegen) emitBoolExpr(n ast.Expr, needJump bool, cond bool, jmpLabel uint16) {
	if be, ok := n.(*ast.BinaryExpr); ok {
		c.emitBinaryExpr(be, needJump, cond, jmpLabel)
	} else {
		ast.Walk(c, n)
		if needJump {
			c.emitJumpOnCondition(cond, jmpLabel)
		}
	}
}

// emitBinaryExpr emits binary expression. If needJump is true and expression evaluates to `cond`,
// jump to jmpLabel is performed and no item is left on stack.
func (c *codegen) emitBinaryExpr(n *ast.BinaryExpr, needJump bool, cond bool, jmpLabel uint16) {
	// The AST package will try to resolve all basic literals for us.
	// If the typeinfo.Value is not nil we know that the expr is resolved
	// and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6.
	// NOTE: Constants will also be automatically resolved be the AST parser.
	// example:
	// const x = 10
	// x + 2 will results into 12
	tinfo := c.typeAndValueOf(n)
	if tinfo.Value != nil {
		c.emitLoadConst(tinfo)
		if needJump && isBool(tinfo.Type) {
			c.emitJumpOnCondition(cond, jmpLabel)
		}
		return
	} else if arg := c.getCompareWithNilArg(n); arg != nil {
		ast.Walk(c, arg)
		emit.Opcodes(c.prog.BinWriter, opcode.ISNULL)
		if needJump {
			c.emitJumpOnCondition(cond == (n.Op == token.EQL), jmpLabel)
		} else if n.Op == token.NEQ {
			emit.Opcodes(c.prog.BinWriter, opcode.NOT)
		}
		return
	}

	switch n.Op {
	case token.LAND, token.LOR:
		end := c.newLabel()

		// true || .. == true, false && .. == false
		condShort := n.Op == token.LOR
		if needJump {
			l := end
			if cond == condShort {
				l = jmpLabel
			}
			c.emitBoolExpr(n.X, true, condShort, l)
			c.emitBoolExpr(n.Y, true, cond, jmpLabel)
		} else {
			push := c.newLabel()
			c.emitBoolExpr(n.X, true, condShort, push)
			c.emitBoolExpr(n.Y, false, false, 0)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, end)
			c.setLabel(push)
			emit.Bool(c.prog.BinWriter, condShort)
		}
		c.setLabel(end)

	default:
		ast.Walk(c, n.X)
		ast.Walk(c, n.Y)
		typ := c.typeOf(n.X)
		if !needJump {
			c.emitToken(n.Op, typ)
			return
		}
		op, ok := getJumpForToken(n.Op, typ)
		if !ok {
			c.emitToken(n.Op, typ)
			c.emitJumpOnCondition(cond, jmpLabel)
			return
		}
		if !cond {
			op = negateJmp(op)
		}
		emit.Jmp(c.prog.BinWriter, op, jmpLabel)
	}
}

func (c *codegen) pushStackLabel(name string, size int) {
	c.labelList = append(c.labelList, labelWithStackSize{
		name: name,
		sz:   size,
	})
}

func (c *codegen) dropStackLabel() {
	last := len(c.labelList) - 1
	c.dropItems(c.labelList[last].sz)
	c.labelList = c.labelList[:last]
}

func (c *codegen) dropItems(n int) {
	if n < 4 {
		for range n {
			emit.Opcodes(c.prog.BinWriter, opcode.DROP)
		}
		return
	}

	emit.Int(c.prog.BinWriter, int64(n))
	emit.Opcodes(c.prog.BinWriter, opcode.PACK, opcode.DROP)
}

// emitReverse reverses top num items of the stack.
func (c *codegen) emitReverse(num int) {
	switch num {
	case 0, 1:
	case 2:
		emit.Opcodes(c.prog.BinWriter, opcode.SWAP)
	case 3:
		emit.Opcodes(c.prog.BinWriter, opcode.REVERSE3)
	case 4:
		emit.Opcodes(c.prog.BinWriter, opcode.REVERSE4)
	default:
		emit.Int(c.prog.BinWriter, int64(num))
		emit.Opcodes(c.prog.BinWriter, opcode.REVERSEN)
	}
}

// generateLabel returns a new label.
func (c *codegen) generateLabel(typ labelOffsetType) (uint16, string) {
	name := c.nextLabel
	if name == "" {
		name = fmt.Sprintf("@%d", len(c.l))
	}

	c.nextLabel = ""
	return c.newNamedLabel(typ, name), name
}

func (c *codegen) getLabelOffset(typ labelOffsetType, name string) uint16 {
	return c.labels[labelWithType{name: name, typ: typ}]
}

// For `&&` and `||` it return an opcode which jumps only if result is known:
// false && .. == false, true || .. = true.
func getJumpForToken(tok token.Token, typ types.Type) (opcode.Opcode, bool) {
	switch tok {
	case token.GTR:
		return opcode.JMPGTL, true
	case token.GEQ:
		return opcode.JMPGEL, true
	case token.LSS:
		return opcode.JMPLTL, true
	case token.LEQ:
		return opcode.JMPLEL, true
	case token.EQL, token.NEQ:
		if isNumber(typ) {
			if tok == token.EQL {
				return opcode.JMPEQL, true
			}
			return opcode.JMPNEL, true
		}
	default:
	}
	return 0, false
}

func (c *codegen) getCallToken(hash util.Uint160, method string, pcount int, hasReturn bool, flag callflag.CallFlag) (uint16, error) {
	needed := nef.MethodToken{
		Hash:       hash,
		Method:     method,
		ParamCount: uint16(pcount),
		HasReturn:  hasReturn,
		CallFlag:   flag,
	}
	for i := range c.callTokens {
		if c.callTokens[i] == needed {
			return uint16(i), nil
		}
	}
	if len(c.callTokens) == math.MaxUint16 {
		return 0, errors.New("call token overflow")
	}
	c.callTokens = append(c.callTokens, needed)
	return uint16(len(c.callTokens) - 1), nil
}

func (c *codegen) convertSyscall(f *funcScope, expr *ast.CallExpr) {
	var callArgs = expr.Args[1:]

	if strings.HasPrefix(f.name, "CallWithToken") {
		callArgs = expr.Args[3:]
	}
	for _, arg := range callArgs {
		ast.Walk(c, arg)
	}
	tv := c.typeAndValueOf(expr.Args[0])
	if tv.Value == nil || !isString(tv.Type) {
		c.prog.Err = fmt.Errorf("bad intrinsic argument")
		return
	}
	arg0Str := constant.StringVal(tv.Value)

	if strings.HasPrefix(f.name, "Syscall") {
		c.emitReverse(len(callArgs))
		emit.Syscall(c.prog.BinWriter, arg0Str)
	} else if strings.HasPrefix(f.name, "CallWithToken") {
		var hasRet = !strings.HasSuffix(f.name, "NoRet")

		c.emitReverse(len(callArgs))

		hash, err := util.Uint160DecodeBytesBE([]byte(arg0Str))
		if err != nil {
			c.prog.Err = fmt.Errorf("bad callt hash: %w", err)
			return
		}

		tv = c.typeAndValueOf(expr.Args[1])
		if tv.Value == nil || !isString(tv.Type) {
			c.prog.Err = errors.New("bad callt method")
			return
		}
		method := constant.StringVal(tv.Value)

		tv = c.typeAndValueOf(expr.Args[2])
		if tv.Value == nil || !isNumber(tv.Type) {
			c.prog.Err = errors.New("bad callt call flags")
			return
		}
		flag, ok := constant.Uint64Val(tv.Value)
		if !ok || flag > 255 {
			c.prog.Err = errors.New("invalid callt flag")
			return
		}

		c.appendInvokedContract(hash, method, flag)

		tokNum, err := c.getCallToken(hash, method, len(callArgs), hasRet, callflag.CallFlag(flag))
		if err != nil {
			c.prog.Err = err
			return
		}
		tokBuf := make([]byte, 2)
		binary.LittleEndian.PutUint16(tokBuf, tokNum)
		emit.Instruction(c.prog.BinWriter, opcode.CALLT, tokBuf)
	} else {
		op, err := opcode.FromString(arg0Str)
		if err != nil {
			c.prog.Err = fmt.Errorf("invalid opcode: %s", op)
			return
		}
		emit.Opcodes(c.prog.BinWriter, op)
	}
}

// emitSliceHelper emits 3 items on stack: slice, its first index, and its size.
func (c *codegen) emitSliceHelper(e ast.Expr) {
	if !isByteSlice(c.typeOf(e)) {
		c.prog.Err = fmt.Errorf("copy is supported only for byte-slices")
		return
	}
	var hasLowIndex bool
	switch src := e.(type) {
	case *ast.SliceExpr:
		ast.Walk(c, src.X)
		if src.High != nil {
			ast.Walk(c, src.High)
		} else {
			emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.SIZE)
		}
		if src.Low != nil {
			ast.Walk(c, src.Low)
			hasLowIndex = true
		} else {
			emit.Int(c.prog.BinWriter, 0)
		}
	default:
		ast.Walk(c, src)
		emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.SIZE)
		emit.Int(c.prog.BinWriter, 0)
	}
	if !hasLowIndex {
		emit.Opcodes(c.prog.BinWriter, opcode.SWAP)
	} else {
		emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.ROT, opcode.SWAP, opcode.SUB)
	}
}

func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
	var name string
	switch t := expr.Fun.(type) {
	case *ast.Ident:
		name = t.Name
	case *ast.SelectorExpr:
		name = t.Sel.Name
	}

	switch name {
	case "copy":
		// stack for MEMCPY is: dst, dst_index, src, src_index, count
		c.emitSliceHelper(expr.Args[0])
		c.emitSliceHelper(expr.Args[1])
		emit.Int(c.prog.BinWriter, 3)
		emit.Opcodes(c.prog.BinWriter, opcode.ROLL, opcode.MIN)
		if !c.scope.voidCalls[expr] {
			// insert top item to the bottom of MEMCPY args, so that it is left on stack
			emit.Opcodes(c.prog.BinWriter, opcode.DUP)
			emit.Int(c.prog.BinWriter, 6)
			emit.Opcodes(c.prog.BinWriter, opcode.REVERSEN)
			emit.Int(c.prog.BinWriter, 5)
			emit.Opcodes(c.prog.BinWriter, opcode.REVERSEN)
		}
		emit.Opcodes(c.prog.BinWriter, opcode.MEMCPY)
	case "make":
		typ := c.typeOf(expr.Args[0])
		switch {
		case isMap(typ):
			emit.Opcodes(c.prog.BinWriter, opcode.NEWMAP)
		default:
			if len(expr.Args) == 3 {
				c.prog.Err = fmt.Errorf("`make()` with a capacity argument is not supported")
				return
			}
			ast.Walk(c, expr.Args[1])
			if isByteSlice(typ) {
				emit.Opcodes(c.prog.BinWriter, opcode.NEWBUFFER)
			} else {
				neoT := toNeoType(typ.(*types.Slice).Elem())
				emit.Instruction(c.prog.BinWriter, opcode.NEWARRAYT, []byte{byte(neoT)})
			}
		}
	case "len":
		emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.ISNULL)
		emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 1 + 2})
		emit.Opcodes(c.prog.BinWriter, opcode.SIZE)
		emit.Instruction(c.prog.BinWriter, opcode.JMP, []byte{2 + 1 + 1})
		emit.Opcodes(c.prog.BinWriter, opcode.DROP, opcode.PUSH0)
	case "append":
		arg := expr.Args[0]
		typ := c.typeOf(arg)
		ast.Walk(c, arg)
		emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.ISNULL)
		if isByteSlice(typ) {
			emit.Instruction(c.prog.BinWriter, opcode.JMPIFNOT, []byte{2 + 3})
			emit.Opcodes(c.prog.BinWriter, opcode.DROP, opcode.PUSHDATA1, 0)
			if expr.Ellipsis.IsValid() {
				ast.Walk(c, expr.Args[1])
			} else {
				c.convertByteArray(expr.Args[1:])
			}
			emit.Opcodes(c.prog.BinWriter, opcode.CAT)
		} else {
			emit.Instruction(c.prog.BinWriter, opcode.JMPIFNOT, []byte{2 + 2})
			emit.Opcodes(c.prog.BinWriter, opcode.DROP, opcode.NEWARRAY0)
			if expr.Ellipsis.IsValid() {
				ast.Walk(c, expr.Args[1])                    // x y
				emit.Opcodes(c.prog.BinWriter, opcode.PUSH0) // x y cnt=0
				start := c.newLabel()
				c.setLabel(start)
				emit.Opcodes(c.prog.BinWriter, opcode.PUSH2, opcode.PICK) // x y cnt x
				emit.Opcodes(c.prog.BinWriter, opcode.PUSH2, opcode.PICK) // x y cnt x y
				emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.SIZE)   // x y cnt x y len(y)
				emit.Opcodes(c.prog.BinWriter, opcode.PUSH3, opcode.PICK) // x y cnt x y len(y) cnt
				after := c.newLabel()
				emit.Jmp(c.prog.BinWriter, opcode.JMPEQL, after)          // x y cnt x y
				emit.Opcodes(c.prog.BinWriter, opcode.PUSH2, opcode.PICK, // x y cnt x y cnt
					opcode.PICKITEM, // x y cnt x y[cnt]
					opcode.APPEND,   // x=append(x, y[cnt]) y cnt
					opcode.INC)      // x y cnt+1
				emit.Jmp(c.prog.BinWriter, opcode.JMPL, start)
				c.setLabel(after)
				for range 4 { // leave x on stack
					emit.Opcodes(c.prog.BinWriter, opcode.DROP)
				}
			} else {
				for _, e := range expr.Args[1:] {
					emit.Opcodes(c.prog.BinWriter, opcode.DUP)
					ast.Walk(c, e)
					emit.Opcodes(c.prog.BinWriter, opcode.APPEND)
				}
			}
		}
	case "panic":
		emit.Opcodes(c.prog.BinWriter, opcode.THROW)
	case "recover":
		if !c.scope.voidCalls[expr] {
			c.emitLoadByIndex(varGlobal, c.exceptionIndex)
		}
		emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL)
		c.emitStoreByIndex(varGlobal, c.exceptionIndex)
	case "delete":
		emit.Opcodes(c.prog.BinWriter, opcode.REMOVE)
	case "ToHash160":
		// We can be sure that this is an ast.BasicLit just containing a simple
		// address string. Note that the string returned from calling Value will
		// contain double quotes that need to be stripped.
		addressStr := expr.Args[0].(*ast.BasicLit).Value
		addressStr = strings.Replace(addressStr, "\"", "", 2)
		uint160, err := address.StringToUint160(addressStr)
		if err != nil {
			c.prog.Err = err
			return
		}
		bytes := uint160.BytesBE()
		emit.Bytes(c.prog.BinWriter, bytes)
		c.emitConvert(stackitem.BufferT)
	case "min":
		for i, arg := range expr.Args {
			typ, ok := c.typeOf(arg).(*types.Basic)
			if !ok {
				c.prog.Err = fmt.Errorf("only integer types are supported by min")
				return
			}
			switch typ.Kind() {
			case types.Int, types.Int8, types.Int16, types.Int32, types.Int64,
				types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64,
				types.UntypedInt:
			default:
				c.prog.Err = fmt.Errorf("only integer types are supported by min, "+
					"got %s at %d", typ, i+1)
				return
			}
		}
		for range len(expr.Args) - 1 {
			emit.Opcodes(c.prog.BinWriter, opcode.MIN)
		}
	case "max":
		for i, arg := range expr.Args {
			typ, ok := c.typeOf(arg).(*types.Basic)
			if !ok {
				c.prog.Err = fmt.Errorf("only integer types are supported by max")
				return
			}
			switch typ.Kind() {
			case types.Int, types.Int8, types.Int16, types.Int32, types.Int64,
				types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64,
				types.UntypedInt:
			default:
				c.prog.Err = fmt.Errorf("only integer types are supported by max, "+
					"got %s at %d", typ, i+1)
				return
			}
		}
		for range len(expr.Args) - 1 {
			emit.Opcodes(c.prog.BinWriter, opcode.MAX)
		}
	case "clear":
		switch typ := c.typeOf(expr.Args[0]).(type) {
		case *types.Map:
			emit.Opcodes(c.prog.BinWriter, opcode.CLEARITEMS)
		case *types.Slice:
			emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.SIZE, opcode.OVER,
				opcode.CLEARITEMS,
			)
			clearItemsBegin := c.newLabel()
			c.setLabel(clearItemsBegin)
			emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.PUSH0)
			clearItemsEnd := c.newLabel()
			emit.Jmp(c.prog.BinWriter, opcode.JMPLEL, clearItemsEnd)
			emit.Opcodes(c.prog.BinWriter, opcode.DEC, opcode.OVER)
			c.emitDefault(typ.Elem())
			emit.Opcodes(c.prog.BinWriter, opcode.APPEND)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, clearItemsBegin)
			c.setLabel(clearItemsEnd)
			emit.Opcodes(c.prog.BinWriter, opcode.DROP, opcode.DROP)
		default:
			c.prog.Err = fmt.Errorf("only map or slice are supported by clear, got: %s", typ)
			return
		}
	}
}

// transformArgs returns a list of function arguments
// which should be put on stack.
// There are special cases for builtins:
//  1. With ToHash160 in case if it behaves like builtin,
//     parameter conversion is happening at compile-time so there is no need to
//     push parameters on stack and perform an actual call
//  2. With panic, the generated code depends on the fact if an argument was nil or a string;
//     so, it should be handled accordingly.
func transformArgs(fs *funcScope, fun ast.Expr, isBuiltin bool, args []ast.Expr) []ast.Expr {
	switch f := fun.(type) {
	case *ast.SelectorExpr:
		if isBuiltin && f.Sel.Name == "ToHash160" {
			return args[1:]
		}
		if fs != nil && isSyscall(fs) {
			return nil
		}
	case *ast.Ident:
		switch f.Name {
		case "make", "copy", "append":
			return nil
		}
	}

	return args
}

// emitConvert converts the top stack item to the specified type.
func (c *codegen) emitConvert(typ stackitem.Type) {
	if typ == stackitem.BooleanT {
		// DUP + ISTYPE + JMPIF costs 3 already with CONVERT of a cost 8192.
		// NOT+NOT at the same time costs 4 and always works (and is shorter).
		emit.Opcodes(c.prog.BinWriter, opcode.NOT, opcode.NOT)
	} else {
		emit.Opcodes(c.prog.BinWriter, opcode.DUP)
		emit.Instruction(c.prog.BinWriter, opcode.ISTYPE, []byte{byte(typ)})
		emit.Instruction(c.prog.BinWriter, opcode.JMPIF, []byte{2 + 2}) // After CONVERT.
		emit.Instruction(c.prog.BinWriter, opcode.CONVERT, []byte{byte(typ)})
	}
}

func (c *codegen) checkGetMapValueWithOKFlag(expr ast.Expr) bool {
	idxExpr, ok := expr.(*ast.IndexExpr)
	if !ok {
		return false
	}
	_, ok = c.typeOf(idxExpr.X).Underlying().(*types.Map)
	return ok
}

func (c *codegen) emitGetMapValueWithOKFlag(expr ast.Expr) {
	var (
		idxExpr = expr.(*ast.IndexExpr)
		typ     = c.typeOf(idxExpr.X).Underlying().(*types.Map)
	)
	ast.Walk(c, idxExpr.X)
	emit.Opcodes(c.prog.BinWriter, opcode.DUP)
	ast.Walk(c, idxExpr.Index)
	emit.Opcodes(c.prog.BinWriter, opcode.DUP, opcode.ROT,
		opcode.SWAP, opcode.HASKEY,
	)
	lHasKey := c.newLabel()
	emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, lHasKey)
	emit.Opcodes(c.prog.BinWriter, opcode.DROP, opcode.DROP,
		opcode.PUSHF,
	)
	c.emitDefault(typ.Elem())
	emit.Instruction(c.prog.BinWriter, opcode.JMP, []byte{2 + 1 + 1 + 1})
	c.setLabel(lHasKey)
	emit.Opcodes(c.prog.BinWriter, opcode.PICKITEM, opcode.PUSHT,
		opcode.SWAP,
	)
}

func (c *codegen) convertByteArray(elems []ast.Expr) {
	buf := make([]byte, len(elems))
	varIndices := []int{}
	for i := range elems {
		t := c.typeAndValueOf(elems[i])
		if t.Value != nil {
			val, _ := constant.Int64Val(t.Value)
			buf[i] = byte(val)
		} else {
			varIndices = append(varIndices, i)
		}
	}
	emit.Bytes(c.prog.BinWriter, buf)
	c.emitConvert(stackitem.BufferT)
	for _, i := range varIndices {
		emit.Opcodes(c.prog.BinWriter, opcode.DUP)
		emit.Int(c.prog.BinWriter, int64(i))
		ast.Walk(c, elems[i])
		emit.Opcodes(c.prog.BinWriter, opcode.SETITEM)
	}
}

func (c *codegen) convertMap(lit *ast.CompositeLit) {
	l := len(lit.Elts)
	for i := l - 1; i >= 0; i-- {
		elem := lit.Elts[i].(*ast.KeyValueExpr)
		ast.Walk(c, elem.Value)
		ast.Walk(c, elem.Key)
	}
	emit.Int(c.prog.BinWriter, int64(l))
	emit.Opcodes(c.prog.BinWriter, opcode.PACKMAP)
}

func (c *codegen) getStruct(typ types.Type) (*types.Struct, bool) {
	switch t := typ.Underlying().(type) {
	case *types.Struct:
		return t, true
	case *types.Pointer:
		strct, ok := t.Elem().Underlying().(*types.Struct)
		return strct, ok
	default:
		return nil, false
	}
}

func (c *codegen) convertStruct(lit *ast.CompositeLit, ptr bool) {
	// Create a new structScope to initialize and store
	// the positions of its variables.
	strct, ok := c.typeOf(lit).Underlying().(*types.Struct)
	if !ok {
		c.prog.Err = fmt.Errorf("the given literal is not of type struct: %v", lit)
		return
	}

	keyedLit := len(lit.Elts) > 0
	if keyedLit {
		_, ok := lit.Elts[0].(*ast.KeyValueExpr)
		keyedLit = keyedLit && ok
	}
	// We need to locally store all the fields, even if they are not initialized.
	// We will initialize all fields to their "zero" value.
	for i := strct.NumFields() - 1; i >= 0; i-- {
		sField := strct.Field(i)
		var initialized bool

		if !keyedLit {
			if len(lit.Elts) > i {
				ast.Walk(c, lit.Elts[i])
				initialized = true
			}
		} else {
			// Fields initialized by the program.
			for _, field := range lit.Elts {
				f := field.(*ast.KeyValueExpr)
				fieldName := f.Key.(*ast.Ident).Name

				if sField.Name() == fieldName {
					ast.Walk(c, f.Value)
					initialized = true
					break
				}
			}
		}
		if !initialized {
			c.emitDefault(sField.Type())
		}
	}
	emit.Int(c.prog.BinWriter, int64(strct.NumFields()))
	if ptr {
		emit.Opcodes(c.prog.BinWriter, opcode.PACK)
	} else {
		emit.Opcodes(c.prog.BinWriter, opcode.PACKSTRUCT)
	}
}

func (c *codegen) emitToken(tok token.Token, typ types.Type) {
	op, err := convertToken(tok, typ)
	if err != nil {
		c.prog.Err = err
		return
	}
	emit.Opcodes(c.prog.BinWriter, op)
}

func convertToken(tok token.Token, typ types.Type) (opcode.Opcode, error) {
	switch tok {
	case token.ADD_ASSIGN, token.ADD:
		// VM has separate opcodes for number and string concatenation
		if isString(typ) {
			return opcode.CAT, nil
		}
		return opcode.ADD, nil
	case token.SUB_ASSIGN:
		return opcode.SUB, nil
	case token.MUL_ASSIGN:
		return opcode.MUL, nil
	case token.QUO_ASSIGN:
		return opcode.DIV, nil
	case token.REM_ASSIGN:
		return opcode.MOD, nil
	case token.SUB:
		return opcode.SUB, nil
	case token.MUL:
		return opcode.MUL, nil
	case token.QUO:
		return opcode.DIV, nil
	case token.REM:
		return opcode.MOD, nil
	case token.LSS:
		return opcode.LT, nil
	case token.LEQ:
		return opcode.LE, nil
	case token.GTR:
		return opcode.GT, nil
	case token.GEQ:
		return opcode.GE, nil
	case token.EQL:
		// VM has separate opcodes for number and string equality
		if isNumber(typ) {
			return opcode.NUMEQUAL, nil
		}
		return opcode.EQUAL, nil
	case token.NEQ:
		// VM has separate opcodes for number and string equality
		if isNumber(typ) {
			return opcode.NUMNOTEQUAL, nil
		}
		return opcode.NOTEQUAL, nil
	case token.DEC:
		return opcode.DEC, nil
	case token.INC:
		return opcode.INC, nil
	case token.NOT:
		return opcode.NOT, nil
	case token.AND, token.AND_ASSIGN:
		return opcode.AND, nil
	case token.OR, token.OR_ASSIGN:
		return opcode.OR, nil
	case token.SHL, token.SHL_ASSIGN:
		return opcode.SHL, nil
	case token.SHR, token.SHR_ASSIGN:
		return opcode.SHR, nil
	case token.XOR:
		return opcode.XOR, nil
	default:
		return 0, fmt.Errorf("compiler could not convert token: %s", tok)
	}
}

func (c *codegen) newFunc(decl *ast.FuncDecl) *funcScope {
	f := c.newFuncScope(decl, c.newLabel())
	c.funcs[c.getFuncNameFromDecl("", decl)] = f
	return f
}

func (c *codegen) getFuncFromIdent(fun *ast.Ident) (*funcScope, bool) {
	var pkgName string
	if len(c.pkgInfoInline) != 0 {
		pkgName = c.pkgInfoInline[len(c.pkgInfoInline)-1].PkgPath
	}

	f, ok := c.funcs[c.getIdentName(pkgName, fun.Name)]
	return f, ok
}

// getFuncNameFromSelector returns fully-qualified function name from the selector expression.
// Second return value is true iff this was a method call, not foreign package call.
func (c *codegen) getFuncNameFromSelector(e *ast.SelectorExpr) (string, bool) {
	if c.typeInfo.Selections[e] != nil {
		typ := c.typeInfo.Types[e.X].Type.String()
		name := c.getIdentName(typ, e.Sel.Name)
		if name[0] == '*' {
			name = name[1:]
		}
		return name, true
	}

	ident := e.X.(*ast.Ident)
	return c.getIdentName(ident.Name, e.Sel.Name), false
}

func (c *codegen) newLambda(u uint16, lit *ast.FuncLit) {
	name := fmt.Sprintf("lambda@%d", u)
	f := c.newFuncScope(&ast.FuncDecl{
		Name: ast.NewIdent(name),
		Type: lit.Type,
		Body: lit.Body,
	}, u)
	c.lambda[c.getFuncNameFromDecl("", f.decl)] = f
}

func (c *codegen) compile(info *buildInfo, pkg *packages.Package) error {
	c.mainPkg = pkg
	c.analyzePkgOrder()
	if c.prog.Err != nil {
		return c.prog.Err
	}
	c.fillDocumentInfo()
	funUsage := c.analyzeFuncAndGlobalVarUsage()
	if c.prog.Err != nil {
		return c.prog.Err
	}

	// Bring all imported functions into scope.
	c.ForEachFile(c.resolveFuncDecls)

	hasDeploy := c.traverseGlobals()

	if hasDeploy {
		deployOffset := c.prog.Len()
		emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{0, 2})
		locCount := c.convertDeployFuncs()
		c.reverseOffsetMap[deployOffset] = nameWithLocals{
			name:  "_deploy",
			count: locCount,
		}
		c.deployEndOffset = c.prog.Len()
		emit.Opcodes(c.prog.BinWriter, opcode.RET)
	}

	// sort map keys to generate code deterministically.
	keys := make([]*types.Package, 0, len(info.program))
	for _, p := range info.program {
		keys = append(keys, p.Types)
	}
	slices.SortFunc(keys, func(a, b *types.Package) int { return cmp.Compare(a.Path(), b.Path()) })

	// Generate the code for the program.
	c.ForEachFile(func(f *ast.File, pkg *types.Package) {
		for _, decl := range f.Decls {
			switch n := decl.(type) {
			case *ast.FuncDecl:
				// Don't convert the function if it's not used. This will save a lot
				// of bytecode space.
				pkgPath := ""
				if pkg != c.mainPkg.Types { // not a main package
					pkgPath = pkg.Path()
				}
				name := c.getFuncNameFromDecl(pkgPath, n)
				if !isInitFunc(n) && !isDeployFunc(n) && funUsage.funcUsed(name) &&
					(!isInteropPath(pkg.Path()) && !canInline(pkg.Path(), n.Name.Name, false)) {
					c.convertFuncDecl(f, n, pkg)
				}
			}
		}
	})

	return c.prog.Err
}

func newCodegen(info *buildInfo, pkg *packages.Package) *codegen {
	return &codegen{
		buildInfo:        info,
		prog:             io.NewBufBinWriter(),
		l:                []int{},
		funcs:            map[string]*funcScope{},
		lambda:           map[string]*funcScope{},
		reverseOffsetMap: map[int]nameWithLocals{},
		globals:          map[string]int{},
		labels:           map[labelWithType]uint16{},
		typeInfo:         pkg.TypesInfo,
		constMap:         map[string]types.TypeAndValue{},
		docIndex:         map[string]int{},
		packageCache:     map[string]*packages.Package{},

		initEndOffset:   -1,
		deployEndOffset: -1,

		emittedEvents:    make(map[string][]EmittedEventInfo),
		invokedContracts: make(map[util.Uint160][]string),
		sequencePoints:   make(map[string][]DebugSeqPoint),
	}
}

// codeGen compiles the program to bytecode.
func codeGen(info *buildInfo) (*nef.File, *DebugInfo, error) {
	if len(info.program) == 0 {
		return nil, nil, errors.New("empty package")
	}
	pkg := info.program[0]
	c := newCodegen(info, pkg)

	if err := c.compile(info, pkg); err != nil {
		return nil, nil, err
	}

	buf, err := c.writeJumps(c.prog.Bytes())
	if err != nil {
		return nil, nil, err
	}

	methods := bitfield.New(len(buf))
	di := c.emitDebugInfo(buf)
	for i := range di.Methods {
		methods.Set(int(di.Methods[i].Range.Start))
	}
	f, err := nef.NewFile(buf)
	if err != nil {
		return nil, nil, fmt.Errorf("error while trying to create .nef file: %w", err)
	}
	if c.callTokens != nil {
		f.Tokens = c.callTokens
	}
	if info.options != nil && info.options.SourceURL != "" {
		if len(info.options.SourceURL) > nef.MaxSourceURLLength {
			return nil, nil, errors.New("too long source URL")
		}
		f.Source = info.options.SourceURL
	}
	f.Checksum = f.CalculateChecksum()
	return f, di, scparser.IsScriptCorrect(buf, methods)
}

func (c *codegen) resolveFuncDecls(f *ast.File, pkg *types.Package) {
	for _, decl := range f.Decls {
		switch n := decl.(type) {
		case *ast.FuncDecl:
			fs := c.newFunc(n)
			fs.file = f
		}
	}
}

func (c *codegen) writeJumps(b []byte) ([]byte, error) {
	ctx := scparser.NewContext(b, 0)
	var nopOffsets []int
	for op, param, err := ctx.Next(); err == nil && ctx.IP() < len(b); op, param, err = ctx.Next() {
		switch op {
		case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
			opcode.JMPEQ, opcode.JMPNE,
			opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT:
		case opcode.TRYL:
			_, err := c.replaceLabelWithOffset(ctx.IP(), param)
			if err != nil {
				return nil, err
			}
			_, err = c.replaceLabelWithOffset(ctx.IP(), param[4:])
			if err != nil {
				return nil, err
			}
		case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
			opcode.JMPEQL, opcode.JMPNEL,
			opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
			opcode.CALLL, opcode.PUSHA, opcode.ENDTRYL:
			offset, err := c.replaceLabelWithOffset(ctx.IP(), param)
			if err != nil {
				return nil, err
			}
			if op != opcode.PUSHA && math.MinInt8 <= offset && offset <= math.MaxInt8 {
				if op == opcode.JMPL && offset == 5 {
					copy(b[ctx.IP():], []byte{byte(opcode.NOP), byte(opcode.NOP), byte(opcode.NOP), byte(opcode.NOP), byte(opcode.NOP)})
					nopOffsets = append(nopOffsets, ctx.IP(), ctx.IP()+1, ctx.IP()+2, ctx.IP()+3, ctx.IP()+4)
				} else {
					copy(b[ctx.IP():], []byte{byte(toShortForm(op)), byte(offset), byte(opcode.NOP), byte(opcode.NOP), byte(opcode.NOP)})
					nopOffsets = append(nopOffsets, ctx.IP()+2, ctx.IP()+3, ctx.IP()+4)
				}
			}
		case opcode.INITSLOT:
			nextIP := ctx.NextIP()
			info := c.reverseOffsetMap[ctx.IP()]
			if argCount := b[nextIP-1]; info.count == 0 && argCount == 0 {
				copy(b[ctx.IP():], []byte{byte(opcode.NOP), byte(opcode.NOP), byte(opcode.NOP)})
				nopOffsets = append(nopOffsets, ctx.IP(), ctx.IP()+1, ctx.IP()+2)
				continue
			}

			if info.count > 255 {
				return nil, fmt.Errorf("func '%s' has %d local variables (maximum is 255)", info.name, info.count)
			}
			b[nextIP-2] = byte(info.count)
		default:
		}
	}

	if c.deployEndOffset >= 0 {
		_, end := correctRange(uint16(c.initEndOffset+1), uint16(c.deployEndOffset), nopOffsets)
		c.deployEndOffset = int(end)
	}
	if c.initEndOffset > 0 {
		_, end := correctRange(0, uint16(c.initEndOffset), nopOffsets)
		c.initEndOffset = int(end)
	}

	// Correct function ip range.
	// Note: indices are sorted in increasing order.
	for _, f := range c.funcs {
		f.rng.Start, f.rng.End = correctRange(f.rng.Start, f.rng.End, nopOffsets)
	}
	return removeNOPs(b, nopOffsets, c.sequencePoints), nil
}

func correctRange(start, end uint16, offsets []int) (uint16, uint16) {
	newStart, newEnd := start, end
loop:
	for _, ind := range offsets {
		switch {
		case ind > int(end):
			break loop
		case ind < int(start):
			newStart--
			newEnd--
		case ind >= int(start):
			newEnd--
		}
	}
	return newStart, newEnd
}

func (c *codegen) replaceLabelWithOffset(ip int, arg []byte) (int, error) {
	index := binary.LittleEndian.Uint16(arg)
	if int(index) > len(c.l) {
		return 0, fmt.Errorf("unexpected label number: %d (max %d)", index, len(c.l))
	}
	if c.l[index] < 0 {
		return 0, fmt.Errorf("invalid label target: %d at %d", c.l[index], ip)
	}
	offset := c.l[index] - ip
	if offset > math.MaxInt32 || offset < math.MinInt32 {
		return 0, fmt.Errorf("label offset is too big at the instruction %d: %d (max %d, min %d)",
			ip, offset, math.MaxInt32, math.MinInt32)
	}
	binary.LittleEndian.PutUint32(arg, uint32(offset))
	return offset, nil
}

// removeNOPs converts b to a program where all long JMP*/CALL* specified by absolute offsets
// are replaced with their corresponding short counterparts. It panics if either b or offsets are invalid.
// This is done in 2 passes:
// 1. Alter jump offsets taking into account parts to be removed.
// 2. Perform actual removal of jump targets.
// 3. Reevaluate debug sequence points offsets.
// Note: after jump offsets altering, there can appear new candidates for conversion.
// These are ignored for now.
func removeNOPs(b []byte, nopOffsets []int, sequencePoints map[string][]DebugSeqPoint) []byte {
	if len(nopOffsets) == 0 {
		return b
	}
	for i := range nopOffsets {
		if b[nopOffsets[i]] != byte(opcode.NOP) {
			panic("NOP offset is invalid")
		}
	}

	// 1. Alter existing jump offsets.
	ctx := scparser.NewContext(b, 0)
	for op, _, err := ctx.Next(); err == nil && ctx.IP() < len(b); op, _, err = ctx.Next() {
		// we can't use arg returned by ctx.Next() because it is copied
		nextIP := ctx.NextIP()
		ip := ctx.IP()
		switch op {
		case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
			opcode.JMPEQ, opcode.JMPNE,
			opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT, opcode.ENDTRY:
			offset := int(int8(b[nextIP-1]))
			offset += calcOffsetCorrection(ip, ip+offset, nopOffsets)
			b[nextIP-1] = byte(offset)
		case opcode.TRY:
			catchOffset := int(int8(b[nextIP-2]))
			catchOffset += calcOffsetCorrection(ip, ip+catchOffset, nopOffsets)
			b[nextIP-1] = byte(catchOffset)
			finallyOffset := int(int8(b[nextIP-1]))
			finallyOffset += calcOffsetCorrection(ip, ip+finallyOffset, nopOffsets)
			b[nextIP-1] = byte(finallyOffset)
		case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
			opcode.JMPEQL, opcode.JMPNEL,
			opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
			opcode.CALLL, opcode.PUSHA, opcode.ENDTRYL:
			arg := b[nextIP-4:]
			offset := int(int32(binary.LittleEndian.Uint32(arg)))
			offset += calcOffsetCorrection(ip, ip+offset, nopOffsets)
			binary.LittleEndian.PutUint32(arg, uint32(offset))
		case opcode.TRYL:
			arg := b[nextIP-8:]
			catchOffset := int(int32(binary.LittleEndian.Uint32(arg)))
			catchOffset += calcOffsetCorrection(ip, ip+catchOffset, nopOffsets)
			binary.LittleEndian.PutUint32(arg, uint32(catchOffset))
			arg = b[nextIP-4:]
			finallyOffset := int(int32(binary.LittleEndian.Uint32(arg)))
			finallyOffset += calcOffsetCorrection(ip, ip+finallyOffset, nopOffsets)
			binary.LittleEndian.PutUint32(arg, uint32(finallyOffset))
		default:
		}
	}

	// 2. Convert instructions.
	copyOffset := 0
	l := len(nopOffsets)
	for i := range l {
		start := nopOffsets[i]
		end := len(b)
		if i != l-1 {
			end = nopOffsets[i+1]
		}
		copy(b[start-copyOffset:], b[start+1:end])
		copyOffset++
	}

	// 3. Reevaluate debug sequence points.
	for _, seqPoints := range sequencePoints {
		for i := range seqPoints {
			diff := 0
			for _, offset := range nopOffsets {
				if offset < seqPoints[i].Opcode {
					diff++
				}
			}
			seqPoints[i].Opcode -= diff
		}
	}

	return b[:len(b)-copyOffset]
}

func calcOffsetCorrection(ip, target int, offsets []int) int {
	cnt := 0
	start, _ := slices.BinarySearch(offsets, min(ip, target))
	for i := start; i < len(offsets) && (offsets[i] < target || offsets[i] <= ip); i++ {
		ind := offsets[i]
		if ip <= ind && ind < target || target <= ind && ind < ip {
			cnt++
		}
	}
	if ip < target {
		return -cnt
	}
	return cnt
}

func negateJmp(op opcode.Opcode) opcode.Opcode {
	switch op {
	case opcode.JMPIFL:
		return opcode.JMPIFNOTL
	case opcode.JMPIFNOTL:
		return opcode.JMPIFL
	case opcode.JMPEQL:
		return opcode.JMPNEL
	case opcode.JMPNEL:
		return opcode.JMPEQL
	case opcode.JMPGTL:
		return opcode.JMPLEL
	case opcode.JMPGEL:
		return opcode.JMPLTL
	case opcode.JMPLEL:
		return opcode.JMPGTL
	case opcode.JMPLTL:
		return opcode.JMPGEL
	default:
		panic(fmt.Errorf("invalid opcode in negateJmp: %s", op))
	}
}

func toShortForm(op opcode.Opcode) opcode.Opcode {
	switch op {
	case opcode.JMPL:
		return opcode.JMP
	case opcode.JMPIFL:
		return opcode.JMPIF
	case opcode.JMPIFNOTL:
		return opcode.JMPIFNOT
	case opcode.JMPEQL:
		return opcode.JMPEQ
	case opcode.JMPNEL:
		return opcode.JMPNE
	case opcode.JMPGTL:
		return opcode.JMPGT
	case opcode.JMPGEL:
		return opcode.JMPGE
	case opcode.JMPLEL:
		return opcode.JMPLE
	case opcode.JMPLTL:
		return opcode.JMPLT
	case opcode.CALLL:
		return opcode.CALL
	case opcode.ENDTRYL:
		return opcode.ENDTRY
	default:
		panic(fmt.Errorf("invalid opcode: %s", op))
	}
}
