diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index c1b01e2ee..ad46b8b93 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -16,6 +16,7 @@ var ( "SHA1", "Hash256", "Hash160", "VerifySignature", "AppCall", "FromAddress", "Equals", + "panic", } ) @@ -69,6 +70,12 @@ func isIdentBool(ident *ast.Ident) bool { 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. func makeBoolFromIdent(ident *ast.Ident, tinfo *types.Info) (types.TypeAndValue, error) { var b bool @@ -180,16 +187,6 @@ func isBuiltin(expr ast.Expr) bool { 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 { if len(lit.Elts) == 0 { return false diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 0be404597..1cce7c6e7 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -523,17 +523,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil } - args := 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:] - } + args := transformArgs(n.Fun, n.Args) // Handle the arguments 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. // We can be sure builtins are of type *ast.Ident. 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): c.convertSyscall(f.selector.Name, f.name) default: @@ -764,12 +744,22 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { if isByteArrayType(typ) { emitOpcode(c.prog.BinWriter, opcode.CAT) } else { + emitOpcode(c.prog.BinWriter, opcode.OVER) 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) } + 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": emitOpcode(c.prog.BinWriter, opcode.SHA256) case "SHA1": @@ -782,6 +772,12 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { emitOpcode(c.prog.BinWriter, opcode.VERIFY) case "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": emitOpcode(c.prog.BinWriter, opcode.EQUAL) 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) { buf := make([]byte, len(lit.Elts)) for i := 0; i < len(lit.Elts); i++ { diff --git a/pkg/compiler/panic_test.go b/pkg/compiler/panic_test.go new file mode 100644 index 000000000..f6e3b4d59 --- /dev/null +++ b/pkg/compiler/panic_test.go @@ -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 + }, + } + } +}