forked from TrueCloudLab/neoneo-go
Merge pull request #629 from nspcc-dev/feature/panic
compiler: support panic builtin
This commit is contained in:
commit
d234a1e718
3 changed files with 122 additions and 34 deletions
|
@ -16,6 +16,7 @@ var (
|
||||||
"SHA1", "Hash256", "Hash160",
|
"SHA1", "Hash256", "Hash160",
|
||||||
"VerifySignature", "AppCall",
|
"VerifySignature", "AppCall",
|
||||||
"FromAddress", "Equals",
|
"FromAddress", "Equals",
|
||||||
|
"panic",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,6 +70,12 @@ func isIdentBool(ident *ast.Ident) bool {
|
||||||
return ident.Name == "true" || ident.Name == "false"
|
return ident.Name == "true" || ident.Name == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isExprNil looks if the given expression is a `nil`.
|
||||||
|
func isExprNil(e ast.Expr) bool {
|
||||||
|
v, ok := e.(*ast.Ident)
|
||||||
|
return ok && v.Name == "nil"
|
||||||
|
}
|
||||||
|
|
||||||
// makeBoolFromIdent creates a bool type from an *ast.Ident.
|
// makeBoolFromIdent creates a bool type from an *ast.Ident.
|
||||||
func makeBoolFromIdent(ident *ast.Ident, tinfo *types.Info) (types.TypeAndValue, error) {
|
func makeBoolFromIdent(ident *ast.Ident, tinfo *types.Info) (types.TypeAndValue, error) {
|
||||||
var b bool
|
var b bool
|
||||||
|
@ -180,16 +187,6 @@ func isBuiltin(expr ast.Expr) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAppCall(expr ast.Expr) bool {
|
|
||||||
t, ok := expr.(*ast.SelectorExpr)
|
|
||||||
return ok && t.Sel.Name == "AppCall"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFromAddress(expr ast.Expr) bool {
|
|
||||||
t, ok := expr.(*ast.SelectorExpr)
|
|
||||||
return ok && t.Sel.Name == "FromAddress"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool {
|
func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool {
|
||||||
if len(lit.Elts) == 0 {
|
if len(lit.Elts) == 0 {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -523,17 +523,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
args := n.Args
|
args := transformArgs(n.Fun, n.Args)
|
||||||
isAppCall := isAppCall(n.Fun)
|
|
||||||
isFromAddress := isFromAddress(n.Fun)
|
|
||||||
// There are 2 special cases:
|
|
||||||
// 1. When using APPCALL, script hash is a part of the instruction so
|
|
||||||
// script hash should be emitted after APPCALL.
|
|
||||||
// 2. With FromAddress, parameter conversion is happening at compile-time
|
|
||||||
// so there is no need to push parameters on stack and perform an actual call
|
|
||||||
if isAppCall || isFromAddress {
|
|
||||||
args = n.Args[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the arguments
|
// Handle the arguments
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
|
@ -560,16 +550,6 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
// Use the ident to check, builtins are not in func scopes.
|
// Use the ident to check, builtins are not in func scopes.
|
||||||
// We can be sure builtins are of type *ast.Ident.
|
// We can be sure builtins are of type *ast.Ident.
|
||||||
c.convertBuiltin(n)
|
c.convertBuiltin(n)
|
||||||
|
|
||||||
if isAppCall {
|
|
||||||
buf := c.getByteArray(n.Args[0])
|
|
||||||
if len(buf) != 20 {
|
|
||||||
c.prog.Err = errors.New("invalid script hash")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.prog.WriteBytes(buf)
|
|
||||||
}
|
|
||||||
case isSyscall(f):
|
case isSyscall(f):
|
||||||
c.convertSyscall(f.selector.Name, f.name)
|
c.convertSyscall(f.selector.Name, f.name)
|
||||||
default:
|
default:
|
||||||
|
@ -764,12 +744,22 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
|
||||||
if isByteArrayType(typ) {
|
if isByteArrayType(typ) {
|
||||||
emitOpcode(c.prog.BinWriter, opcode.CAT)
|
emitOpcode(c.prog.BinWriter, opcode.CAT)
|
||||||
} else {
|
} else {
|
||||||
|
emitOpcode(c.prog.BinWriter, opcode.OVER)
|
||||||
emitOpcode(c.prog.BinWriter, opcode.SWAP)
|
emitOpcode(c.prog.BinWriter, opcode.SWAP)
|
||||||
emitOpcode(c.prog.BinWriter, opcode.DUP)
|
|
||||||
emitOpcode(c.prog.BinWriter, opcode.PUSH2)
|
|
||||||
emitOpcode(c.prog.BinWriter, opcode.XSWAP)
|
|
||||||
emitOpcode(c.prog.BinWriter, opcode.APPEND)
|
emitOpcode(c.prog.BinWriter, opcode.APPEND)
|
||||||
}
|
}
|
||||||
|
case "panic":
|
||||||
|
arg := expr.Args[0]
|
||||||
|
if isExprNil(arg) {
|
||||||
|
emitOpcode(c.prog.BinWriter, opcode.DROP)
|
||||||
|
emitOpcode(c.prog.BinWriter, opcode.THROW)
|
||||||
|
} else if isStringType(c.typeInfo.Types[arg].Type) {
|
||||||
|
ast.Walk(c, arg)
|
||||||
|
emitSyscall(c.prog.BinWriter, "Neo.Runtime.Log")
|
||||||
|
emitOpcode(c.prog.BinWriter, opcode.THROW)
|
||||||
|
} else {
|
||||||
|
c.prog.Err = errors.New("panic should have string or nil argument")
|
||||||
|
}
|
||||||
case "SHA256":
|
case "SHA256":
|
||||||
emitOpcode(c.prog.BinWriter, opcode.SHA256)
|
emitOpcode(c.prog.BinWriter, opcode.SHA256)
|
||||||
case "SHA1":
|
case "SHA1":
|
||||||
|
@ -782,6 +772,12 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
|
||||||
emitOpcode(c.prog.BinWriter, opcode.VERIFY)
|
emitOpcode(c.prog.BinWriter, opcode.VERIFY)
|
||||||
case "AppCall":
|
case "AppCall":
|
||||||
emitOpcode(c.prog.BinWriter, opcode.APPCALL)
|
emitOpcode(c.prog.BinWriter, opcode.APPCALL)
|
||||||
|
buf := c.getByteArray(expr.Args[0])
|
||||||
|
if len(buf) != 20 {
|
||||||
|
c.prog.Err = errors.New("invalid script hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.prog.WriteBytes(buf)
|
||||||
case "Equals":
|
case "Equals":
|
||||||
emitOpcode(c.prog.BinWriter, opcode.EQUAL)
|
emitOpcode(c.prog.BinWriter, opcode.EQUAL)
|
||||||
case "FromAddress":
|
case "FromAddress":
|
||||||
|
@ -800,6 +796,30 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// transformArgs returns a list of function arguments
|
||||||
|
// which should be put on stack.
|
||||||
|
// There are special cases for builtins:
|
||||||
|
// 1. When using AppCall, script hash is a part of the instruction so
|
||||||
|
// it should be emitted after APPCALL.
|
||||||
|
// 2. With FromAddress, parameter conversion is happening at compile-time
|
||||||
|
// so there is no need to push parameters on stack and perform an actual call
|
||||||
|
// 3. With panic, generated code depends on if argument was nil or a string so
|
||||||
|
// it should be handled accordingly.
|
||||||
|
func transformArgs(fun ast.Expr, args []ast.Expr) []ast.Expr {
|
||||||
|
switch f := fun.(type) {
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
if f.Sel.Name == "AppCall" || f.Sel.Name == "FromAddress" {
|
||||||
|
return args[1:]
|
||||||
|
}
|
||||||
|
case *ast.Ident:
|
||||||
|
if f.Name == "panic" {
|
||||||
|
return args[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func (c *codegen) convertByteArray(lit *ast.CompositeLit) {
|
func (c *codegen) convertByteArray(lit *ast.CompositeLit) {
|
||||||
buf := make([]byte, len(lit.Elts))
|
buf := make([]byte, len(lit.Elts))
|
||||||
for i := 0; i < len(lit.Elts); i++ {
|
for i := 0; i < len(lit.Elts); i++ {
|
||||||
|
|
71
pkg/compiler/panic_test.go
Normal file
71
pkg/compiler/panic_test.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package compiler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPanic(t *testing.T) {
|
||||||
|
t.Run("no panic", func(t *testing.T) {
|
||||||
|
src := getPanicSource(false, `"execution fault"`)
|
||||||
|
eval(t, src, big.NewInt(7))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("panic with message", func(t *testing.T) {
|
||||||
|
var logs []string
|
||||||
|
src := getPanicSource(true, `"execution fault"`)
|
||||||
|
v := vmAndCompile(t, src)
|
||||||
|
v.RegisterInteropGetter(logGetter(&logs))
|
||||||
|
|
||||||
|
require.Error(t, v.Run())
|
||||||
|
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) {
|
||||||
|
var logs []string
|
||||||
|
src := getPanicSource(true, `nil`)
|
||||||
|
v := vmAndCompile(t, src)
|
||||||
|
v.RegisterInteropGetter(logGetter(&logs))
|
||||||
|
|
||||||
|
require.Error(t, v.Run())
|
||||||
|
require.True(t, v.HasFailed())
|
||||||
|
require.Equal(t, 0, len(logs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPanicSource(need bool, message string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
package main
|
||||||
|
func Main() int {
|
||||||
|
needPanic := %#v
|
||||||
|
if needPanic {
|
||||||
|
panic(%s)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
`, need, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logGetter(logs *[]string) vm.InteropGetterFunc {
|
||||||
|
logID := vm.InteropNameToID([]byte("Neo.Runtime.Log"))
|
||||||
|
return func(id uint32) *vm.InteropFuncPrice {
|
||||||
|
if id != logID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &vm.InteropFuncPrice{
|
||||||
|
Func: func(v *vm.VM) error {
|
||||||
|
msg := string(v.Estack().Pop().Bytes())
|
||||||
|
*logs = append(*logs, msg)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue