Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[object/error] Add ability to rescue errors and introduce begin/rescue/end #142

Merged
merged 9 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ast/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions ast/begin.go
Original file line number Diff line number Diff line change
@@ -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()
}
9 changes: 9 additions & 0 deletions ast/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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()
}
16 changes: 16 additions & 0 deletions ast/rescue.go
Original file line number Diff line number Diff line change
@@ -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()
}
55 changes: 55 additions & 0 deletions docs/docs/literals/error.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
38 changes: 37 additions & 1 deletion docs/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
9 changes: 8 additions & 1 deletion evaluator/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
16 changes: 9 additions & 7 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 24 additions & 0 deletions object/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
},
}
}
11 changes: 11 additions & 0 deletions parser/begin.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 8 additions & 1 deletion parser/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
1 change: 1 addition & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ const (
IMPORT = "IMPORT"

NIL = "NIL"

BEGIN = "BEGIN"
RESCUE = "RESCUE"
)

var keywords = map[string]TokenType{
Expand All @@ -95,6 +98,8 @@ var keywords = map[string]TokenType{
"nil": NIL,
"and": AND,
"or": OR,
"begin": BEGIN,
"rescue": RESCUE,
}

func LookupIdent(ident string) TokenType {
Expand Down