diff --git a/ast/ast.go b/ast/ast.go index 0396cb5d..1f6cff23 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -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 { @@ -275,6 +287,7 @@ type MethodExpression struct { Method Expression Arguments []Expression Optional bool + Deferred } func (me *MethodExpression) expressionNode() {} @@ -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() {} @@ -479,6 +493,7 @@ type CallExpression struct { Token token.Token // The '(' token Function Expression // Identifier or FunctionLiteral Arguments []Expression + Deferred } func (ce *CallExpression) expressionNode() {} diff --git a/docs/src/.vuepress/config.js b/docs/src/.vuepress/config.js index dc6a7e29..66abc14c 100755 --- a/docs/src/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -82,6 +82,7 @@ module.exports = { 'syntax/system-commands', 'syntax/operators', 'syntax/comments', + 'syntax/defer', ] }, { diff --git a/docs/src/.vuepress/public/abs.wasm b/docs/src/.vuepress/public/abs.wasm index eb267330..1e789f63 100755 Binary files a/docs/src/.vuepress/public/abs.wasm and b/docs/src/.vuepress/public/abs.wasm differ diff --git a/docs/src/docs/syntax/defer.md b/docs/src/docs/syntax/defer.md new file mode 100644 index 00000000..0fec6a3b --- /dev/null +++ b/docs/src/docs/syntax/defer.md @@ -0,0 +1,57 @@ +--- +permalink: /syntax/defer +--- + +# Defer + +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). \ No newline at end of file diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index ab54440b..0a64785c 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -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 } diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 1f424f7f..2a4d323b 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -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 @@ -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; diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 14e1abb9..5247b85c 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -119,6 +119,7 @@ f hello(x, y) { 1 !in [] !in_variable_named_in !i +defer fn ` tests := []struct { @@ -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, ""}, } diff --git a/parser/parser.go b/parser/parser.go index 6788d549..92fd1b84 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -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) @@ -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} diff --git a/parser/parser_test.go b/parser/parser_test.go index b0eefbbb..c3280b92 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -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) @@ -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) { diff --git a/token/token.go b/token/token.go index 69a8e14e..660d04d9 100644 --- a/token/token.go +++ b/token/token.go @@ -83,6 +83,7 @@ const ( NOT_IN = "NOT_IN" BREAK = "BREAK" CONTINUE = "CONTINUE" + DEFER = "DEFER" ) type Token struct { @@ -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