Skip to content

Commit

Permalink
Decorators & dynamic function arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
odino committed Feb 15, 2020
1 parent 9a40c9a commit 02c80fb
Show file tree
Hide file tree
Showing 17 changed files with 600 additions and 49 deletions.
37 changes: 37 additions & 0 deletions ast/ast.go
Expand Up @@ -434,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
Expand Down
1 change: 1 addition & 0 deletions docs/_includes/toc.md
Expand Up @@ -25,6 +25,7 @@
* [Hash](/types/hash)
* [Functions](/types/function)
* [Builtin functions](/types/builtin-function)
* [Decorators](/types/decorator)

## Miscellaneous

Expand Down
2 changes: 1 addition & 1 deletion docs/types/builtin-function.md
Expand Up @@ -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).
73 changes: 73 additions & 0 deletions 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).
60 changes: 60 additions & 0 deletions docs/types/function.md
Expand Up @@ -106,6 +106,66 @@ greeter = f (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()
Expand Down
118 changes: 82 additions & 36 deletions evaluator/evaluator.go
Expand Up @@ -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)}

Expand All @@ -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)
Expand All @@ -161,21 +133,39 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
params := node.Parameters
body := node.Body
name := node.Name
fn := &object.Function{Token: node.Token, Parameters: params, Env: env, Body: body, Name: 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)
if isError(function) {
return function
}

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]
}
Expand Down Expand Up @@ -265,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)
Expand Down Expand Up @@ -1018,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)

Expand Down Expand Up @@ -1073,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)
}

Expand Down

0 comments on commit 02c80fb

Please sign in to comment.