mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-01 23:45:50 +00:00
Merge pull request #1343 from nspcc-dev/compiler/recover
Support `defer` statement
This commit is contained in:
commit
3468f97836
12 changed files with 384 additions and 63 deletions
|
@ -19,11 +19,11 @@ a dialect of Go rather than a complete port of the language:
|
||||||
* goroutines, channels and garbage collection are not supported and will
|
* goroutines, channels and garbage collection are not supported and will
|
||||||
never be because emulating that aspects of Go runtime on top of Neo VM is
|
never be because emulating that aspects of Go runtime on top of Neo VM is
|
||||||
close to impossible
|
close to impossible
|
||||||
* even though `panic()` is supported, `recover()` is not, `panic` shuts the
|
* `defer` and `recover` are supported except for cases where panic occurs in
|
||||||
VM down
|
`return` statement, because this complicates implementation and imposes runtime
|
||||||
* lambdas are not supported (#939)
|
overhead for all contracts. This can easily be mitigated by first storing values
|
||||||
* it's not possible to rename imported interop packages, they won't work this
|
in variables and returning the result.
|
||||||
way (#397, #913)
|
* lambdas are supported, but closures are not.
|
||||||
|
|
||||||
## VM API (interop layer)
|
## VM API (interop layer)
|
||||||
Compiler translates interop function calls into NEO VM syscalls or (for custom
|
Compiler translates interop function calls into NEO VM syscalls or (for custom
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Go language builtin functions.
|
// Go language builtin functions.
|
||||||
goBuiltins = []string{"len", "append", "panic", "make", "copy"}
|
goBuiltins = []string{"len", "append", "panic", "make", "copy", "recover"}
|
||||||
// Custom builtin utility functions.
|
// Custom builtin utility functions.
|
||||||
customBuiltins = []string{
|
customBuiltins = []string{
|
||||||
"FromAddress", "Equals",
|
"FromAddress", "Equals",
|
||||||
|
@ -40,23 +40,30 @@ func (c *codegen) getIdentName(pkg string, name string) string {
|
||||||
// and returns number of variables initialized and
|
// and returns number of variables initialized and
|
||||||
// true if any init functions were encountered.
|
// true if any init functions were encountered.
|
||||||
func (c *codegen) traverseGlobals() (int, bool) {
|
func (c *codegen) traverseGlobals() (int, bool) {
|
||||||
|
var hasDefer bool
|
||||||
var n int
|
var n int
|
||||||
var hasInit bool
|
var hasInit bool
|
||||||
c.ForEachFile(func(f *ast.File, _ *types.Package) {
|
c.ForEachFile(func(f *ast.File, _ *types.Package) {
|
||||||
n += countGlobals(f)
|
n += countGlobals(f)
|
||||||
if !hasInit {
|
if !hasInit || !hasDefer {
|
||||||
ast.Inspect(f, func(node ast.Node) bool {
|
ast.Inspect(f, func(node ast.Node) bool {
|
||||||
n, ok := node.(*ast.FuncDecl)
|
switch n := node.(type) {
|
||||||
if ok {
|
case *ast.FuncDecl:
|
||||||
if isInitFunc(n) {
|
if isInitFunc(n) {
|
||||||
hasInit = true
|
hasInit = true
|
||||||
}
|
}
|
||||||
|
return !hasDefer
|
||||||
|
case *ast.DeferStmt:
|
||||||
|
hasDefer = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if hasDefer {
|
||||||
|
n++
|
||||||
|
}
|
||||||
if n != 0 || hasInit {
|
if n != 0 || hasInit {
|
||||||
if n > 255 {
|
if n > 255 {
|
||||||
c.prog.BinWriter.Err = errors.New("too many global variables")
|
c.prog.BinWriter.Err = errors.New("too many global variables")
|
||||||
|
@ -83,6 +90,11 @@ func (c *codegen) traverseGlobals() (int, bool) {
|
||||||
// encountered after will be recognized as globals.
|
// encountered after will be recognized as globals.
|
||||||
c.scope = nil
|
c.scope = nil
|
||||||
})
|
})
|
||||||
|
// store auxiliary variables after all others.
|
||||||
|
if hasDefer {
|
||||||
|
c.exceptionIndex = len(c.globals)
|
||||||
|
c.globals["<exception>"] = c.exceptionIndex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return n, hasInit
|
return n, hasInit
|
||||||
}
|
}
|
||||||
|
@ -92,7 +104,7 @@ func (c *codegen) traverseGlobals() (int, bool) {
|
||||||
func countGlobals(f ast.Node) (i int) {
|
func countGlobals(f ast.Node) (i int) {
|
||||||
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.
|
// 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
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
@ -78,6 +77,9 @@ type codegen struct {
|
||||||
// packages contains packages in the order they were loaded.
|
// packages contains packages in the order they were loaded.
|
||||||
packages []string
|
packages []string
|
||||||
|
|
||||||
|
// exceptionIndex is the index of static slot where exception is stored.
|
||||||
|
exceptionIndex int
|
||||||
|
|
||||||
// documents contains paths to all files used by the program.
|
// documents contains paths to all files used by the program.
|
||||||
documents []string
|
documents []string
|
||||||
// docIndex maps file path to an index in documents array.
|
// docIndex maps file path to an index in documents array.
|
||||||
|
@ -217,6 +219,11 @@ func getBaseOpcode(t varType) (opcode.Opcode, opcode.Opcode) {
|
||||||
// emitLoadVar loads specified variable to the evaluation stack.
|
// emitLoadVar loads specified variable to the evaluation stack.
|
||||||
func (c *codegen) emitLoadVar(pkg string, name string) {
|
func (c *codegen) emitLoadVar(pkg string, name string) {
|
||||||
t, i := c.getVarIndex(pkg, name)
|
t, i := c.getVarIndex(pkg, name)
|
||||||
|
c.emitLoadByIndex(t, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitLoadByIndex loads specified variable type with index i.
|
||||||
|
func (c *codegen) emitLoadByIndex(t varType, i int) {
|
||||||
base, _ := getBaseOpcode(t)
|
base, _ := getBaseOpcode(t)
|
||||||
if i < 7 {
|
if i < 7 {
|
||||||
emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
|
emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
|
||||||
|
@ -232,6 +239,11 @@ func (c *codegen) emitStoreVar(pkg string, name string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t, i := c.getVarIndex(pkg, name)
|
t, i := c.getVarIndex(pkg, name)
|
||||||
|
c.emitStoreByIndex(t, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitLoadByIndex stores top value in the specified variable type with index i.
|
||||||
|
func (c *codegen) emitStoreByIndex(t varType, i int) {
|
||||||
_, base := getBaseOpcode(t)
|
_, base := getBaseOpcode(t)
|
||||||
if i < 7 {
|
if i < 7 {
|
||||||
emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
|
emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
|
||||||
|
@ -553,6 +565,8 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.processDefers()
|
||||||
|
|
||||||
c.saveSequencePoint(n)
|
c.saveSequencePoint(n)
|
||||||
emit.Opcode(c.prog.BinWriter, opcode.RET)
|
emit.Opcode(c.prog.BinWriter, opcode.RET)
|
||||||
return nil
|
return nil
|
||||||
|
@ -565,6 +579,9 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
lElse := c.newLabel()
|
lElse := c.newLabel()
|
||||||
lElseEnd := c.newLabel()
|
lElseEnd := c.newLabel()
|
||||||
|
|
||||||
|
if n.Init != nil {
|
||||||
|
ast.Walk(c, n.Init)
|
||||||
|
}
|
||||||
if n.Cond != nil {
|
if n.Cond != nil {
|
||||||
c.emitBoolExpr(n.Cond, true, false, lElse)
|
c.emitBoolExpr(n.Cond, true, false, lElse)
|
||||||
}
|
}
|
||||||
|
@ -708,6 +725,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
numArgs = len(n.Args)
|
numArgs = len(n.Args)
|
||||||
isBuiltin bool
|
isBuiltin bool
|
||||||
isFunc bool
|
isFunc bool
|
||||||
|
isLiteral bool
|
||||||
)
|
)
|
||||||
|
|
||||||
switch fun := n.Fun.(type) {
|
switch fun := n.Fun.(type) {
|
||||||
|
@ -746,6 +764,8 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
ast.Walk(c, n.Args[0])
|
ast.Walk(c, n.Args[0])
|
||||||
c.emitConvert(stackitem.BufferT)
|
c.emitConvert(stackitem.BufferT)
|
||||||
return nil
|
return nil
|
||||||
|
case *ast.FuncLit:
|
||||||
|
isLiteral = true
|
||||||
}
|
}
|
||||||
|
|
||||||
c.saveSequencePoint(n)
|
c.saveSequencePoint(n)
|
||||||
|
@ -798,6 +818,9 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
c.emitLoadVar("", name)
|
c.emitLoadVar("", name)
|
||||||
emit.Opcode(c.prog.BinWriter, opcode.CALLA)
|
emit.Opcode(c.prog.BinWriter, opcode.CALLA)
|
||||||
}
|
}
|
||||||
|
case isLiteral:
|
||||||
|
ast.Walk(c, n.Fun)
|
||||||
|
emit.Opcode(c.prog.BinWriter, opcode.CALLA)
|
||||||
case isSyscall(f):
|
case isSyscall(f):
|
||||||
c.convertSyscall(n, f.pkg.Name(), f.name)
|
c.convertSyscall(n, f.pkg.Name(), f.name)
|
||||||
default:
|
default:
|
||||||
|
@ -820,6 +843,20 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
|
c.scope.deferStack = append(c.scope.deferStack, deferInfo{
|
||||||
|
catchLabel: catch,
|
||||||
|
finallyLabel: finally,
|
||||||
|
expr: n.Call,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
|
||||||
case *ast.SelectorExpr:
|
case *ast.SelectorExpr:
|
||||||
typ := c.typeOf(n.X)
|
typ := c.typeOf(n.X)
|
||||||
if typ == nil {
|
if typ == nil {
|
||||||
|
@ -1081,6 +1118,51 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processDefers emits code for `defer` statements.
|
||||||
|
// TRY-related opcodes handle exception as follows:
|
||||||
|
// 1. CATCH block is executed only if exception has occured.
|
||||||
|
// 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 occured.
|
||||||
|
// 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 is set to true.
|
||||||
|
// 2. CATCH and FINALLY blocks are the same, and both contain the same CALLs.
|
||||||
|
// 3. In CATCH block we set Y to true and emit default return values if it is the last defer.
|
||||||
|
// 4. 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()
|
||||||
|
emit.Jmp(c.prog.BinWriter, opcode.ENDTRYL, after)
|
||||||
|
|
||||||
|
c.setLabel(stmt.catchLabel)
|
||||||
|
c.emitStoreByIndex(varGlobal, c.exceptionIndex)
|
||||||
|
emit.Int(c.prog.BinWriter, 1)
|
||||||
|
c.emitStoreByIndex(varLocal, c.scope.finallyProcessedIndex)
|
||||||
|
ast.Walk(c, stmt.expr)
|
||||||
|
if i == 0 {
|
||||||
|
// After panic, default values must be returns, except for named returns,
|
||||||
|
// which we don't support here for now.
|
||||||
|
for i := len(c.scope.decl.Type.Results.List) - 1; i >= 0; i-- {
|
||||||
|
c.emitDefault(c.typeOf(c.scope.decl.Type.Results.List[i].Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit.Jmp(c.prog.BinWriter, opcode.ENDTRYL, after)
|
||||||
|
|
||||||
|
c.setLabel(stmt.finallyLabel)
|
||||||
|
before := c.newLabel()
|
||||||
|
c.emitLoadByIndex(varLocal, c.scope.finallyProcessedIndex)
|
||||||
|
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, c.scope.finallyProcessedIndex)
|
||||||
|
emit.Opcode(c.prog.BinWriter, opcode.ENDFINALLY)
|
||||||
|
c.setLabel(after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *codegen) rangeLoadKey() {
|
func (c *codegen) rangeLoadKey() {
|
||||||
emit.Int(c.prog.BinWriter, 2)
|
emit.Int(c.prog.BinWriter, 2)
|
||||||
emit.Opcode(c.prog.BinWriter, opcode.PICK) // load keys
|
emit.Opcode(c.prog.BinWriter, opcode.PICK) // load keys
|
||||||
|
@ -1428,17 +1510,11 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "panic":
|
case "panic":
|
||||||
arg := expr.Args[0]
|
|
||||||
if isExprNil(arg) {
|
|
||||||
emit.Opcode(c.prog.BinWriter, opcode.DROP)
|
|
||||||
emit.Opcode(c.prog.BinWriter, opcode.THROW)
|
emit.Opcode(c.prog.BinWriter, opcode.THROW)
|
||||||
} else if isString(c.typeInfo.Types[arg].Type) {
|
case "recover":
|
||||||
ast.Walk(c, arg)
|
c.emitLoadByIndex(varGlobal, c.exceptionIndex)
|
||||||
emit.Syscall(c.prog.BinWriter, interopnames.SystemRuntimeLog)
|
emit.Opcode(c.prog.BinWriter, opcode.PUSHNULL)
|
||||||
emit.Opcode(c.prog.BinWriter, opcode.THROW)
|
c.emitStoreByIndex(varGlobal, c.exceptionIndex)
|
||||||
} else {
|
|
||||||
c.prog.Err = errors.New("panic should have string or nil argument")
|
|
||||||
}
|
|
||||||
case "ToInteger", "ToByteArray", "ToBool":
|
case "ToInteger", "ToByteArray", "ToBool":
|
||||||
typ := stackitem.IntegerT
|
typ := stackitem.IntegerT
|
||||||
switch name {
|
switch name {
|
||||||
|
@ -1482,8 +1558,6 @@ func transformArgs(fun ast.Expr, args []ast.Expr) []ast.Expr {
|
||||||
}
|
}
|
||||||
case *ast.Ident:
|
case *ast.Ident:
|
||||||
switch f.Name {
|
switch f.Name {
|
||||||
case "panic":
|
|
||||||
return args[1:]
|
|
||||||
case "make", "copy":
|
case "make", "copy":
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1778,27 +1852,32 @@ func (c *codegen) writeJumps(b []byte) ([]byte, error) {
|
||||||
case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
|
case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
|
||||||
opcode.JMPEQ, opcode.JMPNE,
|
opcode.JMPEQ, opcode.JMPNE,
|
||||||
opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT:
|
opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT:
|
||||||
|
case opcode.TRYL:
|
||||||
|
nextIP := ctx.NextIP()
|
||||||
|
catchArg := b[nextIP-8:]
|
||||||
|
_, err := c.replaceLabelWithOffset(ctx.IP(), catchArg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
finallyArg := b[nextIP-4:]
|
||||||
|
_, err = c.replaceLabelWithOffset(ctx.IP(), finallyArg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
|
case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
|
||||||
opcode.JMPEQL, opcode.JMPNEL,
|
opcode.JMPEQL, opcode.JMPNEL,
|
||||||
opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
|
opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
|
||||||
opcode.CALLL, opcode.PUSHA:
|
opcode.CALLL, opcode.PUSHA, opcode.ENDTRYL:
|
||||||
// we can't use arg returned by ctx.Next() because it is copied
|
// we can't use arg returned by ctx.Next() because it is copied
|
||||||
nextIP := ctx.NextIP()
|
nextIP := ctx.NextIP()
|
||||||
arg := b[nextIP-4:]
|
arg := b[nextIP-4:]
|
||||||
|
offset, err := c.replaceLabelWithOffset(ctx.IP(), arg)
|
||||||
index := binary.LittleEndian.Uint16(arg)
|
if err != nil {
|
||||||
if int(index) > len(c.l) {
|
return nil, err
|
||||||
return nil, fmt.Errorf("unexpected label number: %d (max %d)", index, len(c.l))
|
|
||||||
}
|
|
||||||
offset := c.l[index] - nextIP + 5
|
|
||||||
if offset > math.MaxInt32 || offset < math.MinInt32 {
|
|
||||||
return nil, fmt.Errorf("label offset is too big at the instruction %d: %d (max %d, min %d)",
|
|
||||||
nextIP-5, offset, math.MaxInt32, math.MinInt32)
|
|
||||||
}
|
}
|
||||||
if op != opcode.PUSHA && math.MinInt8 <= offset && offset <= math.MaxInt8 {
|
if op != opcode.PUSHA && math.MinInt8 <= offset && offset <= math.MaxInt8 {
|
||||||
offsets = append(offsets, ctx.IP())
|
offsets = append(offsets, ctx.IP())
|
||||||
}
|
}
|
||||||
binary.LittleEndian.PutUint32(arg, uint32(offset))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Correct function ip range.
|
// Correct function ip range.
|
||||||
|
@ -1820,6 +1899,20 @@ func (c *codegen) writeJumps(b []byte) ([]byte, error) {
|
||||||
return shortenJumps(b, offsets), nil
|
return shortenJumps(b, offsets), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// longToShortRemoveCount is a difference between short and long instruction sizes in bytes.
|
// longToShortRemoveCount is a difference between short and long instruction sizes in bytes.
|
||||||
const longToShortRemoveCount = 3
|
const longToShortRemoveCount = 3
|
||||||
|
|
||||||
|
@ -1844,18 +1937,34 @@ func shortenJumps(b []byte, offsets []int) []byte {
|
||||||
switch op {
|
switch op {
|
||||||
case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
|
case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
|
||||||
opcode.JMPEQ, opcode.JMPNE,
|
opcode.JMPEQ, opcode.JMPNE,
|
||||||
opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT:
|
opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT, opcode.ENDTRY:
|
||||||
offset := int(int8(b[nextIP-1]))
|
offset := int(int8(b[nextIP-1]))
|
||||||
offset += calcOffsetCorrection(ip, ip+offset, offsets)
|
offset += calcOffsetCorrection(ip, ip+offset, offsets)
|
||||||
b[nextIP-1] = byte(offset)
|
b[nextIP-1] = byte(offset)
|
||||||
|
case opcode.TRY:
|
||||||
|
catchOffset := int(int8(b[nextIP-2]))
|
||||||
|
catchOffset += calcOffsetCorrection(ip, ip+catchOffset, offsets)
|
||||||
|
b[nextIP-1] = byte(catchOffset)
|
||||||
|
finallyOffset := int(int8(b[nextIP-1]))
|
||||||
|
finallyOffset += calcOffsetCorrection(ip, ip+finallyOffset, offsets)
|
||||||
|
b[nextIP-1] = byte(finallyOffset)
|
||||||
case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
|
case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
|
||||||
opcode.JMPEQL, opcode.JMPNEL,
|
opcode.JMPEQL, opcode.JMPNEL,
|
||||||
opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
|
opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
|
||||||
opcode.CALLL, opcode.PUSHA:
|
opcode.CALLL, opcode.PUSHA, opcode.ENDTRYL:
|
||||||
arg := b[nextIP-4:]
|
arg := b[nextIP-4:]
|
||||||
offset := int(int32(binary.LittleEndian.Uint32(arg)))
|
offset := int(int32(binary.LittleEndian.Uint32(arg)))
|
||||||
offset += calcOffsetCorrection(ip, ip+offset, offsets)
|
offset += calcOffsetCorrection(ip, ip+offset, offsets)
|
||||||
binary.LittleEndian.PutUint32(arg, uint32(offset))
|
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, offsets)
|
||||||
|
binary.LittleEndian.PutUint32(arg, uint32(catchOffset))
|
||||||
|
arg = b[nextIP-4:]
|
||||||
|
finallyOffset := int(int32(binary.LittleEndian.Uint32(arg)))
|
||||||
|
finallyOffset += calcOffsetCorrection(ip, ip+finallyOffset, offsets)
|
||||||
|
binary.LittleEndian.PutUint32(arg, uint32(finallyOffset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1939,6 +2048,8 @@ func toShortForm(op opcode.Opcode) opcode.Opcode {
|
||||||
return opcode.JMPLT
|
return opcode.JMPLT
|
||||||
case opcode.CALLL:
|
case opcode.CALLL:
|
||||||
return opcode.CALL
|
return opcode.CALL
|
||||||
|
case opcode.ENDTRYL:
|
||||||
|
return opcode.ENDTRY
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf("invalid opcode: %s", op))
|
panic(fmt.Errorf("invalid opcode: %s", op))
|
||||||
}
|
}
|
||||||
|
|
139
pkg/compiler/defer_test.go
Normal file
139
pkg/compiler/defer_test.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package compiler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefer(t *testing.T) {
|
||||||
|
t.Run("Simple", func(t *testing.T) {
|
||||||
|
src := `package main
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer f()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
func f() { a += 2 }`
|
||||||
|
eval(t, src, big.NewInt(3))
|
||||||
|
})
|
||||||
|
t.Run("ValueUnchanged", func(t *testing.T) {
|
||||||
|
src := `package main
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
defer f()
|
||||||
|
a = 3
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
func f() { a += 2 }`
|
||||||
|
eval(t, src, big.NewInt(3))
|
||||||
|
})
|
||||||
|
t.Run("Function", func(t *testing.T) {
|
||||||
|
src := `package main
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer f()
|
||||||
|
a = 3
|
||||||
|
return g()
|
||||||
|
}
|
||||||
|
func g() int {
|
||||||
|
a++
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
func f() { a += 2 }`
|
||||||
|
eval(t, src, big.NewInt(10))
|
||||||
|
})
|
||||||
|
t.Run("MultipleDefers", func(t *testing.T) {
|
||||||
|
src := `package main
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer f()
|
||||||
|
defer g()
|
||||||
|
a = 3
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
func g() { a *= 2 }
|
||||||
|
func f() { a += 2 }`
|
||||||
|
eval(t, src, big.NewInt(11))
|
||||||
|
})
|
||||||
|
t.Run("FunctionLiteral", func(t *testing.T) {
|
||||||
|
src := `package main
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer func() {
|
||||||
|
a = 10
|
||||||
|
}()
|
||||||
|
a = 3
|
||||||
|
return a
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(13))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecover(t *testing.T) {
|
||||||
|
t.Run("Panic", func(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
a = 3
|
||||||
|
} else {
|
||||||
|
a = 4
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
a = 1
|
||||||
|
panic("msg")
|
||||||
|
return a
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(3))
|
||||||
|
})
|
||||||
|
t.Run("NoPanic", func(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
a = 3
|
||||||
|
} else {
|
||||||
|
a = 4
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
a = 1
|
||||||
|
return a
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(5))
|
||||||
|
})
|
||||||
|
t.Run("PanicInDefer", func(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var a int
|
||||||
|
func Main() int {
|
||||||
|
return h() + a
|
||||||
|
}
|
||||||
|
func h() int {
|
||||||
|
defer func() { a += 2; _ = recover() }()
|
||||||
|
defer func() { a *= 3; _ = recover(); panic("again") }()
|
||||||
|
a = 1
|
||||||
|
panic("msg")
|
||||||
|
return a
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(5))
|
||||||
|
})
|
||||||
|
}
|
|
@ -30,6 +30,12 @@ type funcScope struct {
|
||||||
// Variables together with it's type in neo-vm.
|
// Variables together with it's type in neo-vm.
|
||||||
variables []string
|
variables []string
|
||||||
|
|
||||||
|
// deferStack is a stack containing encountered `defer` statements.
|
||||||
|
deferStack []deferInfo
|
||||||
|
// finallyProcessed is a index of static slot with boolean flag determining
|
||||||
|
// if `defer` statement was already processed.
|
||||||
|
finallyProcessedIndex int
|
||||||
|
|
||||||
// Local variables
|
// Local variables
|
||||||
vars varScope
|
vars varScope
|
||||||
|
|
||||||
|
@ -45,6 +51,12 @@ type funcScope struct {
|
||||||
i int
|
i int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type deferInfo struct {
|
||||||
|
catchLabel uint16
|
||||||
|
finallyLabel uint16
|
||||||
|
expr *ast.CallExpr
|
||||||
|
}
|
||||||
|
|
||||||
func (c *codegen) newFuncScope(decl *ast.FuncDecl, label uint16) *funcScope {
|
func (c *codegen) newFuncScope(decl *ast.FuncDecl, label uint16) *funcScope {
|
||||||
var name string
|
var name string
|
||||||
if decl.Name != nil {
|
if decl.Name != nil {
|
||||||
|
@ -115,6 +127,7 @@ func (c *funcScope) analyzeVoidCalls(node ast.Node) bool {
|
||||||
|
|
||||||
func (c *funcScope) countLocals() int {
|
func (c *funcScope) countLocals() int {
|
||||||
size := 0
|
size := 0
|
||||||
|
hasDefer := false
|
||||||
ast.Inspect(c.decl, func(n ast.Node) bool {
|
ast.Inspect(c.decl, func(n ast.Node) bool {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *ast.FuncType:
|
case *ast.FuncType:
|
||||||
|
@ -124,8 +137,11 @@ func (c *funcScope) countLocals() int {
|
||||||
}
|
}
|
||||||
case *ast.AssignStmt:
|
case *ast.AssignStmt:
|
||||||
if n.Tok == token.DEFINE {
|
if n.Tok == token.DEFINE {
|
||||||
size += len(n.Rhs)
|
size += len(n.Lhs)
|
||||||
}
|
}
|
||||||
|
case *ast.DeferStmt:
|
||||||
|
hasDefer = true
|
||||||
|
return false
|
||||||
case *ast.ReturnStmt, *ast.IfStmt:
|
case *ast.ReturnStmt, *ast.IfStmt:
|
||||||
size++
|
size++
|
||||||
// This handles the inline GenDecl like "var x = 2"
|
// This handles the inline GenDecl like "var x = 2"
|
||||||
|
@ -143,6 +159,10 @@ func (c *funcScope) countLocals() int {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
if hasDefer {
|
||||||
|
c.finallyProcessedIndex = size
|
||||||
|
size++
|
||||||
|
}
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,18 @@ func TestMultiDeclaration(t *testing.T) {
|
||||||
eval(t, src, big.NewInt(6))
|
eval(t, src, big.NewInt(6))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCountLocal(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
func Main() int {
|
||||||
|
a, b, c, d := f()
|
||||||
|
return a + b + c + d
|
||||||
|
}
|
||||||
|
func f() (int, int, int, int) {
|
||||||
|
return 1, 2, 3, 4
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(10))
|
||||||
|
}
|
||||||
|
|
||||||
func TestMultiDeclarationLocal(t *testing.T) {
|
func TestMultiDeclarationLocal(t *testing.T) {
|
||||||
src := `package foo
|
src := `package foo
|
||||||
func Main() int {
|
func Main() int {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package compiler_test
|
package compiler_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -91,3 +92,32 @@ func TestNestedIF(t *testing.T) {
|
||||||
`
|
`
|
||||||
eval(t, src, big.NewInt(0))
|
eval(t, src, big.NewInt(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInitIF(t *testing.T) {
|
||||||
|
t.Run("Simple", func(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
func Main() int {
|
||||||
|
if a := 42; true {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(42))
|
||||||
|
})
|
||||||
|
t.Run("Shadow", func(t *testing.T) {
|
||||||
|
srcTmpl := `package foo
|
||||||
|
func Main() int {
|
||||||
|
a := 11
|
||||||
|
if a := 42; %v {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}`
|
||||||
|
t.Run("True", func(t *testing.T) {
|
||||||
|
eval(t, fmt.Sprintf(srcTmpl, true), big.NewInt(42))
|
||||||
|
})
|
||||||
|
t.Run("False", func(t *testing.T) {
|
||||||
|
eval(t, fmt.Sprintf(srcTmpl, false), big.NewInt(11))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -13,3 +13,16 @@ func TestFuncLiteral(t *testing.T) {
|
||||||
}`
|
}`
|
||||||
eval(t, src, big.NewInt(5))
|
eval(t, src, big.NewInt(5))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCallInPlace(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var a int = 1
|
||||||
|
func Main() int {
|
||||||
|
func() {
|
||||||
|
a += 10
|
||||||
|
}()
|
||||||
|
a += 100
|
||||||
|
return a
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(111))
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
package compiler_test
|
package compiler_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,26 +15,19 @@ func TestPanic(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("panic with message", func(t *testing.T) {
|
t.Run("panic with message", func(t *testing.T) {
|
||||||
var logs []string
|
|
||||||
src := getPanicSource(true, `"execution fault"`)
|
src := getPanicSource(true, `"execution fault"`)
|
||||||
v := vmAndCompile(t, src)
|
v := vmAndCompile(t, src)
|
||||||
v.SyscallHandler = getLogHandler(&logs)
|
|
||||||
|
|
||||||
require.Error(t, v.Run())
|
require.Error(t, v.Run())
|
||||||
require.True(t, v.HasFailed())
|
require.True(t, v.HasFailed())
|
||||||
require.Equal(t, 1, len(logs))
|
|
||||||
require.Equal(t, "execution fault", logs[0])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("panic with nil", func(t *testing.T) {
|
t.Run("panic with nil", func(t *testing.T) {
|
||||||
var logs []string
|
|
||||||
src := getPanicSource(true, `nil`)
|
src := getPanicSource(true, `nil`)
|
||||||
v := vmAndCompile(t, src)
|
v := vmAndCompile(t, src)
|
||||||
v.SyscallHandler = getLogHandler(&logs)
|
|
||||||
|
|
||||||
require.Error(t, v.Run())
|
require.Error(t, v.Run())
|
||||||
require.True(t, v.HasFailed())
|
require.True(t, v.HasFailed())
|
||||||
require.Equal(t, 0, len(logs))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,16 +44,3 @@ func getPanicSource(need bool, message string) string {
|
||||||
}
|
}
|
||||||
`, need, message)
|
`, need, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLogHandler(logs *[]string) vm.SyscallHandler {
|
|
||||||
logID := interopnames.ToID([]byte(interopnames.SystemRuntimeLog))
|
|
||||||
return func(v *vm.VM, id uint32) error {
|
|
||||||
if id != logID {
|
|
||||||
return errors.New("syscall not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := v.Estack().Pop().String()
|
|
||||||
*logs = append(*logs, msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -160,5 +160,5 @@ func AppCallWithOperationAndArgs(w *io.BinWriter, scriptHash util.Uint160, opera
|
||||||
}
|
}
|
||||||
|
|
||||||
func isInstructionJmp(op opcode.Opcode) bool {
|
func isInstructionJmp(op opcode.Opcode) bool {
|
||||||
return opcode.JMP <= op && op <= opcode.CALLL
|
return opcode.JMP <= op && op <= opcode.CALLL || op == opcode.ENDTRYL
|
||||||
}
|
}
|
||||||
|
|
|
@ -1476,6 +1476,7 @@ func (v *VM) Call(ctx *Context, offset int) {
|
||||||
newCtx.CheckReturn = false
|
newCtx.CheckReturn = false
|
||||||
newCtx.local = nil
|
newCtx.local = nil
|
||||||
newCtx.arguments = nil
|
newCtx.arguments = nil
|
||||||
|
newCtx.tryStack = NewStack("exception")
|
||||||
v.istack.PushVal(newCtx)
|
v.istack.PushVal(newCtx)
|
||||||
v.Jump(newCtx, offset)
|
v.Jump(newCtx, offset)
|
||||||
}
|
}
|
||||||
|
@ -1517,7 +1518,7 @@ func (v *VM) handleException() {
|
||||||
pop := 0
|
pop := 0
|
||||||
ictx := v.istack.Peek(0).Value().(*Context)
|
ictx := v.istack.Peek(0).Value().(*Context)
|
||||||
for ictx != nil {
|
for ictx != nil {
|
||||||
e := ictx.tryStack.Peek(pop)
|
e := ictx.tryStack.Peek(0)
|
||||||
for e != nil {
|
for e != nil {
|
||||||
ectx := e.Value().(*exceptionHandlingContext)
|
ectx := e.Value().(*exceptionHandlingContext)
|
||||||
if ectx.State == eFinally || (ectx.State == eCatch && !ectx.HasFinally()) {
|
if ectx.State == eFinally || (ectx.State == eCatch && !ectx.HasFinally()) {
|
||||||
|
|
|
@ -1336,6 +1336,12 @@ func TestTRY(t *testing.T) {
|
||||||
checkVMFailed(t, vm)
|
checkVMFailed(t, vm)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
t.Run("ThrowInCall", func(t *testing.T) {
|
||||||
|
catchP := []byte{byte(opcode.CALL), 2, byte(opcode.PUSH1), byte(opcode.ADD), byte(opcode.THROW), byte(opcode.RET)}
|
||||||
|
inner := getTRYProgram(throw, catchP, []byte{byte(opcode.PUSH2)})
|
||||||
|
// add 5 to the exception, mul to the result of inner finally (2)
|
||||||
|
getTRYTestFunc(47, inner, append(add5, byte(opcode.MUL)), add9)(t)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMEMCPY(t *testing.T) {
|
func TestMEMCPY(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue