Repository: abs-lang/abs
Pull request #319 (2020-02-14 11:31:03)

Named functions
Functions can now be "named":

```js
f my_func(x) {
  return x**2
}
```

Earlier, you would have had to assign
the function to a variable:

```js
my_func = f(x) {
  return x**2
}
```

while right now function names are first-class
citizens. For example, the string representation
of a function will actualy include its name (to be
included in stacktraces as well):

```console
⧐  f test() {echo(1)}
f test() {echo(1)}
```


Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 6deb1c6..6cbbb4b 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -410,6 +410,7 @@ func (ce *CommandExpression) String() string {
 
 type FunctionLiteral struct {
 	Token      token.Token // The 'fn' token
+	Name       string      // identifier for this function
 	Parameters []*Identifier
 	Body       *BlockStatement
 }
@@ -433,6 +434,43 @@ func (fl *FunctionLiteral) String() string {
 	return out.String()
 }
 
+type Decorator struct {
+	Token     token.Token // @
+	Name      string
+	Arguments []Expression
+	Decorated *FunctionLiteral
+}
+
+func (dc *Decorator) expressionNode()      {}
+func (dc *Decorator) TokenLiteral() string { return dc.Token.Literal }
+func (dc *Decorator) String() string {
+	var out bytes.Buffer
+
+	args := []string{}
+	for _, a := range dc.Arguments {
+		args = append(args, a.String())
+	}
+
+	out.WriteString(dc.TokenLiteral())
+	out.WriteString(dc.Name)
+	out.WriteString("(")
+	out.WriteString(strings.Join(args, ", "))
+	out.WriteString(") ")
+	out.WriteString(dc.Decorated.String())
+
+	return out.String()
+}
+
+type CurrentArgsLiteral struct {
+	Token token.Token // ...
+}
+
+func (cal *CurrentArgsLiteral) expressionNode()      {}
+func (cal *CurrentArgsLiteral) TokenLiteral() string { return cal.Token.Literal }
+func (cal *CurrentArgsLiteral) String() string {
+	return "..."
+}
+
 type CallExpression struct {
 	Token     token.Token // The '(' token
 	Function  Expression  // Identifier or FunctionLiteral
diff --git a/docs/_includes/toc.md b/docs/_includes/toc.md
index 89a31e8..fcbc8a3 100644
--- a/docs/_includes/toc.md
+++ b/docs/_includes/toc.md
@@ -25,6 +25,7 @@
 * [Hash](/types/hash)
 * [Functions](/types/function)
 * [Builtin functions](/types/builtin-function)
+* [Decorators](/types/decorator)
 
 ## Miscellaneous
 
diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md
index 93e756b..f4db36c 100644
--- a/docs/types/builtin-function.md
+++ b/docs/types/builtin-function.md
@@ -353,4 +353,4 @@ statements until changed.
 
 That's about it for this section!
 
-You can now head over to read a little bit about [how to install 3rd party libraries](/misc/3pl).
+You can now head over to read a little bit about [decorators](/types/decorator).
diff --git a/docs/types/decorator.md b/docs/types/decorator.md
new file mode 100644
index 0000000..08aa2f1
--- /dev/null
+++ b/docs/types/decorator.md
@@ -0,0 +1,73 @@
+# Decorator
+
+Decorators are a feature built on top of
+ABS' functions -- they're not a type *per se*.
+
+A decorator is a function that "wraps" another
+function, allowing you to enhance the original
+function's functionality with the decorator's
+one.
+
+An example could be a decorator that logs how
+long a function takes to execute, or delays
+execution altogether.
+
+## Declaring decorators
+
+A decorator is a plain-old function that
+accepts `1 + N` arguments, where `1` is the
+function being wrapped, and returns a new
+function that wraps the original one:
+
+```py
+f log_if_slow(original_fn, treshold_ms) {
+    return f() {
+        start = `date +%s%3N`.int()
+        res = original_fn(...)
+        end = `date +%s%3N`.int()
+
+        if end - start > treshold_ms {
+            echo("mmm, we were pretty slow...")
+        }
+
+        return res
+    }
+}
+```
+
+That's as simple as that: a named function 
+that returns a new function that executes the
+decorated one (`original_fn`) and returns its
+result, while logging if it takes longer than
+a few milliseconds.
+
+## Using decorators
+
+Now that we've declared our decorator, it's time
+to use it, through the `@` notation:
+
+```py
+@log_if_slow(500)
+f return_random_number_after_sleeping(seconds) {
+    `sleep $seconds`
+    return rand(1000)
+}
+```
+
+and we can test our decorator has takn the stage:
+
+```console
+⧐  return_random_number_after_sleeping(0)
+493
+⧐  return_random_number_after_sleeping(1)
+mmm, we were pretty slow...
+371
+```
+
+Decorators are heavily inspired by [Python](https://www.python.org/dev/peps/pep-0318/).
+
+## Next
+
+That's about it for this section!
+
+You can now head over to read a little bit about [how to install 3rd party libraries](/misc/3pl).
\ No newline at end of file
diff --git a/docs/types/function.md b/docs/types/function.md
index 73f4eef..e774079 100644
--- a/docs/types/function.md
+++ b/docs/types/function.md
@@ -32,7 +32,8 @@ Functions must be called with the right number of arguments:
 
 ``` bash
 fn = f(x) { x }
-fn() # ERROR: Wrong number of arguments passed to f(x) {
+fn()
+# ERROR: Wrong number of arguments passed to f(x) {
 # x
 # }. Want [x], got []
 ```
@@ -80,6 +81,91 @@ func = f(x) {
 }
 ```
 
+## Named functions
+
+You can create named functions by specifying an identifier
+after the `f` keyword:
+
+``` bash
+f greeter(name) {
+    echo("Hello $name!")
+}
+
+greeter(`whoami`) # "Hello root!"
+```
+
+As an alternative, you can manually assign
+a function declaration to a variable, though
+this is not the recommended approach:
+
+``` bash
+greeter = f (name) {
+    echo("Hello $name!")
+}
+
+greeter(`whoami`) # "Hello root!"
+```
+
+Named functions are the basis of [decorators](/types/decorators).
+
+## Accessing function arguments
+
+Functions can receive a dynamic number of arguments,
+and arguments can be "packed" through the special
+`...` variable:
+
+```py
+f sum_numbers() {
+    s = 0
+    for x in ... {
+        s += x
+    }
+
+    return s
+}
+
+sum_numbers(1) # 1
+sum_numbers(1, 2, 3) # 6
+```
+
+`...` is a special variable that acts
+like an array, so you can loop and slice
+it however you want:
+
+```py
+f first_arg() {
+    if ....len() > 0 {
+        return ...[0]
+    }
+
+    return "No first arg"
+}
+
+first_arg() # "No first arg"
+first_arg(1) # 1
+```
+
+When you pass `...` directly to a function,
+it will be unpacked:
+
+```py
+f echo_wrapper() {
+    echo(...)
+}
+
+echo_wrapper("hello %s", "root") # "hello root"
+```
+
+and you can add additional arguments as well:
+
+```py
+f echo_wrapper() {
+    echo(..., "root")
+}
+
+echo_wrapper("hello %s %s", "sir") # "hello sir root"
+```
+
 ## Supported functions
 
 ### str()
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index c477f66..ad8aeed 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -92,6 +92,9 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 	case *ast.NullLiteral:
 		return NULL
 
+	case *ast.CurrentArgsLiteral:
+		return &object.Array{Token: node.Token, Elements: env.CurrentArgs, IsCurrentArgs: true}
+
 	case *ast.StringLiteral:
 		return &object.String{Token: node.Token, Value: util.InterpolateStringVars(node.Value, env)}
 
@@ -109,38 +112,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		return evalInfixExpression(node.Token, node.Operator, node.Left, node.Right, env)
 
 	case *ast.CompoundAssignment:
-		left := Eval(node.Left, env)
-		if isError(left) {
-			return left
-		}
-		right := Eval(node.Right, env)
-		if isError(right) {
-			return right
-		}
-		// multi-character operators like "+=" and "**=" are reduced to "+" or "**" for evalInfixExpression()
-		op := node.Operator
-		if len(op) >= 2 {
-			op = op[:len(op)-1]
-		}
-		// get the result of the infix operation
-		expr := evalInfixExpression(node.Token, op, node.Left, node.Right, env)
-		if isError(expr) {
-			return expr
-		}
-		switch nodeLeft := node.Left.(type) {
-		case *ast.Identifier:
-			env.Set(nodeLeft.String(), expr)
-			return NULL
-		case *ast.IndexExpression:
-			// support index assignment expressions: a[0] += 1, h["a"] += 1
-			return evalIndexAssignment(nodeLeft, expr, env)
-		case *ast.PropertyExpression:
-			// support assignment to hash property: h.a += 1
-			return evalPropertyAssignment(nodeLeft, expr, env)
-		}
-		// otherwise
-		env.Set(node.Left.String(), expr)
-		return NULL
+		return evalCompoundAssignment(node, env)
 
 	case *ast.IfExpression:
 		return evalIfExpression(node, env)
@@ -160,7 +132,17 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 	case *ast.FunctionLiteral:
 		params := node.Parameters
 		body := node.Body
-		return &object.Function{Token: node.Token, Parameters: params, Env: env, Body: body}
+		name := node.Name
+		fn := &object.Function{Token: node.Token, Parameters: params, Env: env, Body: body, Name: name, Node: node}
+
+		if name != "" {
+			env.Set(name, fn)
+		}
+
+		return fn
+
+	case *ast.Decorator:
+		return evalDecorator(node, env)
 
 	case *ast.CallExpression:
 		function := Eval(node.Function, env)
@@ -169,6 +151,21 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
 		}
 
 		args := evalExpressions(node.Arguments, env)
+
+		// Did we pass arguments as ...?
+		// If so, replace arguments with the
+		// environment's CurrentArgs.
+		// If other arguments were passed afterwards
+		// (eg. func(..., x, y)) we also add those.
+		if len(args) > 0 {
+			firstArg, ok := args[0].(*object.Array)
+
+			if ok && firstArg.IsCurrentArgs {
+				newArgs := env.CurrentArgs
+				args = append(newArgs, args[1:]...)
+			}
+		}
+
 		if len(args) == 1 && isError(args[0]) {
 			return args[0]
 		}
@@ -258,6 +255,63 @@ func evalBlockStatement(
 	return result
 }
 
+func evalCompoundAssignment(node *ast.CompoundAssignment, env *object.Environment) object.Object {
+	left := Eval(node.Left, env)
+	if isError(left) {
+		return left
+	}
+	right := Eval(node.Right, env)
+	if isError(right) {
+		return right
+	}
+	// multi-character operators like "+=" and "**=" are reduced to "+" or "**" for evalInfixExpression()
+	op := node.Operator
+	if len(op) >= 2 {
+		op = op[:len(op)-1]
+	}
+	// get the result of the infix operation
+	expr := evalInfixExpression(node.Token, op, node.Left, node.Right, env)
+	if isError(expr) {
+		return expr
+	}
+	switch nodeLeft := node.Left.(type) {
+	case *ast.Identifier:
+		env.Set(nodeLeft.String(), expr)
+		return NULL
+	case *ast.IndexExpression:
+		// support index assignment expressions: a[0] += 1, h["a"] += 1
+		return evalIndexAssignment(nodeLeft, expr, env)
+	case *ast.PropertyExpression:
+		// support assignment to hash property: h.a += 1
+		return evalPropertyAssignment(nodeLeft, expr, env)
+	}
+	// otherwise
+	env.Set(node.Left.String(), expr)
+	return NULL
+}
+
+func evalDecorator(node *ast.Decorator, env *object.Environment) object.Object {
+	decorator, ok := env.Get(node.Name)
+
+	if !ok {
+		return 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)
+
+		env.Set(node.Decorated.Name, fn)
+
+		return decorator
+	default:
+		return newError(node.Token, "decorator '%s' must be a function, %s given", node.Name, decorator.Type())
+	}
+}
+
 // support index assignment expressions: a[0] = 1, h["a"] = 1
 func evalIndexAssignment(iex *ast.IndexExpression, expr object.Object, env *object.Environment) object.Object {
 	leftObj := Eval(iex.Left, env)
@@ -1011,7 +1065,6 @@ func evalPropertyExpression(pe *ast.PropertyExpression, env *object.Environment)
 
 func applyFunction(tok token.Token, fn object.Object, env *object.Environment, args []object.Object) object.Object {
 	switch fn := fn.(type) {
-
 	case *object.Function:
 		extendedEnv, err := extendFunctionEnv(fn, args)
 
@@ -1066,9 +1119,9 @@ func extendFunctionEnv(
 	fn *object.Function,
 	args []object.Object,
 ) (*object.Environment, *object.Error) {
-	env := object.NewEnclosedEnvironment(fn.Env)
+	env := object.NewEnclosedEnvironment(fn.Env, args)
 
-	if len(args) != len(fn.Parameters) {
+	if len(args) < len(fn.Parameters) {
 		return nil, newError(fn.Token, "Wrong number of arguments passed to %s. Want %s, got %s", fn.Inspect(), fn.Parameters, args)
 	}
 
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 1fe03ac..4b7f67b 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -429,7 +429,7 @@ func validateVarArgs(tok token.Token, name string, args []object.Object, specs [
 }
 
 func usageVarArgs(name string, specs [][][]string) string {
-	signatures := []string{"Usage:"}
+	signatures := []string{"Wrong arguments passed to '" + name + "'. Usage:"}
 
 	for _, spec := range specs {
 		args := []string{}
diff --git a/examples/decorators.abs b/examples/decorators.abs
new file mode 100644
index 0000000..3423cfc
--- /dev/null
+++ b/examples/decorators.abs
@@ -0,0 +1,39 @@
+# Function to print
+# the current ts in milliseconds
+f current_ts_milliseconds() {
+    return ` date +%s%3N`.int()
+}
+
+# A decorator that prints execution time of a function
+f timer(fn) {
+    return f() {
+        start = current_ts_milliseconds()
+        fn(...)
+        end = current_ts_milliseconds()
+
+        echo("function took %s milliseconds", end - start)
+    }
+}
+
+# A decorator that sleeps before a function
+f sleeper(fn, duration) {
+    return f() {
+        `sleep $duration`
+        echo("Yawn, I slept for $duration second")
+        fn(...)
+    }
+}
+
+@timer
+f greeter(name) {
+    echo("hello $name")
+}
+
+greeter(`whoami`)
+
+@sleeper(1)
+f greeter(name) {
+    echo("hello $name")
+}
+
+greeter(`whoami`)
\ No newline at end of file
diff --git a/examples/dynamic_variadic_arguments.abs b/examples/dynamic_variadic_arguments.abs
new file mode 100644
index 0000000..492d9b3
--- /dev/null
+++ b/examples/dynamic_variadic_arguments.abs
@@ -0,0 +1,38 @@
+# ... is a special "keyword" to identify
+# the arguments passed to the current function
+
+f breakline() {
+    echo("--------")
+}
+
+f f1() {
+    echo(...)
+}
+
+f1("hello %s", "world")
+
+breakline()
+
+f f2() {
+    echo(....len())
+}
+
+f2(1,2,3,4,5)
+
+breakline()
+
+f f3() {
+    echo(sum(... + [1]))
+}
+
+f3(1)
+
+breakline()
+
+f f4() {
+    for arg in ... {
+        echo(arg)
+    }
+}
+
+f4(1,2,3,4,5)
\ No newline at end of file
diff --git a/lexer/lexer.go b/lexer/lexer.go
index b7698d6..5af5487 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -156,6 +156,8 @@ func (l *Lexer) NextToken() token.Token {
 		}
 	case '^':
 		tok = l.newToken(token.BIT_XOR)
+	case '@':
+		tok = l.newToken(token.AT)
 	case '*':
 		if l.peekChar() == '*' {
 			l.readChar()
@@ -220,11 +222,21 @@ func (l *Lexer) NextToken() token.Token {
 	case ',':
 		tok = l.newToken(token.COMMA)
 	case '.':
+		position := l.position
+
 		if l.peekChar() == '.' {
-			tok.Type = token.RANGE
-			tok.Position = l.position
-			tok.Literal = ".."
 			l.readChar()
+
+			if l.peekChar() == '.' {
+				tok.Type = token.CURRENT_ARGS
+				tok.Position = position
+				tok.Literal = "..."
+				l.readChar()
+			} else {
+				tok.Type = token.RANGE
+				tok.Position = position
+				tok.Literal = ".."
+			}
 		} else {
 			tok = l.newToken(token.DOT)
 		}
diff --git a/object/environment.go b/object/environment.go
index 01cbaa8..f2db203 100644
--- a/object/environment.go
+++ b/object/environment.go
@@ -9,9 +9,10 @@ import (
 // with another one embedded to it, so that the
 // new environment has access to identifiers stored
 // in the outer one.
-func NewEnclosedEnvironment(outer *Environment) *Environment {
+func NewEnclosedEnvironment(outer *Environment, args []Object) *Environment {
 	env := NewEnvironment(outer.Writer, outer.Dir)
 	env.outer = outer
+	env.CurrentArgs = args
 	return env
 }
 
@@ -29,7 +30,17 @@ func NewEnvironment(w io.Writer, dir string) *Environment {
 // holds all variables etc.
 type Environment struct {
 	store map[string]Object
-	outer *Environment
+	// Arguments this environment was created in.
+	// When we call function(1, 2, 3), a new environment
+	// for the function to execute is created, and 1/2/3
+	// are recorded as arguments for this environment.
+	//
+	// Later, if we need to access the arguments passed
+	// to the function, we can refer back to them
+	// through env.CurrentArgs. This is how ... is
+	// implemented.
+	CurrentArgs []Object
+	outer       *Environment
 	// Used to capture output. This is typically os.Stdout,
 	// but you could capture in any io.Writer of choice
 	Writer io.Writer
diff --git a/object/object.go b/object/object.go
index 16b41c8..47440f7 100644
--- a/object/object.go
+++ b/object/object.go
@@ -127,9 +127,11 @@ type ContinueError struct {
 
 type Function struct {
 	Token      token.Token
+	Name       string
 	Parameters []*ast.Identifier
 	Body       *ast.BlockStatement
 	Env        *Environment
+	Node       *ast.FunctionLiteral
 }
 
 func (f *Function) Type() ObjectType { return FUNCTION_OBJ }
@@ -142,6 +144,11 @@ func (f *Function) Inspect() string {
 	}
 
 	out.WriteString("f")
+
+	if f.Name != "" {
+		out.WriteString(" " + f.Name)
+	}
+
 	out.WriteString("(")
 	out.WriteString(strings.Join(params, ", "))
 	out.WriteString(") {")
@@ -288,7 +295,15 @@ func (b *Builtin) Json() string     { return b.Inspect() }
 type Array struct {
 	Token    token.Token
 	Elements []Object
-	position int
+	// ... is aliased to an array of arguments.
+	//
+	// Since this is a special case of an array,
+	// we need a flag to make sure we know when
+	// to unpack them, else if we do func(...),
+	// func would receive only one array argument
+	// as opposd to the unpacked arguments.
+	IsCurrentArgs bool
+	position      int
 }
 
 func (ao *Array) Type() ObjectType { return ARRAY_OBJ }
diff --git a/parser/parser.go b/parser/parser.go
index 37e84ee..2ae2f3f 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -112,6 +112,8 @@ func New(l *lexer.Lexer) *Parser {
 	p.registerPrefix(token.COMMENT, p.parseComment)
 	p.registerPrefix(token.BREAK, p.parseBreak)
 	p.registerPrefix(token.CONTINUE, p.parseContinue)
+	p.registerPrefix(token.CURRENT_ARGS, p.parseCurrentArgsLiteral)
+	p.registerPrefix(token.AT, p.parseDecorator)
 
 	p.infixParseFns = make(map[token.TokenType]infixParseFn)
 	p.registerInfix(token.QUESTION, p.parseQuestionExpression)
@@ -778,6 +780,11 @@ func (p *Parser) parseBlockStatement() *ast.BlockStatement {
 func (p *Parser) parseFunctionLiteral() ast.Expression {
 	lit := &ast.FunctionLiteral{Token: p.curToken}
 
+	if p.peekTokenIs(token.IDENT) {
+		p.nextToken()
+		lit.Name = p.curToken.Literal
+	}
+
 	if !p.expectPeek(token.LPAREN) {
 		return nil
 	}
@@ -793,6 +800,61 @@ func (p *Parser) parseFunctionLiteral() ast.Expression {
 	return lit
 }
 
+// @decorator
+// @decorator(1, 2)
+func (p *Parser) parseDecorator() ast.Expression {
+	dc := &ast.Decorator{Token: p.curToken}
+	// A decorator always preceeds a
+	// named function, 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.
+	defer (func() {
+		p.nextToken()
+		exp := p.parseFunctionLiteral()
+
+		switch fn := exp.(type) {
+		case *ast.FunctionLiteral:
+			if fn.Name == "" {
+				p.reportError("a decorator should decorate a named function", dc.Token)
+			}
+
+			dc.Decorated = fn
+		default:
+			p.reportError("a decorator should decorate a named function", dc.Token)
+		}
+	})()
+
+	if p.peekTokenIs(token.IDENT) {
+		p.nextToken()
+		dc.Name = p.curToken.Literal
+	}
+
+	// This is a decorator without arguments
+	// @func
+	if !p.peekTokenIs(token.LPAREN) {
+		return dc
+	}
+
+	// This is a decorator with arguments
+	// @func(x, y, z)
+	p.nextToken()
+	dc.Arguments = p.parseExpressionList(token.RPAREN)
+
+	return dc
+}
+
+// ...
+func (p *Parser) parseCurrentArgsLiteral() ast.Expression {
+	return &ast.CurrentArgsLiteral{Token: p.curToken}
+}
+
 // f(x, y)
 func (p *Parser) parseFunctionParameters() []*ast.Identifier {
 	identifiers := []*ast.Identifier{}
diff --git a/token/token.go b/token/token.go
index 1fe8b62..733f931 100644
--- a/token/token.go
+++ b/token/token.go
@@ -7,11 +7,13 @@ const (
 	EOF     = "EOF"
 
 	// Identifiers + literals
-	IDENT   = "IDENT"  // add, foobar, x, y, ...
-	NUMBER  = "NUMBER" // 1343456, 1.23456
-	STRING  = "STRING" // "foobar"
-	COMMENT = "#"      // # Comment
-	NULL    = "NULL"   // # null
+	IDENT        = "IDENT"  // add, foobar, x, y, ...
+	NUMBER       = "NUMBER" // 1343456, 1.23456
+	STRING       = "STRING" // "foobar"
+	COMMENT      = "#"      // # Comment
+	AT           = "@"      // @ At symbol
+	NULL         = "NULL"   // # null
+	CURRENT_ARGS = "..."    // # ... function args
 
 	// Operators
 	TILDE         = "~"


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index d8e9213..5bb212b 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -699,6 +699,8 @@ func TestFunctionApplication(t *testing.T) {
 		input    string
 		expected interface{}
 	}{
+		{"f test() { return 12 }; test()", 12},
+		{"f test(x) { return x }; test(12)", 12},
 		{"identity = f(x) { x; }; identity(5);", 5},
 		{"identity = f(x) { return x; }; identity(5);", 5},
 		{"double = f(x) { x * 2; }; double(5);", 10},
@@ -718,8 +720,71 @@ func TestFunctionApplication(t *testing.T) {
 				continue
 			}
 			logErrorWithPosition(t, errObj.Message, tt.expected)
-		case float64:
+		case int:
+			testNumberObject(t, evaluated, float64(expected))
+		default:
+			t.Fatalf("unhandled type %T", expected)
+		}
+	}
+}
+
+func TestDecorators(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{"@decorator f hello() {}", "function 'decorator' is not defined (used as decorator)"},
+		{"a = 1; @a f hello(){}", "decorator 'a' must be a function, NUMBER given"},
+		{"f decorator(fn) { return f () { return fn() * 2 } }; @decorator f test() { return 1 }; test()", 2},
+		{"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},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+		switch expected := tt.expected.(type) {
+		case string:
+			errObj, ok := evaluated.(*object.Error)
+			if !ok {
+				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
+				continue
+			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
+		case int:
 			testNumberObject(t, evaluated, float64(expected))
+		default:
+			t.Fatalf("unhandled type %T", expected)
+		}
+	}
+}
+
+func TestCurrentArgs(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected interface{}
+	}{
+		{"f wrap_env() { return env(*args) }; wrap_env('x')", ""},
+		{"env('x', '2'); f wrap_env() { return env(...) }; wrap_env('x').int()", 2},
+		{"env('x', '2'); f wrap_env() { return env(...) }; wrap_env('x', '5'); wrap_env('x').int()", 5},
+		{"f test() { sum(... + [1]) }; test(1,1,1,1)", 5},
+		{"f argsummer() { x = 0; for i in ... {x += i }; return x }; f test() { argsummer(..., 20) }; test(10, 10)", 40},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+		switch expected := tt.expected.(type) {
+		case string:
+			errObj, ok := evaluated.(*object.Error)
+			if !ok {
+				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
+				continue
+			}
+			logErrorWithPosition(t, errObj.Message, tt.expected)
+		case int:
+			testNumberObject(t, evaluated, float64(expected))
+		default:
+			t.Fatalf("unhandled type %T", expected)
 		}
 	}
 }
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 8af1666..a32a609 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -107,6 +107,12 @@ for true {
 a[1:3]
 a?.b
 a?.b()
+f hello(x, y) {
+	x + y;
+};
+@decorator
+@decorator()
+...
 `
 
 	tests := []struct {
@@ -365,6 +371,27 @@ a?.b()
 		{token.IDENT, "b"},
 		{token.LPAREN, "("},
 		{token.RPAREN, ")"},
+		{token.FUNCTION, "f"},
+		{token.IDENT, "hello"},
+		{token.LPAREN, "("},
+		{token.IDENT, "x"},
+		{token.COMMA, ","},
+		{token.IDENT, "y"},
+		{token.RPAREN, ")"},
+		{token.LBRACE, "{"},
+		{token.IDENT, "x"},
+		{token.PLUS, "+"},
+		{token.IDENT, "y"},
+		{token.SEMICOLON, ";"},
+		{token.RBRACE, "}"},
+		{token.SEMICOLON, ";"},
+		{token.AT, "@"},
+		{token.IDENT, "decorator"},
+		{token.AT, "@"},
+		{token.IDENT, "decorator"},
+		{token.LPAREN, "("},
+		{token.RPAREN, ")"},
+		{token.CURRENT_ARGS, "..."},
 		{token.EOF, ""},
 	}
 
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 2f079b0..cb4236c 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -2,6 +2,7 @@ package parser
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/abs-lang/abs/ast"
@@ -1081,10 +1082,12 @@ func TestFunctionParameterParsing(t *testing.T) {
 	tests := []struct {
 		input          string
 		expectedParams []string
+		name           string
 	}{
 		{input: "f() {};", expectedParams: []string{}},
 		{input: "f(x) {};", expectedParams: []string{"x"}},
 		{input: "f(x, y, z) {};", expectedParams: []string{"x", "y", "z"}},
+		{input: "f hello() {};", expectedParams: []string{}, name: "hello"},
 	}
 
 	for _, tt := range tests {
@@ -1097,8 +1100,11 @@ func TestFunctionParameterParsing(t *testing.T) {
 		function := stmt.Expression.(*ast.FunctionLiteral)
 
 		if len(function.Parameters) != len(tt.expectedParams) {
-			t.Errorf("length parameters wrong. want %d, got=%d\n",
-				len(tt.expectedParams), len(function.Parameters))
+			t.Errorf("length parameters wrong. want %d, got=%d\n", len(tt.expectedParams), len(function.Parameters))
+		}
+
+		if tt.name != function.Name {
+			t.Errorf("name parameter wrong. want %s, got=%s\n", tt.name, function.Name)
 		}
 
 		for i, ident := range tt.expectedParams {
@@ -1107,6 +1113,100 @@ func TestFunctionParameterParsing(t *testing.T) {
 	}
 }
 
+func TestCurrentArgsParsing(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{input: "...", expected: "..."},
+		{input: "xyz(...)", expected: "..."},
+		{input: "f x() {env(...)}", expected: "env(...)"},
+	}
+
+	for _, tt := range tests {
+		l := lexer.New(tt.input)
+		p := New(l)
+		program := p.ParseProgram()
+		checkParserErrors(t, p)
+
+		stmt := program.Statements[0].(*ast.ExpressionStatement)
+
+		var result ast.Node
+		switch expr := stmt.Expression.(type) {
+		case *ast.CurrentArgsLiteral:
+			result = stmt.Expression.(*ast.CurrentArgsLiteral)
+		case *ast.FunctionLiteral:
+			result = expr.Body.Statements[0]
+		case *ast.CallExpression:
+			result = expr.Arguments[0]
+		default:
+			t.Errorf("unknown type %T", expr)
+		}
+
+		if result.String() != tt.expected {
+			t.Errorf("args not parsed correctly. want '%s', got=%s\n", tt.expected, result.String())
+		}
+	}
+}
+
+func TestDecoratorParsing(t *testing.T) {
+	tests := []struct {
+		input     string
+		args      []string
+		name      string
+		decorated string
+		err       string
+	}{
+		{input: "@decorator", err: "a decorator should decorate a named function"},
+		{input: "@decorator('x', 'y')", err: "a decorator should decorate a named function"},
+		{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"},
+	}
+
+	for _, tt := range tests {
+		l := lexer.New(tt.input)
+		p := New(l)
+		program := p.ParseProgram()
+
+		if tt.err != "" {
+			if len(p.Errors()) == 0 {
+				t.Errorf("no parsing error detected")
+				t.FailNow()
+			}
+
+			parseError := p.Errors()[len(p.Errors())-1]
+			if !strings.HasPrefix(parseError, tt.err) {
+				t.Errorf("wrong parser error detected: want '%s', got '%s'", tt.err, parseError)
+			}
+			continue
+		} else {
+			checkParserErrors(t, p)
+		}
+
+		stmt := program.Statements[0].(*ast.ExpressionStatement)
+		decorator := stmt.Expression.(*ast.Decorator)
+
+		if len(decorator.Arguments) != len(tt.args) {
+			t.Errorf("length arguments wrong. want %d, got=%d\n", len(tt.args), len(decorator.Arguments))
+		}
+
+		if tt.name != decorator.Name {
+			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")
+		}
+
+		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])
+			}
+		}
+	}
+}
+
 func TestCallExpressionParsing(t *testing.T) {
 	input := "add(1, 2 * 3, 4 + 5);"