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

Ability to defer functions, closes #403 #431

Merged
merged 2 commits into from
May 24, 2021
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
24 changes: 24 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ type Expression interface {
expressionNode()
}

// Deferrable is used to be able to
// check whether an expression needs
// to be executed now or at the end
// of the scope
type Deferrable interface {
IsDeferred() bool
SetDeferred(bool)
}

// Deferred is a struct that can be embedded
// in order to define whether the current node
// needs to be executed right away or whether
// it should be deferred until the end of the
// current scope.
type Deferred struct {
deferred bool
}

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

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

func (me *MethodExpression) expressionNode() {}
Expand Down Expand Up @@ -416,6 +438,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 +502,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