diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index c1b01e2ee..974ba34d2 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 diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 0be404597..8a9650d43 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -770,6 +770,17 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { 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) { + 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": 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 + }, + } + } +}