From f0bb27224ae629257760fc02cda5b16d29e00ed4 Mon Sep 17 00:00:00 2001 From: odino Date: Thu, 14 May 2020 01:34:20 +0400 Subject: [PATCH] Default values for function parameters, closes #368 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the ability to specify default values for function arguments. Optional parameters need to come right after mandatory ones: ``` ⧐ f test(x = 1, y){} parser errors: found mandatory parameter after optional one [1:15] f test(x = 1, y){} ``` and they can use any kind of expression: ``` ⧐ f test(x = 1){ x }; test() 1 ⧐ f test(x = []){ x }; test() [] ⧐ f test(x = {}){ x }; test() {} ⧐ f test(x = 1..10){ x }; test() [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` If a mandatory argument is not passed, ABS will now give you a more detailed message: ``` ⧐ f test(x){ x }; test() ERROR: argument x to function f test(x) {x} is missing, and doesn't have a default value [1:1] f test(x){ x }; test() ``` --- ast/ast.go | 21 +++++++++- docs/types/function.md | 83 ++++++++++++++++++++++++++----------- evaluator/evaluator.go | 19 ++++++--- evaluator/evaluator_test.go | 7 +++- object/object.go | 2 +- parser/parser.go | 67 ++++++++++++++++++++++++++---- parser/parser_test.go | 55 ++++++++++++++++++++++-- 7 files changed, 209 insertions(+), 45 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index ef9e5311..0396cb5d 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -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 @@ -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 } diff --git a/docs/types/function.md b/docs/types/function.md index e1ba74bd..e22f057e 100644 --- a/docs/types/function.md +++ b/docs/types/function.md @@ -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] @@ -87,11 +77,11 @@ 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 @@ -99,15 +89,60 @@ 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, @@ -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: @@ -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! diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 059df37f..b5c23a20 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -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 diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 475b64f9..be7c8032 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -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 { diff --git a/object/object.go b/object/object.go index f897fb0b..8a2f2d57 100644 --- a/object/object.go +++ b/object/object.go @@ -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 diff --git a/parser/parser.go b/parser/parser.go index c3429341..152d10c9 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -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() diff --git a/parser/parser_test.go b/parser/parser_test.go index b3a1a0cd..345154d2 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -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) @@ -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", @@ -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"}, @@ -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) } } } @@ -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 {