Skip to content

Commit

Permalink
Ability to defer functions, closes #403
Browse files Browse the repository at this point in the history
Sometimes it is very helpful to guarantee a certain function is executed
regardless of what code path we take: you can use the `defer` keyword for
this.

```py
echo(1)
defer echo(3)
echo(2)
```

When you schedule a function to be deferred, it will executed right at
the end of the current scope. A `defer` inside a function will then
execute at the end of that function itself:

```py
echo(1)
f fn() {
    defer echo(3)
    echo(2)
}
fn()
echo(4)
```

You can `defer` any callable: a function call, a method or even a system
command. This can be very helpful if you need to run a cleanup function
right before wrapping up with your code:

```sh
defer `rm my-file.txt`
"some text" > "my-file.txt"

...
...
"some other text" >> "my-file.txt"
```

In this case, you will be guaranteed to execute the command that removes
`my-file.txt` before the program closes.

Be aware that code that is deferred does not have access to the return value
of its scope, and will supress errors -- if a `defer` block messes up you're
not going to see any error. This behavior is experimental, but we would most
likely like to give this kind of control through [try...catch...finally](#118).
  • Loading branch information
odino committed May 24, 2021
1 parent 8d75408 commit 878f489
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 12 deletions.
15 changes: 15 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ type Expression interface {
expressionNode()
}

type Deferrable interface {
IsDeferred() bool
SetDeferred(bool)
}

type Deferred struct {
Defer bool
}

func (d *Deferred) IsDeferred() bool { return d.Defer }
func (d *Deferred) SetDeferred(deferred bool) { d.Defer = deferred }

// Represents the whole program
// as a bunch of statements
type Program struct {
Expand Down Expand Up @@ -275,6 +287,7 @@ type MethodExpression struct {
Method Expression
Arguments []Expression
Optional bool
Deferred
}

func (me *MethodExpression) expressionNode() {}
Expand Down Expand Up @@ -416,6 +429,7 @@ func (fe *ForExpression) String() string {
type CommandExpression struct {
Token token.Token // The command itself
Value string
Deferred
}

func (ce *CommandExpression) expressionNode() {}
Expand Down Expand Up @@ -479,6 +493,7 @@ type CallExpression struct {
Token token.Token // The '(' token
Function Expression // Identifier or FunctionLiteral
Arguments []Expression
Deferred
}

func (ce *CallExpression) expressionNode() {}
Expand Down
1 change: 1 addition & 0 deletions docs/src/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ module.exports = {
'syntax/system-commands',
'syntax/operators',
'syntax/comments',
'syntax/defer',
]
},
{
Expand Down
Binary file modified docs/src/.vuepress/public/abs.wasm
Binary file not shown.
57 changes: 57 additions & 0 deletions docs/src/docs/syntax/defer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
permalink: /syntax/defer
---

# Defer <Badge text="experimental" type="warning"/>

Sometimes it is very helpful to guarantee a certain function is executed
regardless of what code path we take: you can use the `defer` keyword for
this.

```py
echo(1)
defer echo(3)
echo(2)
# 1
# 2
# 3
```

When you schedule a function to be deferred, it will executed right at
the end of the current scope. A `defer` inside a function will then
execute at the end of that function itself:

```py
echo(1)
f fn() {
defer echo(3)
echo(2)
}
fn()
echo(4)
# 1
# 2
# 3
# 4
```

You can `defer` any callable: a function call, a method or even a system
command. This can be very helpful if you need to run a cleanup function
right before wrapping up with your code:

```sh
defer `rm my-file.txt`
"some text" > "my-file.txt"

...
...
"some other text" >> "my-file.txt"
```

In this case, you will be guaranteed to execute the command that removes
`my-file.txt` before the program closes.

Be aware that code that is deferred does not have access to the return value
of its scope, and will supress errors -- if a `defer` block messes up you're
not going to see any error. This behavior is experimental, but we would most
likely like to give this kind of control through [try...catch...finally](https://github.com/abs-lang/abs/issues/118).
42 changes: 38 additions & 4 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,36 +231,70 @@ func Eval(node ast.Node, env *object.Environment) object.Object {

func evalProgram(program *ast.Program, env *object.Environment) object.Object {
var result object.Object
deferred := []*ast.ExpressionStatement{}

loop:
for _, statement := range program.Statements {
x, ok := statement.(*ast.ExpressionStatement)

if ok {
if d, ok := x.Expression.(ast.Deferrable); ok && d.IsDeferred() {
deferred = append(deferred, x)
continue
}
}
result = Eval(statement, env)
switch result := result.(type) {

switch ret := result.(type) {
case *object.ReturnValue:
return result.Value
result = ret.Value
break loop
case *object.Error:
return result
break loop
}
}

for _, statement := range deferred {
Eval(statement, env)
}

return result
}

// This should fundamentally be using the same function as evalProgram,
// but there are some subtle difference on how they brak / handle return
// values. You will see a lot of repeated code between the 2, especially
// since we introduced `defer` which adds a bit of complexity to both.
func evalBlockStatement(
block *ast.BlockStatement,
env *object.Environment,
) object.Object {
var result object.Object
deferred := []*ast.ExpressionStatement{}

for _, statement := range block.Statements {
x, ok := statement.(*ast.ExpressionStatement)

if ok {
if d, ok := x.Expression.(ast.Deferrable); ok && d.IsDeferred() {
deferred = append(deferred, x)
continue
}
}
result = Eval(statement, env)

if result != nil {
rt := result.Type()
if rt == object.RETURN_VALUE_OBJ || rt == object.ERROR_OBJ {
return result
break
}
}
}

for _, statement := range deferred {
Eval(statement, env)
}

return result
}

Expand Down
31 changes: 27 additions & 4 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,29 @@ func TestStringInterpolation(t *testing.T) {
}
}

func TestDeferredFunctions(t *testing.T) {
tests := []struct {
input string
expected string
}{
// you can use functions
{`x = {"test": ""}; f deferred() { x.test += "c"}; f fn() { x.test += "a"; defer deferred(); x.test += "b"}; fn(); x.test`, "abc"},
{`x = {"test": ""}; f deferred() { x.test += "c"}; f fn() { x.test += "a"; defer deferred(); x.test += "b"; return;}; fn(); x.test`, "abc"},
{`x = {"test": ""}; f deferred() { x.test += "c"}; f fn() { x.test += "a"; defer deferred(); return; x.test += "b"}; fn(); x.test`, "ac"},
// you can use commands
{"f test() { 'a' > 'test-ignore-defer.abs'; defer `echo c >> test-ignore-defer.abs`; 'b' >> 'test-ignore-defer.abs' }; test(); `cat test-ignore-defer.abs`", "abc"},
// let's just test you can use methods
{`x = {"test": ""}; f fn() { x.test += "a"; defer "".upper(); x.test += "b"}; fn(); x.test`, "ab"},
// you can use defer in the main scope
{`x = {"test": "a"}; defer f() {x.test += "c"}; x.test += "b"; x.test`, "ab"},
}

for _, tt := range tests {
evaluated := testEval(tt.input)
testStringObject(t, evaluated, tt.expected)
}
}

func TestForInExpressions(t *testing.T) {
tests := []struct {
input string
Expand Down Expand Up @@ -541,17 +564,17 @@ func TestReturnStatements(t *testing.T) {
expected interface{}
}{
{`
if false {
if true {
return
}
return 3
`, 3},
`, nil},
{`
if true {
if false {
return
}
return 3
`, nil},
`, 3},
{`
if false {
return;
Expand Down
3 changes: 3 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ f hello(x, y) {
1 !in []
!in_variable_named_in
!i
defer fn
`

tests := []struct {
Expand Down Expand Up @@ -414,6 +415,8 @@ f hello(x, y) {
{token.IDENT, "in_variable_named_in"},
{token.BANG, "!"},
{token.IDENT, "i"},
{token.DEFER, "defer"},
{token.IDENT, "fn"},
{token.EOF, ""},
}

Expand Down
15 changes: 15 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func New(l *lexer.Lexer) *Parser {
p.registerPrefix(token.CONTINUE, p.parseContinue)
p.registerPrefix(token.CURRENT_ARGS, p.parseCurrentArgsLiteral)
p.registerPrefix(token.AT, p.parseDecorator)
p.registerPrefix(token.DEFER, p.parseDefer)

p.infixParseFns = make(map[token.TokenType]infixParseFn)
p.registerInfix(token.PLUS, p.parseInfixExpression)
Expand Down Expand Up @@ -848,6 +849,20 @@ func (p *Parser) parseDecorator() ast.Expression {
return dc
}

// defer fn()
func (p *Parser) parseDefer() ast.Expression {
p.nextToken()
exp := p.parseExpression(0)

if d, ok := exp.(ast.Deferrable); ok {
d.SetDeferred(true)
} else {
p.reportError("you can only defer a call: defer some.method() | defer `some command` | defer some_fn()", p.curToken)
}

return exp
}

// ...
func (p *Parser) parseCurrentArgsLiteral() ast.Expression {
return &ast.CurrentArgsLiteral{Token: p.curToken}
Expand Down
19 changes: 15 additions & 4 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ func TestForElseExpression(t *testing.T) {
}

func TestFunctionLiteralParsing(t *testing.T) {
input := `f(x, y = 2) { x + y; }`
input := `f(x, y = 2) { defer f() {echo(1)}(); x + y; }`

l := lexer.New(input)
p := New(l)
Expand Down Expand Up @@ -1012,18 +1012,29 @@ func TestFunctionLiteralParsing(t *testing.T) {
testParameter(t, function.Parameters[0], "x")
testParameter(t, function.Parameters[1], "y = 2")

if len(function.Body.Statements) != 1 {
if len(function.Body.Statements) != 2 {
t.Fatalf("function.Body.Statements has not 1 statements. got=%d\n",
len(function.Body.Statements))
}

bodyStmt, ok := function.Body.Statements[0].(*ast.ExpressionStatement)
stmt, ok = function.Body.Statements[0].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("function body stmt is not ast.ExpressionStatement. got=%T",
function.Body.Statements[0])
}

testInfixExpression(t, bodyStmt.Expression, "x", "+", "y")
_, ok = stmt.Expression.(*ast.CallExpression)
if !ok {
t.Fatalf("expression should be a call expression, got=%T", stmt.Expression)
}

secondExpr, ok := function.Body.Statements[1].(*ast.ExpressionStatement)
if !ok {
t.Fatalf("function body stmt is not ast.ExpressionStatement. got=%T",
function.Body.Statements[1])
}

testInfixExpression(t, secondExpr.Expression, "x", "+", "y")
}

func TestCommandParsing(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const (
NOT_IN = "NOT_IN"
BREAK = "BREAK"
CONTINUE = "CONTINUE"
DEFER = "DEFER"
)

type Token struct {
Expand All @@ -104,6 +105,7 @@ var keywords = map[string]TokenType{
"null": NULL,
"break": BREAK,
"continue": CONTINUE,
"defer": DEFER,
}

// NumberAbbreviations is a list of abbreviations that can be used in numbers eg. 1k, 20B
Expand Down

0 comments on commit 878f489

Please sign in to comment.