Repository: abs-lang/abs
Pull request #332 (2020-03-22 23:03:41)

Allowing multiple decorators to be declared on a function
This allows multiple decorators to be declared on a
function, such as:

```
@debug()
@sleep()
@upper()
f greet(name) {
  echo("hello $name")
}
```


Patch:
diff --git a/.travis.yml b/.travis.yml
index 067e23b..e0470a9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,7 @@ os:
   - osx
 
 go:
-  - "1.13.x"
+  - "1.14.x"
 
 before_script:
 - go get -u 
diff --git a/ast/ast.go b/ast/ast.go
index 6cbbb4b..f6d541c 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -438,7 +438,7 @@ type Decorator struct {
 	Token     token.Token // @
 	Name      string
 	Arguments []Expression
-	Decorated *FunctionLiteral
+	Decorated Expression
 }
 
 func (dc *Decorator) expressionNode()      {}
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index 99af9bb..1b38002 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -291,25 +291,106 @@ func evalCompoundAssignment(node *ast.CompoundAssignment, env *object.Environmen
 }
 
 func evalDecorator(node *ast.Decorator, env *object.Environment) object.Object {
+	ident, fn, err := doEvalDecorator(node, env)
+
+	if isError(err) {
+		return err
+	}
+
+	env.Set(ident, fn)
+	return object.NULL
+}
+
+// This is the core of decorators. I would like
+// to refactor this at some point as I think there
+// is a much simpler way to go about this with
+// a simple touch of recursion -- without having
+// a special case for decorators of decorators
+// and for single decorators. Eventually we might
+// want to consider re-thinking the way the parser
+// works -- as currently it has 2 distinct entities,
+// functions and decorators. Ideally, the parser should
+// simply convert:
+
+// @deco()
+// f test() {}
+
+// into
+
+// f test() {}
+// test = deco(test)
+
+// which is much simpler to parse evaluate and does not
+// require any extra "entity" like a decorator. This
+// has many more implications and it's 2.55 AM so
+// let's call it for today...
+func doEvalDecorator(node *ast.Decorator, env *object.Environment) (string, object.Object, object.Object) {
 	decorator, ok := env.Get(node.Name)
 
 	if !ok {
-		return newError(node.Token, "function '%s' is not defined (used as decorator)", node.Name)
+		return "", nil, newError(node.Token, "function '%s' is not defined (used as decorator)", node.Name)
 	}
 
 	switch d := decorator.(type) {
 	case *object.Function:
-		fn := Eval(&ast.CallExpression{
-			Function:  d.Node,
-			Arguments: append([]ast.Expression{node.Decorated}, node.Arguments...),
-		}, env)
+		name, ok := getDecoratedName(node.Decorated)
 
-		env.Set(node.Decorated.Name, fn)
+		if !ok {
+			return "", nil, newError(node.Token, "error while processing decorator: unable to find the name of the function you're trying to decorate")
+		}
+
+		switch decorated := node.Decorated.(type) {
+		case *ast.FunctionLiteral:
+			// Here we have a single decorator
+			return name, Eval(&ast.CallExpression{
+				Function:  d.Node,
+				Arguments: append([]ast.Expression{decorated}, node.Arguments...),
+			}, env), nil
+		case *ast.Decorator:
+			// Here we have a decorator of another decorator
+			decoratorObj, _ := env.Get(node.Name)
+			decorator := decoratorObj.(*object.Function)
+
+			// First eval the later decorator(s).
+			fnName, fn, err := doEvalDecorator(node.Decorated.(*ast.Decorator), env)
+
+			if isError(err) {
+				return "", nil, err
+			}
 
-		return decorator
+			args := evalExpressions(node.Arguments, env)
+			return fnName, applyFunction(node.Token, decorator, env, append([]object.Object{fn}, args...)), nil
+		default:
+			return "", nil, newError(node.Token, "a decorator must decorate a named function or another decorator")
+		}
 	default:
-		return newError(node.Token, "decorator '%s' must be a function, %s given", node.Name, decorator.Type())
+		return "", nil, newError(node.Token, "decorator '%s' must be a function, %s given", node.Name, decorator.Type())
+	}
+}
+
+// Finds the actual name of the decorated function.
+//
+// Given this:
+// @deco1()
+// @deco2()
+// f hello() {}
+//
+// After we evaluate the decorators, we need to
+// re-assign the original function "hello", like this:
+//
+// hello = deco1(deco2(hello))
+//
+// This function traverses the decorators and finds the
+// name of the function we have to re-assign.
+func getDecoratedName(decorated ast.Expression) (string, bool) {
+	switch d := decorated.(type) {
+	case *ast.FunctionLiteral:
+		return d.Name, true
+	case *ast.Decorator:
+		return getDecoratedName(d.Decorated)
 	}
+
+	return "", false
 }
 
 // support index assignment expressions: a[0] = 1, h["a"] = 1
