Skip to content

Commit

Permalink
Merge branch 'default-args-for-functions' into 2.1.x
Browse files Browse the repository at this point in the history
  • Loading branch information
odino committed May 13, 2020
2 parents 299535c + f0bb272 commit 1f790b3
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 45 deletions.
21 changes: 20 additions & 1 deletion ast/ast.go
Expand Up @@ -171,6 +171,25 @@ func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
func (i *Identifier) String() string { return i.Value }

// Parameter is a function parameter
// fn(x, y = 2)
type Parameter struct {
*Identifier
Default Expression
}

func (p *Parameter) expressionNode() {}
func (p *Parameter) TokenLiteral() string { return p.Token.Literal }
func (p *Parameter) String() string {
s := p.Value

if p.Default != nil {
s += " = " + p.Default.String()
}

return s
}

type Boolean struct {
Token token.Token
Value bool
Expand Down Expand Up @@ -411,7 +430,7 @@ func (ce *CommandExpression) String() string {
type FunctionLiteral struct {
Token token.Token // The 'fn' token
Name string // identifier for this function
Parameters []*Identifier
Parameters []*Parameter
Body *BlockStatement
}

Expand Down
83 changes: 59 additions & 24 deletions docs/types/function.md
Expand Up @@ -28,17 +28,7 @@ favors `f` for 2 main reasons:
* brevity
* resembles the standard mathematical notation everyone is used to (*x ↦ f(x)*)

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) {
# x
# }. Want [x], got []
```
They can be passed as arguments to other functions:
Functions can be passed as arguments to other functions:

``` bash
[1, 2, 3].map(f(x){ x + 1}) # [2, 3, 4]
Expand Down Expand Up @@ -87,27 +77,72 @@ You can create named functions by specifying an identifier
after the `f` keyword:

``` bash
f greeter(name) {
f greet(name) {
echo("Hello $name!")
}

greeter(`whoami`) # "Hello root!"
greet(`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) {
greet = f (name) {
echo("Hello $name!")
}

greeter(`whoami`) # "Hello root!"
greet(`whoami`) # "Hello root!"
```

Named functions are the basis of [decorators](/types/decorators).

## Optional parameters

Functions must be called with the right number of arguments:

``` bash
f greet(name, greeting) {
echo("$greeting $name!")
}
greet("user")
# ERROR: argument greeting to function f greet(name, greeting) {echo($greeting $name!)} is missing, and doesn't have a default value
# [1:1] greet("user")
```

but note that you could make a parameter optional by specifying its
default value:

``` bash
f greet(name, greeting = "hello") {
echo("$greeting $name!")
}
greet("user") # hello user!
greet("user", "hola") # hola user!
```

A default value can be any expression (doesn't have to be a literal):

```bash
f test(x = 1){x}; test() # 1
f test(x = "test".split("")){x}; test() # ["t", "e", "s", "t"]
f test(x = {}){x}; test() # {}
y = 100; f test(x = y){x}; test() # 100
x = 100; f test(x = x){x}; test() # 100
x = 100; f test(x = x){x}; test(1) # 1
```

Note that mandatory arguments always need to be declared
before optional ones:

``` bash
f(x = null, y){}
# parser errors:
# found mandatory parameter after optional one
# [1:13] f(x = null, y){}
```

## Accessing function arguments

Functions can receive a dynamic number of arguments,
Expand Down Expand Up @@ -168,6 +203,15 @@ echo_wrapper("hello %s %s", "sir") # "hello sir root"

## Supported functions

### call(args)

Calls a function with the given arguments:

``` bash
doubler = f(x) { x * 2 }
doubler.call([10]) # 20
```
### str()
Returns the string representation of the function:
Expand All @@ -179,15 +223,6 @@ f(x){}.str()
# }
```
### call(args)
Calls a function with the given arguments:
``` bash
doubler = f(x) { x * 2 }
doubler.call([10]) # 20
```
## Next
That's about it for this section!
Expand Down
19 changes: 14 additions & 5 deletions evaluator/evaluator.go
Expand Up @@ -1217,12 +1217,21 @@ func extendFunctionEnv(
) (*object.Environment, *object.Error) {
env := object.NewEnclosedEnvironment(fn.Env, args)

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)
}

for paramIdx, param := range fn.Parameters {
env.Set(param.Value, args[paramIdx])
argumentPassed := len(args) > paramIdx

if !argumentPassed && param.Default == nil {
return nil, newError(fn.Token, "argument %s to function %s is missing, and doesn't have a default value", param.Value, fn.Inspect())
}

var arg object.Object
if argumentPassed {
arg = args[paramIdx]
} else {
arg = Eval(param.Default, env)
}

env.Set(param.Value, arg)
}

return env, nil
Expand Down
7 changes: 6 additions & 1 deletion evaluator/evaluator_test.go
Expand Up @@ -731,7 +731,12 @@ func TestFunctionApplication(t *testing.T) {
{"add = f(x, y) { x + y; }; add(5, 5);", 10},
{"add = f(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20},
{"f(x) { x; }(5)", 5},
{"f(x) { x; }()", "Wrong number of arguments passed to f(x) {x}. Want [x], got []"},
{"f(x) { x; }()", "argument x to function f(x) {x} is missing, and doesn't have a default value"},
{"f(x = 2) { x; }()", 2},
{"f(x, y = 2) { x + y; }()", "argument x to function f(x, y = 2) {(x + y)} is missing, and doesn't have a default value"},
{"f test(x, y = 2) { x + y; }()", "argument x to function f test(x, y = 2) {(x + y)} is missing, and doesn't have a default value"},
{"f(x, y = 2) { x + y; }(1)", 3},
{"f(x, y = 2) { x + y; }(1, 1)", 2},
}

for _, tt := range tests {
Expand Down
2 changes: 1 addition & 1 deletion object/object.go
Expand Up @@ -152,7 +152,7 @@ type ContinueError struct {
type Function struct {
Token token.Token
Name string
Parameters []*ast.Identifier
Parameters []*ast.Parameter
Body *ast.BlockStatement
Env *Environment
Node *ast.FunctionLiteral
Expand Down
67 changes: 58 additions & 9 deletions parser/parser.go
Expand Up @@ -842,32 +842,81 @@ func (p *Parser) parseCurrentArgsLiteral() ast.Expression {
return &ast.CurrentArgsLiteral{Token: p.curToken}
}

// f(x, y)
func (p *Parser) parseFunctionParameters() []*ast.Identifier {
identifiers := []*ast.Identifier{}
// f(x, y = 2)
func (p *Parser) parseFunctionParameters() []*ast.Parameter {
parameters := []*ast.Parameter{}

if p.peekTokenIs(token.RPAREN) {
p.nextToken()
return identifiers
return parameters
}

p.nextToken()

ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
identifiers = append(identifiers, ident)
param, foundOptionalParameter := p.parseFunctionParameter()
parameters = append(parameters, param)

for p.peekTokenIs(token.COMMA) {
p.nextToken()
p.nextToken()
ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
identifiers = append(identifiers, ident)

param, optional := p.parseFunctionParameter()

if foundOptionalParameter && !optional {
p.reportError("found mandatory parameter after optional one", p.curToken)
}

if optional {
foundOptionalParameter = true
}

parameters = append(parameters, param)
}

if !p.expectPeek(token.RPAREN) {
return nil
}

return identifiers
return parameters
}

// parse a single function parameter
// x
// x = 2
func (p *Parser) parseFunctionParameter() (param *ast.Parameter, optional bool) {
// first, parse the identifier
ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}

// if we find a comma or the closing parenthesis, then this
// parameter is done eg. fn(x, y, z)
if p.peekTokenIs(token.COMMA) || p.peekTokenIs(token.RPAREN) {
return &ast.Parameter{Identifier: ident, Default: nil}, false
}

// else, we are in front of an optional parameter
// fn(x = 2)
// if the next token is not an assignment, though, there's
// a major problem
if !p.peekTokenIs(token.ASSIGN) {
p.reportError("invalid parameter format", p.curToken)
return &ast.Parameter{Identifier: ident, Default: nil}, false
}

// skip to the =
p.nextToken()
// skip to the default value of the parameter
p.nextToken()
// parse this default value as an expression
// this allows for funny stuff like:
// fn(x = 1)
// fn(x = "")
// fn(x = null)
// fn(x = {})
// fn(x = [1, 2, 3, 4])
// fn(x = [1, 2, 3, 4].filter(f(x) {x > 2}) <--- very funny but ¯\_(ツ)_/¯
exp := p.parseExpression(LOWEST)

return &ast.Parameter{Identifier: ident, Default: exp}, true
}

// function()
Expand Down
55 changes: 51 additions & 4 deletions parser/parser_test.go
Expand Up @@ -976,7 +976,7 @@ func TestForElseExpression(t *testing.T) {
}

func TestFunctionLiteralParsing(t *testing.T) {
input := `f(x, y) { x + y; }`
input := `f(x, y = 2) { x + y; }`

l := lexer.New(input)
p := New(l)
Expand Down Expand Up @@ -1005,8 +1005,8 @@ func TestFunctionLiteralParsing(t *testing.T) {
len(function.Parameters))
}

testLiteralExpression(t, function.Parameters[0], "x")
testLiteralExpression(t, function.Parameters[1], "y")
testParameter(t, function.Parameters[0], "x")
testParameter(t, function.Parameters[1], "y = 2")

if len(function.Body.Statements) != 1 {
t.Fatalf("function.Body.Statements has not 1 statements. got=%d\n",
Expand Down Expand Up @@ -1085,6 +1085,9 @@ func TestFunctionParameterParsing(t *testing.T) {
name string
}{
{input: "f() {};", expectedParams: []string{}},
{input: "f(x, y = 2) {};", expectedParams: []string{"x", "y = 2"}},
{input: "f(x, y = 2, z = 3) {};", expectedParams: []string{"x", "y = 2", "z = 3"}},
{input: "f(x = 1, y = 2, z = 3) {};", expectedParams: []string{"x = 1", "y = 2", "z = 3"}},
{input: "f(x) {};", expectedParams: []string{"x"}},
{input: "f(x, y, z) {};", expectedParams: []string{"x", "y", "z"}},
{input: "f hello() {};", expectedParams: []string{}, name: "hello"},
Expand All @@ -1108,7 +1111,35 @@ func TestFunctionParameterParsing(t *testing.T) {
}

for i, ident := range tt.expectedParams {
testLiteralExpression(t, function.Parameters[i], ident)
testParameter(t, function.Parameters[i], ident)
}
}
}

func TestFunctionParameterParsingOptionalParametersMustComeAfterAllMandatoryOnes(t *testing.T) {
tests := []struct {
input string
err string
}{
{input: "f(x, y = 2, z) {};", err: "found mandatory parameter after optional one"},
{input: "f(x = 2, y) {};", err: "found mandatory parameter after optional one"},
{input: "f(x = 1, y = 1, z = 1, a) {};", err: "found mandatory parameter after optional one"},
{input: "f(x, y, z = 1, a) {};", err: "found mandatory parameter after optional one"},
}

for _, tt := range tests {
l := lexer.New(tt.input)
p := New(l)
p.ParseProgram()

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)
}
}
}
Expand Down Expand Up @@ -1915,6 +1946,22 @@ func testIdentifier(t *testing.T, exp ast.Expression, value string) bool {
return true
}

func testParameter(t *testing.T, exp ast.Expression, value string) bool {
ident, ok := exp.(*ast.Parameter)
if !ok {
t.Errorf("exp not *ast.Parameter. got=%T", exp)
return false
}

if ident.String() != value {
t.Errorf("Parameter.TokenLiteral not %s. got=%s", value,
ident.TokenLiteral())
return false
}

return true
}

func testCommand(t *testing.T, exp ast.Expression, value string) bool {
command, ok := exp.(*ast.CommandExpression)
if !ok {
Expand Down

0 comments on commit 1f790b3

Please sign in to comment.