From 8171d580719c6c8c2ba3be28361da91a201c5466 Mon Sep 17 00:00:00 2001 From: Daniel Kang Date: Wed, 16 Jan 2019 12:23:20 -0800 Subject: [PATCH] Error Object (#4) add error expression and error object --- Makefile | 5 +- README.md | 12 ++ assert/assert.go | 2 + compiler/ast/error_expr.go | 29 ++++ compiler/compiler.go | 7 + compiler/definitions.go | 68 --------- compiler/instructions.go | 26 ++-- compiler/opcodes.go | 141 +++++++++++++++++- .../{operands_test.go => opcodes_test.go} | 5 +- compiler/operands.go | 27 ---- compiler/parser/parser.go | 23 ++- compiler/parser/parser_error_test.go | 43 ++++++ compiler/parser/parser_test.go | 9 ++ compiler/token/tokens.go | 2 + objects/builtin_is_error.go | 18 +++ objects/builtin_is_undefined.go | 18 +++ objects/builtins.go | 8 + objects/error.go | 47 ++++++ objects/error_test.go | 19 +++ objects/object_test.go | 11 ++ runtime/vm.go | 29 ++++ runtime/vm_boolean_test.go | 20 +++ runtime/vm_builtin_test.go | 6 + runtime/vm_error_test.go | 44 ++---- runtime/vm_string_test.go | 2 + runtime/vm_test.go | 8 +- 26 files changed, 477 insertions(+), 152 deletions(-) create mode 100644 compiler/ast/error_expr.go delete mode 100644 compiler/definitions.go rename compiler/{operands_test.go => opcodes_test.go} (79%) delete mode 100644 compiler/operands.go create mode 100644 compiler/parser/parser_error_test.go create mode 100644 objects/builtin_is_error.go create mode 100644 objects/builtin_is_undefined.go create mode 100644 objects/error.go create mode 100644 objects/error_test.go diff --git a/Makefile b/Makefile index 4409f84..e461339 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ vet: go vet ./... -test: vet +lint: + golint -set_exit_status ./... + +test: vet lint go test -race -cover ./... fmt: diff --git a/README.md b/README.md index bc8ac2d..5350318 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,19 @@ for i, x in [1, 2, 3] { // array: index and element for k, v in {k1: 1, k2: 2} { // map: key and value // ... } +``` + +An error object is created using `error` function-like keyword. An error can have any types of value and the underlying value of the error can be accessed using `.value` selector. + +```golang +err1 := error("oops") // error with string value +err2 := error(1+2+3) // error with int value +if is_error(err1) { // 'is_error' builtin function + err_val := err1.value // get underlying value +} ``` +> [Run in Playground](https://tengolang.com/?s=5eaba4289c9d284d97704dd09cb15f4f03ad05c1) + ## Embedding Tengo in Go diff --git a/assert/assert.go b/assert/assert.go index b09be7c..35b7117 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -173,6 +173,8 @@ func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool return equalClosure(t, expected, actual.(*objects.Closure)) case *objects.Undefined: return true + case *objects.Error: + return Equal(t, expected.Value, actual.(*objects.Error).Value) default: panic(fmt.Errorf("type not implemented: %T", expected)) } diff --git a/compiler/ast/error_expr.go b/compiler/ast/error_expr.go new file mode 100644 index 0000000..7ce5667 --- /dev/null +++ b/compiler/ast/error_expr.go @@ -0,0 +1,29 @@ +package ast + +import ( + "github.com/d5/tengo/compiler/source" +) + +// ErrorExpr represents an error expression +type ErrorExpr struct { + Expr Expr + ErrorPos source.Pos + LParen source.Pos + RParen source.Pos +} + +func (e *ErrorExpr) exprNode() {} + +// Pos returns the position of first character belonging to the node. +func (e *ErrorExpr) Pos() source.Pos { + return e.ErrorPos +} + +// End returns the position of first character immediately after the node. +func (e *ErrorExpr) End() source.Pos { + return e.RParen +} + +func (e *ErrorExpr) String() string { + return "error(" + e.Expr.String() + ")" +} diff --git a/compiler/compiler.go b/compiler/compiler.go index d0ebfb9..27d87b1 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -408,6 +408,13 @@ func (c *Compiler) Compile(node ast.Node) error { } c.emit(OpCall, len(node.Args)) + + case *ast.ErrorExpr: + if err := c.Compile(node.Expr); err != nil { + return err + } + + c.emit(OpError) } return nil diff --git a/compiler/definitions.go b/compiler/definitions.go deleted file mode 100644 index a84410f..0000000 --- a/compiler/definitions.go +++ /dev/null @@ -1,68 +0,0 @@ -package compiler - -// Definition represents an Opcode name and -// the number of operands. -type Definition struct { - Name string - Operands []int -} - -var definitions = map[Opcode]*Definition{ - OpConstant: {Name: "CONST", Operands: []int{2}}, - OpPop: {Name: "POP", Operands: []int{}}, - OpTrue: {Name: "TRUE", Operands: []int{}}, - OpFalse: {Name: "FALSE", Operands: []int{}}, - OpAdd: {Name: "ADD", Operands: []int{}}, - OpSub: {Name: "SUB", Operands: []int{}}, - OpMul: {Name: "MUL", Operands: []int{}}, - OpDiv: {Name: "DIV", Operands: []int{}}, - OpRem: {Name: "REM", Operands: []int{}}, - OpBAnd: {Name: "AND", Operands: []int{}}, - OpBOr: {Name: "OR", Operands: []int{}}, - OpBXor: {Name: "XOR", Operands: []int{}}, - OpBAndNot: {Name: "ANDN", Operands: []int{}}, - OpBShiftLeft: {Name: "SHL", Operands: []int{}}, - OpBShiftRight: {Name: "SHR", Operands: []int{}}, - OpBComplement: {Name: "NEG", Operands: []int{}}, - OpEqual: {Name: "EQL", Operands: []int{}}, - OpNotEqual: {Name: "NEQ", Operands: []int{}}, - OpGreaterThan: {Name: "GTR", Operands: []int{}}, - OpGreaterThanEqual: {Name: "GEQ", Operands: []int{}}, - OpMinus: {Name: "NEG", Operands: []int{}}, - OpLNot: {Name: "NOT", Operands: []int{}}, - OpJumpFalsy: {Name: "JMPF", Operands: []int{2}}, - OpAndJump: {Name: "ANDJMP", Operands: []int{2}}, - OpOrJump: {Name: "ORJMP", Operands: []int{2}}, - OpJump: {Name: "JMP", Operands: []int{2}}, - OpNull: {Name: "NULL", Operands: []int{}}, - OpGetGlobal: {Name: "GETG", Operands: []int{2}}, - OpSetGlobal: {Name: "SETG", Operands: []int{2}}, - OpSetSelGlobal: {Name: "SETSG", Operands: []int{2, 1}}, - OpArray: {Name: "ARR", Operands: []int{2}}, - OpMap: {Name: "MAP", Operands: []int{2}}, - OpIndex: {Name: "INDEX", Operands: []int{}}, - OpSliceIndex: {Name: "SLICE", Operands: []int{}}, - OpCall: {Name: "CALL", Operands: []int{1}}, - OpReturn: {Name: "RET", Operands: []int{}}, - OpReturnValue: {Name: "RETVAL", Operands: []int{1}}, - OpGetLocal: {Name: "GETL", Operands: []int{1}}, - OpSetLocal: {Name: "SETL", Operands: []int{1}}, - OpDefineLocal: {Name: "DEFL", Operands: []int{1}}, - OpSetSelLocal: {Name: "SETSL", Operands: []int{1, 1}}, - OpGetBuiltin: {Name: "BUILTIN", Operands: []int{1}}, - OpClosure: {Name: "CLOSURE", Operands: []int{2, 1}}, - OpGetFree: {Name: "GETF", Operands: []int{1}}, - OpSetFree: {Name: "SETF", Operands: []int{1}}, - OpSetSelFree: {Name: "SETSF", Operands: []int{1, 1}}, - OpIteratorInit: {Name: "ITER", Operands: []int{}}, - OpIteratorNext: {Name: "ITNXT", Operands: []int{}}, - OpIteratorKey: {Name: "ITKEY", Operands: []int{}}, - OpIteratorValue: {Name: "ITVAL", Operands: []int{}}, -} - -// Lookup returns a Definition of a given opcode. -func Lookup(opcode Opcode) (def *Definition, ok bool) { - def, ok = definitions[opcode] - - return -} diff --git a/compiler/instructions.go b/compiler/instructions.go index fc5d9bc..b04b282 100644 --- a/compiler/instructions.go +++ b/compiler/instructions.go @@ -6,13 +6,10 @@ import ( // MakeInstruction returns a bytecode for an opcode and the operands. func MakeInstruction(opcode Opcode, operands ...int) []byte { - def, ok := Lookup(opcode) - if !ok { - return nil - } + numOperands := OpcodeOperands[opcode] totalLen := 1 - for _, w := range def.Operands { + for _, w := range numOperands { totalLen += w } @@ -21,7 +18,7 @@ func MakeInstruction(opcode Opcode, operands ...int) []byte { offset := 1 for i, o := range operands { - width := def.Operands[i] + width := numOperands[i] switch width { case 1: instruction[offset] = byte(o) @@ -43,21 +40,16 @@ func FormatInstructions(b []byte, posOffset int) []string { i := 0 for i < len(b) { - def, ok := Lookup(Opcode(b[i])) - if !ok { - out = append(out, fmt.Sprintf("error: unknown Opcode %d", b[i])) - continue - } + numOperands := OpcodeOperands[Opcode(b[i])] + operands, read := ReadOperands(numOperands, b[i+1:]) - operands, read := ReadOperands(def, b[i+1:]) - - switch len(def.Operands) { + switch len(numOperands) { case 0: - out = append(out, fmt.Sprintf("%04d %-7s", posOffset+i, def.Name)) + out = append(out, fmt.Sprintf("%04d %-7s", posOffset+i, OpcodeNames[Opcode(b[i])])) case 1: - out = append(out, fmt.Sprintf("%04d %-7s %-5d", posOffset+i, def.Name, operands[0])) + out = append(out, fmt.Sprintf("%04d %-7s %-5d", posOffset+i, OpcodeNames[Opcode(b[i])], operands[0])) case 2: - out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d", posOffset+i, def.Name, operands[0], operands[1])) + out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d", posOffset+i, OpcodeNames[Opcode(b[i])], operands[0], operands[1])) } i += 1 + read diff --git a/compiler/opcodes.go b/compiler/opcodes.go index 6783ed5..19c66d9 100644 --- a/compiler/opcodes.go +++ b/compiler/opcodes.go @@ -32,8 +32,9 @@ const ( OpOrJump // Logical OR jump OpJump // Jump OpNull // Push null - OpArray // Array literal - OpMap // Map literal + OpArray // Array object + OpMap // Map object + OpError // Error object OpIndex // Index operation OpSliceIndex // Slice operation OpCall // Call function @@ -56,3 +57,139 @@ const ( OpIteratorKey // Iterator key OpIteratorValue // Iterator value ) + +// OpcodeNames is opcode names. +var OpcodeNames = [...]string{ + OpConstant: "CONST", + OpPop: "POP", + OpTrue: "TRUE", + OpFalse: "FALSE", + OpAdd: "ADD", + OpSub: "SUB", + OpMul: "MUL", + OpDiv: "DIV", + OpRem: "REM", + OpBAnd: "AND", + OpBOr: "OR", + OpBXor: "XOR", + OpBAndNot: "ANDN", + OpBShiftLeft: "SHL", + OpBShiftRight: "SHR", + OpBComplement: "NEG", + OpEqual: "EQL", + OpNotEqual: "NEQ", + OpGreaterThan: "GTR", + OpGreaterThanEqual: "GEQ", + OpMinus: "NEG", + OpLNot: "NOT", + OpJumpFalsy: "JMPF", + OpAndJump: "ANDJMP", + OpOrJump: "ORJMP", + OpJump: "JMP", + OpNull: "NULL", + OpGetGlobal: "GETG", + OpSetGlobal: "SETG", + OpSetSelGlobal: "SETSG", + OpArray: "ARR", + OpMap: "MAP", + OpError: "ERROR", + OpIndex: "INDEX", + OpSliceIndex: "SLICE", + OpCall: "CALL", + OpReturn: "RET", + OpReturnValue: "RETVAL", + OpGetLocal: "GETL", + OpSetLocal: "SETL", + OpDefineLocal: "DEFL", + OpSetSelLocal: "SETSL", + OpGetBuiltin: "BUILTIN", + OpClosure: "CLOSURE", + OpGetFree: "GETF", + OpSetFree: "SETF", + OpSetSelFree: "SETSF", + OpIteratorInit: "ITER", + OpIteratorNext: "ITNXT", + OpIteratorKey: "ITKEY", + OpIteratorValue: "ITVAL", +} + +// OpcodeOperands is the number of operands. +var OpcodeOperands = [...][]int{ + OpConstant: {2}, + OpPop: {}, + OpTrue: {}, + OpFalse: {}, + OpAdd: {}, + OpSub: {}, + OpMul: {}, + OpDiv: {}, + OpRem: {}, + OpBAnd: {}, + OpBOr: {}, + OpBXor: {}, + OpBAndNot: {}, + OpBShiftLeft: {}, + OpBShiftRight: {}, + OpBComplement: {}, + OpEqual: {}, + OpNotEqual: {}, + OpGreaterThan: {}, + OpGreaterThanEqual: {}, + OpMinus: {}, + OpLNot: {}, + OpJumpFalsy: {2}, + OpAndJump: {2}, + OpOrJump: {2}, + OpJump: {2}, + OpNull: {}, + OpGetGlobal: {2}, + OpSetGlobal: {2}, + OpSetSelGlobal: {2, 1}, + OpArray: {2}, + OpMap: {2}, + OpError: {}, + OpIndex: {}, + OpSliceIndex: {}, + OpCall: {1}, + OpReturn: {}, + OpReturnValue: {1}, + OpGetLocal: {1}, + OpSetLocal: {1}, + OpDefineLocal: {1}, + OpSetSelLocal: {1, 1}, + OpGetBuiltin: {1}, + OpClosure: {2, 1}, + OpGetFree: {1}, + OpSetFree: {1}, + OpSetSelFree: {1, 1}, + OpIteratorInit: {}, + OpIteratorNext: {}, + OpIteratorKey: {}, + OpIteratorValue: {}, +} + +// ReadOperands reads operands from the bytecode. +func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) { + for _, width := range numOperands { + switch width { + case 1: + operands = append(operands, int(ReadUint8(ins[offset:]))) + case 2: + operands = append(operands, int(ReadUint16(ins[offset:]))) + } + + offset += width + } + + return +} + +// ReadUint16 reads uint16 from the byte slice. +func ReadUint16(b []byte) uint16 { + return uint16(b[1]) | uint16(b[0])<<8 +} + +// ReadUint8 reads uint8 from the byte slice. +func ReadUint8(b []byte) uint8 { + return uint8(b[0]) +} diff --git a/compiler/operands_test.go b/compiler/opcodes_test.go similarity index 79% rename from compiler/operands_test.go rename to compiler/opcodes_test.go index 1b5f97e..2ac868d 100644 --- a/compiler/operands_test.go +++ b/compiler/opcodes_test.go @@ -13,9 +13,8 @@ func TestReadOperands(t *testing.T) { func assertReadOperand(t *testing.T, opcode compiler.Opcode, operands []int, expectedBytes int) { inst := compiler.MakeInstruction(opcode, operands...) - def, ok := compiler.Lookup(opcode) - assert.True(t, ok) - operandsRead, read := compiler.ReadOperands(def, inst[1:]) + numOperands := compiler.OpcodeOperands[opcode] + operandsRead, read := compiler.ReadOperands(numOperands, inst[1:]) assert.Equal(t, expectedBytes, read) assert.Equal(t, operands, operandsRead) } diff --git a/compiler/operands.go b/compiler/operands.go deleted file mode 100644 index a442857..0000000 --- a/compiler/operands.go +++ /dev/null @@ -1,27 +0,0 @@ -package compiler - -// ReadOperands reads operands from the bytecode. -func ReadOperands(def *Definition, ins []byte) (operands []int, offset int) { - for _, width := range def.Operands { - switch width { - case 1: - operands = append(operands, int(ReadUint8(ins[offset:]))) - case 2: - operands = append(operands, int(ReadUint16(ins[offset:]))) - } - - offset += width - } - - return -} - -// ReadUint16 reads uint16 from the byte slice. -func ReadUint16(b []byte) uint16 { - return uint16(b[1]) | uint16(b[0])<<8 -} - -// ReadUint8 reads uint8 from the byte slice. -func ReadUint8(b []byte) uint8 { - return uint8(b[0]) -} diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index bc23c5c..2d8cff9 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -355,6 +355,9 @@ func (p *Parser) parseOperand() ast.Expr { case token.Func: // function literal return p.parseFuncLit() + + case token.Error: // error expression + return p.parseErrorExpr() } pos := p.pos @@ -427,7 +430,25 @@ func (p *Parser) parseArrayLit() ast.Expr { LBrack: lbrack, RBrack: rbrack, } +} +func (p *Parser) parseErrorExpr() ast.Expr { + pos := p.pos + + p.next() + + lparen := p.expect(token.LParen) + value := p.parseExpr() + rparen := p.expect(token.RParen) + + expr := &ast.ErrorExpr{ + ErrorPos: pos, + Expr: value, + LParen: lparen, + RParen: rparen, + } + + return expr } func (p *Parser) parseFuncType() *ast.FuncType { @@ -519,7 +540,7 @@ func (p *Parser) parseStmt() (stmt ast.Stmt) { switch p.token { case // simple statements - token.Func, token.Ident, token.Int, token.Float, token.Char, token.String, token.True, token.False, token.Undefined, token.LParen, // operands + token.Func, token.Error, token.Ident, token.Int, token.Float, token.Char, token.String, token.True, token.False, token.Undefined, token.LParen, // operands token.LBrace, token.LBrack, // composite types token.Add, token.Sub, token.Mul, token.And, token.Xor, token.Not: // unary operators s := p.parseSimpleStmt(false) diff --git a/compiler/parser/parser_error_test.go b/compiler/parser/parser_error_test.go new file mode 100644 index 0000000..a9bbb1d --- /dev/null +++ b/compiler/parser/parser_error_test.go @@ -0,0 +1,43 @@ +package parser_test + +import ( + "testing" + + "github.com/d5/tengo/compiler/ast" + "github.com/d5/tengo/compiler/token" +) + +func TestImport(t *testing.T) { + expect(t, `error(1234)`, func(p pfn) []ast.Stmt { + return stmts( + exprStmt( + errorExpr(p(1, 1), intLit(1234, p(1, 7)), p(1, 6), p(1, 11)))) + }) + + expect(t, `err1 := error("some error")`, func(p pfn) []ast.Stmt { + return stmts( + assignStmt( + exprs(ident("err1", p(1, 1))), + exprs(errorExpr(p(1, 9), stringLit("some error", p(1, 15)), p(1, 14), p(1, 27))), + token.Define, p(1, 6))) + }) + + expect(t, `return error("some error")`, func(p pfn) []ast.Stmt { + return stmts( + returnStmt(p(1, 1), + errorExpr(p(1, 8), stringLit("some error", p(1, 14)), p(1, 13), p(1, 26)))) + }) + + expect(t, `return error("some" + "error")`, func(p pfn) []ast.Stmt { + return stmts( + returnStmt(p(1, 1), + errorExpr(p(1, 8), + binaryExpr( + stringLit("some", p(1, 14)), + stringLit("error", p(1, 23)), + token.Add, p(1, 21)), + p(1, 13), p(1, 30)))) + }) + + expectError(t, `error()`) // must have a value +} diff --git a/compiler/parser/parser_test.go b/compiler/parser/parser_test.go index d284b38..c2069f2 100644 --- a/compiler/parser/parser_test.go +++ b/compiler/parser/parser_test.go @@ -244,6 +244,10 @@ func sliceExpr(x, low, high ast.Expr, lbrack, rbrack source.Pos) *ast.SliceExpr return &ast.SliceExpr{Expr: x, Low: low, High: high, LBrack: lbrack, RBrack: rbrack} } +func errorExpr(pos source.Pos, x ast.Expr, lparen, rparen source.Pos) *ast.ErrorExpr { + return &ast.ErrorExpr{Expr: x, ErrorPos: pos, LParen: lparen, RParen: rparen} +} + func selectorExpr(x, sel ast.Expr) *ast.SelectorExpr { return &ast.SelectorExpr{Expr: x, Sel: sel} } @@ -381,6 +385,11 @@ func equalExpr(t *testing.T, expected, actual ast.Expr) bool { case *ast.SelectorExpr: return equalExpr(t, expected.Expr, actual.(*ast.SelectorExpr).Expr) && equalExpr(t, expected.Sel, actual.(*ast.SelectorExpr).Sel) + case *ast.ErrorExpr: + return equalExpr(t, expected.Expr, actual.(*ast.ErrorExpr).Expr) && + assert.Equal(t, int(expected.ErrorPos), int(actual.(*ast.ErrorExpr).ErrorPos)) && + assert.Equal(t, int(expected.LParen), int(actual.(*ast.ErrorExpr).LParen)) && + assert.Equal(t, int(expected.RParen), int(actual.(*ast.ErrorExpr).RParen)) default: panic(fmt.Errorf("unknown type: %T", expected)) } diff --git a/compiler/token/tokens.go b/compiler/token/tokens.go index 77246e8..7bf1e72 100644 --- a/compiler/token/tokens.go +++ b/compiler/token/tokens.go @@ -73,6 +73,7 @@ const ( Else For Func + Error If Return Switch @@ -146,6 +147,7 @@ var tokens = [...]string{ Else: "else", For: "for", Func: "func", + Error: "error", If: "if", Return: "return", Switch: "switch", diff --git a/objects/builtin_is_error.go b/objects/builtin_is_error.go new file mode 100644 index 0000000..ffa3f2f --- /dev/null +++ b/objects/builtin_is_error.go @@ -0,0 +1,18 @@ +package objects + +import ( + "fmt" +) + +func builtinIsError(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, fmt.Errorf("wrong number of arguments (got=%d, want=1)", len(args)) + } + + switch args[0].(type) { + case *Error: + return TrueValue, nil + } + + return FalseValue, nil +} diff --git a/objects/builtin_is_undefined.go b/objects/builtin_is_undefined.go new file mode 100644 index 0000000..80c0c9f --- /dev/null +++ b/objects/builtin_is_undefined.go @@ -0,0 +1,18 @@ +package objects + +import ( + "fmt" +) + +func builtinIsUndefined(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, fmt.Errorf("wrong number of arguments (got=%d, want=1)", len(args)) + } + + switch args[0].(type) { + case *Undefined: + return TrueValue, nil + } + + return FalseValue, nil +} diff --git a/objects/builtins.go b/objects/builtins.go index 845ecb4..a2284e8 100644 --- a/objects/builtins.go +++ b/objects/builtins.go @@ -44,4 +44,12 @@ var Builtins = []struct { Name: "char", Func: builtinChar, }, + { + Name: "is_error", + Func: builtinIsError, + }, + { + Name: "is_undefined", + Func: builtinIsUndefined, + }, } diff --git a/objects/error.go b/objects/error.go new file mode 100644 index 0000000..be21de0 --- /dev/null +++ b/objects/error.go @@ -0,0 +1,47 @@ +package objects + +import ( + "fmt" + + "github.com/d5/tengo/compiler/token" +) + +// Error represents a string value. +type Error struct { + Value Object +} + +// TypeName returns the name of the type. +func (o *Error) TypeName() string { + return "error" +} + +func (o *Error) String() string { + if o.Value != nil { + return fmt.Sprintf("error: %s", o.Value.String()) + } + + return "error" +} + +// BinaryOp returns another object that is the result of +// a given binary operator and a right-hand side object. +func (o *Error) BinaryOp(op token.Token, rhs Object) (Object, error) { + return nil, ErrInvalidOperator +} + +// IsFalsy returns true if the value of the type is falsy. +func (o *Error) IsFalsy() bool { + return true // error is always false. +} + +// Copy returns a copy of the type. +func (o *Error) Copy() Object { + return &Error{Value: o.Value.Copy()} +} + +// Equals returns true if the value of the type +// is equal to the value of another object. +func (o *Error) Equals(x Object) bool { + return o == x // pointer equality +} diff --git a/objects/error_test.go b/objects/error_test.go new file mode 100644 index 0000000..db2d3d0 --- /dev/null +++ b/objects/error_test.go @@ -0,0 +1,19 @@ +package objects_test + +import ( + "testing" + + "github.com/d5/tengo/assert" + "github.com/d5/tengo/objects" +) + +func TestError_Equals(t *testing.T) { + err1 := &objects.Error{Value: &objects.String{Value: "some error"}} + err2 := err1 + assert.True(t, err1.Equals(err2)) + assert.True(t, err2.Equals(err1)) + + err2 = &objects.Error{Value: &objects.String{Value: "some error"}} + assert.False(t, err1.Equals(err2)) + assert.False(t, err2.Equals(err1)) +} diff --git a/objects/object_test.go b/objects/object_test.go index 55e6a8e..0159b2b 100644 --- a/objects/object_test.go +++ b/objects/object_test.go @@ -44,6 +44,8 @@ func TestObject_TypeName(t *testing.T) { assert.Equal(t, "return-value", o.TypeName()) o = &objects.Undefined{} assert.Equal(t, "undefined", o.TypeName()) + o = &objects.Error{} + assert.Equal(t, "error", o.TypeName()) } func TestObject_IsFalsy(t *testing.T) { @@ -92,6 +94,8 @@ func TestObject_IsFalsy(t *testing.T) { assert.False(t, o.IsFalsy()) o = &objects.Undefined{} assert.True(t, o.IsFalsy()) + o = &objects.Error{} + assert.True(t, o.IsFalsy()) } func TestObject_String(t *testing.T) { @@ -116,6 +120,10 @@ func TestObject_String(t *testing.T) { assert.Equal(t, "[]", o.String()) o = &objects.Map{Value: nil} assert.Equal(t, "{}", o.String()) + o = &objects.Error{Value: nil} + assert.Equal(t, "error", o.String()) + o = &objects.Error{Value: &objects.String{Value: "error 1"}} + assert.Equal(t, `error: "error 1"`, o.String()) o = &objects.StringIterator{} assert.Equal(t, "", o.String()) o = &objects.ArrayIterator{} @@ -173,4 +181,7 @@ func TestObject_BinaryOp(t *testing.T) { o = &objects.Undefined{} _, err = o.BinaryOp(token.Add, objects.UndefinedValue) assert.Error(t, err) + o = &objects.Error{} + _, err = o.BinaryOp(token.Add, objects.UndefinedValue) + assert.Error(t, err) } diff --git a/runtime/vm.go b/runtime/vm.go index 14ff18b..d0c2297 100644 --- a/runtime/vm.go +++ b/runtime/vm.go @@ -1,6 +1,7 @@ package runtime import ( + "errors" "fmt" "github.com/d5/tengo/compiler" @@ -562,6 +563,21 @@ func (v *VM) Run() error { v.stack[v.sp] = &m v.sp++ + case compiler.OpError: + value := v.stack[v.sp-1] + v.sp-- + + var err objects.Object = &objects.Error{ + Value: *value, + } + + if v.sp >= StackSize { + return ErrStackOverflow + } + + v.stack[v.sp] = &err + v.sp++ + case compiler.OpIndex: index := v.stack[v.sp-1] left := v.stack[v.sp-2] @@ -625,6 +641,19 @@ func (v *VM) Run() error { v.stack[v.sp] = &res v.sp++ + case *objects.Error: // err.value + key, ok := (*index).(*objects.String) + if !ok || key.Value != "value" { + return errors.New("invalid selector on error") + } + + if v.sp >= StackSize { + return ErrStackOverflow + } + + v.stack[v.sp] = &left.Value + v.sp++ + default: return fmt.Errorf("type %s does not support indexing", left.TypeName()) } diff --git a/runtime/vm_boolean_test.go b/runtime/vm_boolean_test.go index 7fb2fa6..2c61a62 100644 --- a/runtime/vm_boolean_test.go +++ b/runtime/vm_boolean_test.go @@ -30,4 +30,24 @@ func TestBoolean(t *testing.T) { expect(t, `out = (1 < 2) == false`, false) expect(t, `out = (1 > 2) == true`, false) expect(t, `out = (1 > 2) == false`, true) + + expectError(t, `5 + true`) + expectError(t, `5 + true; 5`) + expectError(t, `-true`) + expectError(t, `true + false`) + expectError(t, `5; true + false; 5`) + expectError(t, `if (10 > 1) { true + false; }`) + expectError(t, ` +if (10 > 1) { + if (10 > 1) { + return true + false; + } + + return 1; +} +`) + expectError(t, `if (true + false) { 10 }`) + expectError(t, `10 + (true + false)`) + expectError(t, `(true + false) + 20`) + expectError(t, `!(true + false)`) } diff --git a/runtime/vm_builtin_test.go b/runtime/vm_builtin_test.go index 4d75a81..c6a8abe 100644 --- a/runtime/vm_builtin_test.go +++ b/runtime/vm_builtin_test.go @@ -73,4 +73,10 @@ func TestBuiltinFunction(t *testing.T) { expect(t, `out = bool({a: 1})`, true) // non-empty maps: true expect(t, `out = bool({})`, false) // empty maps: false expect(t, `out = bool(undefined)`, false) // undefined: false + + expect(t, `out = is_error(error(1))`, true) + expect(t, `out = is_error(1)`, false) + + expect(t, `out = is_undefined(undefined)`, true) + expect(t, `out = is_undefined(error(1))`, false) } diff --git a/runtime/vm_error_test.go b/runtime/vm_error_test.go index dd80aca..b40a38f 100644 --- a/runtime/vm_error_test.go +++ b/runtime/vm_error_test.go @@ -5,37 +5,17 @@ import ( ) func TestError(t *testing.T) { - expectError(t, `5 + true`) + expect(t, `out = error(1)`, errorObject(1)) + expect(t, `out = error(1).value`, 1) + expect(t, `out = error("some error")`, errorObject("some error")) + expect(t, `out = error("some" + " error")`, errorObject("some error")) + expect(t, `out = func() { return error(5) }()`, errorObject(5)) + expect(t, `out = error(error("foo"))`, errorObject(errorObject("foo"))) + expect(t, `out = error("some error")`, errorObject("some error")) + expect(t, `out = error("some error").value`, "some error") + expect(t, `out = error("some error")["value"]`, "some error") - expectError(t, `5 + true; 5`) - - expectError(t, `-true`) - - expectError(t, `true + false`) - - expectError(t, `5; true + false; 5`) - - expectError(t, `if (10 > 1) { true + false; }`) - - expectError(t, ` -if (10 > 1) { - if (10 > 1) { - return true + false; - } - - return 1; -} -`) - - expectError(t, `if (true + false) { 10 }`) - - expectError(t, `10 + (true + false)`) - - expectError(t, `(true + false) + 20`) - - expectError(t, `!(true + false)`) - - expectError(t, `foobar`) - - expectError(t, `"foo" - "bar"`) + expectError(t, `error("error").err`) + expectError(t, `error("error").value_`) + expectError(t, `error([1,2,3])[1]`) } diff --git a/runtime/vm_string_test.go b/runtime/vm_string_test.go index d84cd11..112502e 100644 --- a/runtime/vm_string_test.go +++ b/runtime/vm_string_test.go @@ -41,4 +41,6 @@ func TestString(t *testing.T) { expectError(t, fmt.Sprintf("%s[%d:]", strStr, -1)) expectError(t, fmt.Sprintf("%s[:%d]", strStr, strLen+1)) expectError(t, fmt.Sprintf("%s[%d:%d]", strStr, 2, 1)) + + expectError(t, `"foo" - "bar"`) } diff --git a/runtime/vm_test.go b/runtime/vm_test.go index 569832c..3c05748 100644 --- a/runtime/vm_test.go +++ b/runtime/vm_test.go @@ -82,6 +82,10 @@ func runVMError(t *testing.T, file *ast.File) (ok bool) { return } +func errorObject(v interface{}) *objects.Error { + return &objects.Error{Value: toObject(v)} +} + func toObject(v interface{}) objects.Object { switch v := v.(type) { case objects.Object: @@ -212,7 +216,7 @@ func traceCompileRun(file *ast.File, symbols map[string]objects.Object) (res map globalsStr = append(globalsStr, fmt.Sprintf(" %s", l)) } } else { - globalsStr = append(globalsStr, fmt.Sprintf("[% 3d] %s (%s|%p)", gidx, *g, reflect.TypeOf(*g).Elem().Name(), g)) + globalsStr = append(globalsStr, fmt.Sprintf("[% 3d] %s (%s|%p)", gidx, (*g).String(), reflect.TypeOf(*g).Elem().Name(), g)) } } trace = append(trace, fmt.Sprintf("\n[Globals]\n\n%s", strings.Join(globalsStr, "\n"))) @@ -257,6 +261,8 @@ func objectZeroCopy(o objects.Object) objects.Object { return &objects.Map{} case *objects.Undefined: return &objects.Undefined{} + case *objects.Error: + return &objects.Error{} case nil: panic("nil") default: