diff --git a/ast/ast_test.go b/ast/ast_test.go index 986eabb..5a0c8d9 100644 --- a/ast/ast_test.go +++ b/ast/ast_test.go @@ -56,6 +56,10 @@ func TestString(t *testing.T) { "break", "break", }, + { + "begin\ntrue\nrescue e\nfalse\nend", + "begin\ntrue\nrescue e\nfalse\nend", + }, } for _, tt := range tests { diff --git a/ast/begin.go b/ast/begin.go new file mode 100644 index 0000000..2274955 --- /dev/null +++ b/ast/begin.go @@ -0,0 +1,23 @@ +package ast + +import ( + "bytes" + + "github.com/flipez/rocket-lang/token" +) + +type Begin struct { + Token token.Token + Block *Block +} + +func (b *Begin) TokenLiteral() string { return b.Token.Literal } +func (b *Begin) String() string { + var out bytes.Buffer + + out.WriteString("begin\n") + out.WriteString(b.Block.String()) + out.WriteString("\nend") + + return out.String() +} diff --git a/ast/block.go b/ast/block.go index d2ea08d..7ae761f 100644 --- a/ast/block.go +++ b/ast/block.go @@ -9,6 +9,7 @@ import ( type Block struct { Token token.Token // the { token Statements []Statement + Rescue *Rescue } func (bs *Block) TokenLiteral() string { return bs.Token.Literal } @@ -19,5 +20,13 @@ func (bs *Block) String() string { out.WriteString(s.String()) } + if bs.Rescue != nil { + out.WriteString("\nrescue ") + out.WriteString(bs.Rescue.ErrorIdent.Literal + "\n") + for _, s := range bs.Rescue.Block.Statements { + out.WriteString(s.String()) + } + } + return out.String() } diff --git a/ast/rescue.go b/ast/rescue.go new file mode 100644 index 0000000..b5e963b --- /dev/null +++ b/ast/rescue.go @@ -0,0 +1,16 @@ +package ast + +import ( + "github.com/flipez/rocket-lang/token" +) + +type Rescue struct { + Token token.Token // the { token + ErrorIdent token.Token + Block *Block +} + +func (r *Rescue) TokenLiteral() string { return r.Token.Literal } +func (r *Rescue) String() string { + return r.Block.String() +} diff --git a/docs/docs/literals/error.md b/docs/docs/literals/error.md index c179f95..e3edbbd 100644 --- a/docs/docs/literals/error.md +++ b/docs/docs/literals/error.md @@ -1,10 +1,65 @@ # Error +An Error is created by RocketLang if unallowed or invalid code is run. +An error does often replace the original return value of a function or identifier. +The documentation of those functions does indicate ERROR as a potential return value. +A program can rescue from errors within a block or alter it's behavior within other blocks like 'if' or 'def'. + +```js +def test() + puts(nope) +rescue e + puts("Got error: '" + e.msg() + "'") +end + +test() + +=> "Got error in if: 'identifier not found: error'" + +if (true) + nope() +rescue your_name + puts("Got error in if: '" + your_name.msg() + "'") +end + +=> "Got error in if: 'identifier not found: nope'" + +begin + puts(nope) +rescue e + puts("rescue") +end + +=> "rescue" + +``` + ## Literal Specific Methods +### msg() +> Returns `STRING` + +Returns the error message + +:::caution +Please note that performing `.msg()` on a ERROR object does result in a STRING object which then will no longer be treated as an error! +::: + + +```js +» def () +puts(nope) +rescue e +puts((rescued error: + e.msg())) +end +🚀 » test() +"rescued error:identifier not found: nope" +``` + + ## Generic Literal Methods diff --git a/docs/generate.go b/docs/generate.go index d00e288..4c5a90a 100644 --- a/docs/generate.go +++ b/docs/generate.go @@ -148,7 +148,43 @@ is_true = a != b;`, DefaultMethods: default_methods} create_doc("docs/templates/literal.md", "docs/docs/literals/boolean.md", tempData) - tempData = templateData{Title: "Error", LiteralMethods: error_methods, DefaultMethods: default_methods} + tempData = templateData{ + Title: "Error", + Description: `An Error is created by RocketLang if unallowed or invalid code is run. +An error does often replace the original return value of a function or identifier. +The documentation of those functions does indicate ERROR as a potential return value. + +A program can rescue from errors within a block or alter it's behavior within other blocks like 'if' or 'def'. +`, + Example: `def test() + puts(nope) +rescue e + puts("Got error: '" + e.msg() + "'") +end + +test() + +=> "Got error in if: 'identifier not found: error'" + +if (true) + nope() +rescue your_name + puts("Got error in if: '" + your_name.msg() + "'") +end + +=> "Got error in if: 'identifier not found: nope'" + +begin + puts(nope) +rescue e + puts("rescue") +end + +=> "rescue" +`, + LiteralMethods: error_methods, + DefaultMethods: default_methods, + } create_doc("docs/templates/literal.md", "docs/docs/literals/error.md", tempData) tempData = templateData{ diff --git a/evaluator/block.go b/evaluator/block.go index c142b92..5a43130 100644 --- a/evaluator/block.go +++ b/evaluator/block.go @@ -13,7 +13,14 @@ func evalBlock(block *ast.Block, env *object.Environment) object.Object { if result != nil { rt := result.Type() - if rt == object.RETURN_VALUE_OBJ || rt == object.ERROR_OBJ || rt == object.BREAK_VALUE_OBJ || rt == object.NEXT_VALUE_OBJ { + if rt == object.ERROR_OBJ { + if block.Rescue != nil { + env.Set(block.Rescue.ErrorIdent.Literal, result) + result = evalBlock(block.Rescue.Block, env) + } + return result + } + if rt == object.RETURN_VALUE_OBJ || rt == object.BREAK_VALUE_OBJ || rt == object.NEXT_VALUE_OBJ { return result } } diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index a4e75c1..9de8d4d 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -16,6 +16,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return Eval(node.Expression, env) case *ast.Block: return evalBlock(node, env) + case *ast.Begin: + return evalBlock(node.Block, env) case *ast.Foreach: return evalForeach(node, env) case *ast.While: @@ -209,13 +211,13 @@ func evalBangOperatorExpression(right object.Object) object.Object { } func evalMinusPrefixOperatorExpression(right object.Object) object.Object { - switch val := right.(type) { - case *object.Integer: - return object.NewInteger(-val.Value) - case *object.Float: - return object.NewFloat(-val.Value) - } - return object.NewErrorFormat("unknown operator: -%s", right.Type()) + switch val := right.(type) { + case *object.Integer: + return object.NewInteger(-val.Value) + case *object.Float: + return object.NewFloat(-val.Value) + } + return object.NewErrorFormat("unknown operator: -%s", right.Type()) } func nativeBoolToBooleanObject(input bool) *object.Boolean { diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index dc2999d..1a5505b 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -218,6 +218,9 @@ func TestErrorHandling(t *testing.T) { {"break(1.nope())", "undefined method `.nope()` for INTEGER"}, {"next(1.nope())", "undefined method `.nope()` for INTEGER"}, {"nil.nope()", "undefined method `.nope()` for NIL"}, + {"begin puts(nope) end", "identifier not found: nope"}, + {"begin puts(nope) rescue e e.nope() end", "undefined method `.nope()` for ERROR"}, + {"a = begin puts(nope) rescue e e.msg() end; a.nope()", "undefined method `.nope()` for STRING"}, } for _, tt := range tests { diff --git a/object/error.go b/object/error.go index c6cdd57..8e06cbd 100644 --- a/object/error.go +++ b/object/error.go @@ -19,3 +19,27 @@ func (e *Error) Inspect() string { return "ERROR: " + e.Message } func (e *Error) InvokeMethod(method string, env Environment, args ...Object) Object { return objectMethodLookup(e, method, env, args) } + +func init() { + objectMethods[ERROR_OBJ] = map[string]ObjectMethod{ + "msg": ObjectMethod{ + Layout: MethodLayout{ + Description: "Returns the error message\n\n:::caution\nPlease note that performing `.msg()` on a ERROR object does result in a STRING object which then will no longer be treated as an error!\n:::", + Example: `» def test() +puts(nope) +rescue e +puts((rescued error: + e.msg())) +end +🚀 » test() +"rescued error:identifier not found: nope"`, + ReturnPattern: Args( + Arg(STRING_OBJ), + ), + }, + method: func(o Object, _ []Object, _ Environment) Object { + e := o.(*Error) + return NewString(e.Message) + }, + }, + } +} diff --git a/parser/begin.go b/parser/begin.go new file mode 100644 index 0000000..9945bc4 --- /dev/null +++ b/parser/begin.go @@ -0,0 +1,11 @@ +package parser + +import ( + "github.com/flipez/rocket-lang/ast" +) + +func (p *Parser) parseBegin() ast.Expression { + expression := &ast.Begin{Token: p.curToken} + expression.Block = p.parseBlock() + return expression +} diff --git a/parser/block.go b/parser/block.go index 01d02c6..7b8a317 100644 --- a/parser/block.go +++ b/parser/block.go @@ -11,7 +11,7 @@ func (p *Parser) parseBlock() *ast.Block { p.nextToken() - for !p.curTokenIs(token.RBRACE) && !p.curTokenIs(token.EOF) && !p.curTokenIs(token.END) && !p.curTokenIs(token.ELSE) { + for !p.curTokenIs(token.RBRACE) && !p.curTokenIs(token.EOF) && !p.curTokenIs(token.END) && !p.curTokenIs(token.ELSE) && !p.curTokenIs(token.RESCUE) { stmt := p.parseStatement() if stmt != nil { block.Statements = append(block.Statements, stmt) @@ -20,5 +20,12 @@ func (p *Parser) parseBlock() *ast.Block { p.nextToken() } + if p.curTokenIs(token.RESCUE) { + block.Rescue = &ast.Rescue{Token: p.curToken} + p.expectPeek(token.IDENT) + block.Rescue.ErrorIdent = p.curToken + block.Rescue.Block = p.parseBlock() + } + return block } diff --git a/parser/parser.go b/parser/parser.go index 5fe0e71..cdc0234 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -89,6 +89,7 @@ func New(l *lexer.Lexer, imports map[string]struct{}) *Parser { p.registerPrefix(token.LBRACE, p.parseHash) p.registerPrefix(token.IMPORT, p.parseImport) p.registerPrefix(token.NIL, p.parseNil) + p.registerPrefix(token.BEGIN, p.parseBegin) p.infixParseFns = make(map[token.TokenType]infixParseFn) p.registerInfix(token.ASSIGN, p.parseAssignExpression) diff --git a/token/token.go b/token/token.go index f72fc6c..c557c0b 100644 --- a/token/token.go +++ b/token/token.go @@ -75,6 +75,9 @@ const ( IMPORT = "IMPORT" NIL = "NIL" + + BEGIN = "BEGIN" + RESCUE = "RESCUE" ) var keywords = map[string]TokenType{ @@ -95,6 +98,8 @@ var keywords = map[string]TokenType{ "nil": NIL, "and": AND, "or": OR, + "begin": BEGIN, + "rescue": RESCUE, } func LookupIdent(ident string) TokenType {