diff --git a/parser/parser.go b/parser/parser.go
index 2ae2f3f..27776fc 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -805,26 +805,24 @@ func (p *Parser) parseFunctionLiteral() ast.Expression {
 func (p *Parser) parseDecorator() ast.Expression {
 	dc := &ast.Decorator{Token: p.curToken}
 	// A decorator always preceeds a
-	// named function, so once we're done
+	// named function or another decorator,
+	// so once we're done
 	// with our "own" parsing, we can defer
-	// to the function literal parsing.
-	//
-	// Note that this code does not allow
-	// nested decorators, so we will probably
-	// want to change the implementation at some
-	// point, by "recursively" parsing the next
-	// block and seeing whether they are other
-	// decorators / functions.
+	// to parsing the next statement, which
+	// should either return another decorator
+	// or a function.
 	defer (func() {
 		p.nextToken()
-		exp := p.parseFunctionLiteral()
+		exp := p.parseExpressionStatement()
 
-		switch fn := exp.(type) {
+		switch fn := exp.Expression.(type) {
 		case *ast.FunctionLiteral:
 			if fn.Name == "" {
 				p.reportError("a decorator should decorate a named function", dc.Token)
 			}
 
+			dc.Decorated = fn
+		case *ast.Decorator:
 			dc.Decorated = fn
 		default:
 			p.reportError("a decorator should decorate a named function", dc.Token)


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index e87d436..5f0cbfb 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -742,6 +742,10 @@ func TestDecorators(t *testing.T) {
 		{"f decorator(fn) { return f () { return fn(...) * 2 } }; @decorator f test(x) { return x }; test(1)", 2},
 		{"f decorator(fn, multiplier) { return f () { return fn() * multiplier } }; @decorator(4) f test() { return 1 }; test()", 4},
 		{"f decorator(fn, multiplier) { return f () { return fn(...) * multiplier } }; @decorator(1000) f test(x) { return x }; test(1)", 1000},
+		{"f decorator(fn, multiplier) { return f () { return fn(...) * multiplier } }; @decorator(2) @decorator(2) f test(x) { return x }; test(1)", 4},
+		{"f decorator(fn, multiplier) { return f () { return fn(...) * multiplier } }; @decorator(2) @decorator(2) @decorator(2) f test(x) { return x }; test(1)", 8},
+		{"f multiply(fn, multiplier) { return f () { return fn(...) * multiplier } }; f divide(fn, div) { return f () { return fn(...) / div } }; @multiply(10) @divide(5)  f test(x) { return x }; test(1)", 2},
+		{"f decorator(fn) { return f () { return fn(...) } }; @decorator() @decorator_not_existing() f test(x) { return x }; test(1)", "function 'decorator_not_existing' is not defined (used as decorator)"},
 	}
 
 	for _, tt := range tests {
diff --git a/parser/parser_test.go b/parser/parser_test.go
index cb4236c..5a9eafc 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -1162,6 +1162,8 @@ func TestDecoratorParsing(t *testing.T) {
 		{input: "@decorator f hello(){}", name: "decorator", args: []string{}, decorated: "hello"},
 		{input: "@decorator('x', 'y') f hello(){}", name: "decorator", args: []string{"x", "y"}, decorated: "hello"},
 		{input: "@decorator('x', 'y') f (){}", err: "a decorator should decorate a named function"},
+		{input: "@decorator('x', 'y') @decorator('x', 'y') f hello(){}", name: "decorator", args: []string{"x", "y"}, decorated: "hello"},
+		{input: "@decorator('x', 'y') @decorator('x', 'y')", err: "a decorator should decorate a named function"},
 	}
 
 	for _, tt := range tests {
@@ -1195,14 +1197,46 @@ func TestDecoratorParsing(t *testing.T) {
 			t.Errorf("name parameter wrong. want %s, got=%s\n", tt.name, decorator.Name)
 		}
 
-		if decorator.Decorated.Name == "" {
-			t.Errorf("a decorator should have a decorated function")
-		}
+		switch decorated := decorator.Decorated.(type) {
+		case *ast.FunctionLiteral:
+			if decorated.Name == "" {
+				t.Errorf("a decorator should have a decorated function")
+			}
+
+			for i, arg := range tt.args {
+				if arg != decorator.Arguments[i].String() {
+					t.Errorf("wrong argument. want %s, got=%s\n", arg, decorator.Arguments[i])
+				}
+			}
+		case *ast.Decorator:
+			// Here we have a double decorator:
+			// @dec(...)
+			// @dec(...)
+			// f function(){}
+			// For the sake of keeping tests simple, the
+			// 2 decorators must be identical, as here we're
+			// just testing decorators can decorate either functions
+			// or other decorators
+			if len(decorator.Arguments) != len(tt.args) {
+				t.Errorf("length arguments wrong. want %d, got=%d\n", len(tt.args), len(decorator.Arguments))
+			}
 
-		for i, arg := range tt.args {
-			if arg != decorator.Arguments[i].String() {
-				t.Errorf("wrong argument. want %s, got=%s\n", arg, decorator.Arguments[i])
+			if tt.name != decorator.Name {
+				t.Errorf("name parameter wrong. want %s, got=%s\n", tt.name, decorator.Name)
 			}
+
+			realDecorated := decorated.Decorated.(*ast.FunctionLiteral)
+			if realDecorated.Name == "" {
+				t.Errorf("a decorator should have a realDecorated function")
+			}
+
+			for i, arg := range tt.args {
+				if arg != decorator.Arguments[i].String() {
+					t.Errorf("wrong argument. want %s, got=%s\n", arg, decorator.Arguments[i])
+				}
+			}
+		default:
+			t.Errorf("unhandled type %T", decorated)
 		}
 	}
 }