From f6c90cf33bafd86856125cbc0e4ddf714ee9b924 Mon Sep 17 00:00:00 2001 From: Jonathan Fontes Date: Wed, 3 Aug 2022 12:47:08 +0100 Subject: [PATCH] add arguments with default values && elseif conditions --- README.md | 49 +++++++++++++++--- evaluator/function.go | 69 +++++++++++++++++++++---- evaluator/function_test.go | 42 ++++++++++++++++ parser/arguments.go | 8 +++ parser/arguments_test.go | 28 +++++++++++ parser/call_test.go | 58 +++++++++++---------- parser/function.go | 2 +- parser/if.go | 6 +++ parser/if_test.go | 92 ++++++++++++++++++++++++++++++++++ parser/parser.go | 1 + testdata/assert_function.ninja | 31 ++++++++++++ testdata/assert_variable.ninja | 4 +- testdata/assertions.ninja | 30 ++++++++--- testdata/expected.txt | 28 +++++++++++ 14 files changed, 397 insertions(+), 51 deletions(-) create mode 100644 testdata/assert_function.ninja diff --git a/README.md b/README.md index 686dd87..c7b04a6 100644 --- a/README.md +++ b/README.md @@ -390,16 +390,51 @@ puts(STATUS::OK); ## Conditions -### If / Else -`if () { } else { }` +### If / ElseIf / Else +`if () { } elseif () { } else { }` -``` +#### Simple If Condition +``` +if (true) { + puts("Hello"); +} +``` + +#### If / Else + +``` if (true) { puts("Hello"); } else { - puts("Yes"); -} + puts("Nope"); +} +``` + +#### If / ElseIf / Else + ``` +if (true) { + puts("Hello"); +} elseif (1 > 2) { + puts("1 is greater than 2"); +} else { + puts("Nope"); +} +``` + +We can omit else condition + +``` +if (true) { + puts("Hello"); +} elseif (1 > 2) { + puts("1 is greater than 2"); +} + +if (true) { + puts("Hello"); +} +``` ### Ternary @@ -521,8 +556,8 @@ true.type(); // "BOOLEAN" ## Keywords ``` -var true false function delete enum -return if else for import break case +var true false function return if +else for import delete break enum case ``` ## Lexical Scooping diff --git a/evaluator/function.go b/evaluator/function.go index 1a14de0..9a5a688 100644 --- a/evaluator/function.go +++ b/evaluator/function.go @@ -1,6 +1,8 @@ package evaluator import ( + "errors" + "fmt" "github.com/gravataLonga/ninja/ast" "github.com/gravataLonga/ninja/object" ) @@ -9,15 +11,15 @@ func applyFunction(fn object.Object, args []object.Object) object.Object { switch fn := fn.(type) { case *object.FunctionLiteral: - if len(fn.Parameters) != len(args) { - return object.NewErrorFormat("Function expected %d arguments, got %d at %s", len(fn.Parameters), len(args), fn.Body.Token) + if err := argumentsIsValid(args, fn.Parameters); err != nil { + return object.NewErrorFormat(err.Error()+" at %s", fn.Body.Token) } extendedEnv := extendFunctionEnv(fn.Env, fn.Parameters, args) evaluated := Eval(fn.Body, extendedEnv) return unwrapReturnValue(evaluated) case *object.Function: - if len(fn.Parameters) != len(args) { - return object.NewErrorFormat("Function expected %d arguments, got %d at %s", len(fn.Parameters), len(args), fn.Body.Token) + if err := argumentsIsValid(args, fn.Parameters); err != nil { + return object.NewErrorFormat(err.Error()+" at %s", fn.Body.Token) } extendedEnv := extendFunctionEnv(fn.Env, fn.Parameters, args) evaluated := Eval(fn.Body, extendedEnv) @@ -33,16 +35,65 @@ func applyFunction(fn object.Object, args []object.Object) object.Object { func extendFunctionEnv( fnEnv *object.Environment, fnArguments []ast.Expression, - args []object.Object, + parameters []object.Object, ) *object.Environment { + maxLen := len(parameters) + env := object.NewEnclosedEnvironment(fnEnv) - for paramIdx, param := range fnArguments { - // @todo need to test this - ident, _ := param.(*ast.Identifier) - env.Set(ident.Value, args[paramIdx]) + for argumentIndex, argument := range fnArguments { + var value object.Object + var identifier string + + switch argument.(type) { + case *ast.Identifier: + ident, _ := argument.(*ast.Identifier) + value = parameters[argumentIndex] + identifier = ident.Value + break + case *ast.InfixExpression: + infix, _ := argument.(*ast.InfixExpression) + ident, _ := infix.Left.(*ast.Identifier) + identifier = ident.Value + value = Eval(infix.Right, env) + if maxLen > argumentIndex { + value = parameters[argumentIndex] + } + } + + env.Set(identifier, value) } return env } + +// argumentsIsValid check if parameters passed to function is expected by arguments +func argumentsIsValid(parameters []object.Object, arguments []ast.Expression) error { + if len(parameters) == len(arguments) { + return nil + } + + if len(parameters) > len(arguments) { + return errors.New(fmt.Sprintf("Function expected %d arguments, got %d", len(arguments), len(parameters))) + } + + // all arguments are infix expression, which mean, they have a default value + total := 0 + for _, arg := range arguments { + if _, ok := arg.(*ast.InfixExpression); ok { + total++ + } + } + // all arguments have default value + if total == len(arguments) { + return nil + } + + // a, b = 1 + if total+len(parameters) == len(arguments) { + return nil + } + + return errors.New(fmt.Sprintf("Function expected %d arguments, got %d", len(arguments), len(parameters))) +} diff --git a/evaluator/function_test.go b/evaluator/function_test.go index 9c04152..838b6e1 100644 --- a/evaluator/function_test.go +++ b/evaluator/function_test.go @@ -56,6 +56,48 @@ func TestFunctionObject(t *testing.T) { } } +func TestFunctionWithDefaultArguments(t *testing.T) { + tests := []struct { + input string + result interface{} + }{ + { + `function (a = 0) { return a;}();`, + 0, + }, + { + `function (a = 0) { return a;}(2);`, + 2, + }, + { + `function add (a = 0) { return a;}; add();`, + 0, + }, + { + `function add (a = 0) { return a;}; add(2);`, + 2, + }, + { + `function (a, b = 1) { return a + b;}(1);`, + 2, + }, + { + `function (a, b = 1) { return a + b;}(1, 2);`, + 3, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("TestFunctionWithDefaultArguments[%d]", i), func(t *testing.T) { + evaluated := testEval(tt.input, t) + + if !testObjectLiteral(t, evaluated, tt.result) { + t.Errorf("TestCallFunction unable to test") + } + }) + } +} + func TestCallFunction(t *testing.T) { tests := []struct { expression string diff --git a/parser/arguments.go b/parser/arguments.go index 8466efa..2c8fa8a 100644 --- a/parser/arguments.go +++ b/parser/arguments.go @@ -25,12 +25,14 @@ func (p *Parser) parseFunctionParameters() []ast.Expression { func (p *Parser) parseParameterWithOptional() []ast.Expression { var identifiers []ast.Expression + isOnRequiredParameters := true if p.peekTokenIs(token.ASSIGN) { ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} p.nextToken() infix := p.parseInfixExpression(ident) identifiers = append(identifiers, infix) + isOnRequiredParameters = false } else { ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} identifiers = append(identifiers, ident) @@ -45,9 +47,15 @@ func (p *Parser) parseParameterWithOptional() []ast.Expression { p.nextToken() infix := p.parseInfixExpression(ident) identifiers = append(identifiers, infix) + isOnRequiredParameters = false continue } + if !isOnRequiredParameters { + p.newError("require arguments must be on declare first") + return nil + } + ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} identifiers = append(identifiers, ident) } diff --git a/parser/arguments_test.go b/parser/arguments_test.go index c21fee2..445d106 100644 --- a/parser/arguments_test.go +++ b/parser/arguments_test.go @@ -99,3 +99,31 @@ func TestFunctionParameterOptionalParsing(t *testing.T) { testInfixExpression(t, fn.Parameters[3], "k", "=", true) testInfixExpression(t, fn.Parameters[4], "a", "=", "b") } + +func TestArgumentErrorMessageWhenDeclareOptionalArgumentFirst(t *testing.T) { + l := lexer.New(strings.NewReader(`function (a = 1, b) {}`)) + p := New(l) + p.ParseProgram() + + if len(p.Errors()) <= 0 { + t.Fatalf("Expected at least 1 error. Got 0") + } + + if p.Errors()[0] != "require arguments must be on declare first" { + t.Errorf("Expected error to be %s. Got: %s", "require arguments must be on declare first", p.Errors()[0]) + } +} + +func TestArgumentErrorMessageWhenMixingOrderOfOptionalArguments(t *testing.T) { + l := lexer.New(strings.NewReader(`function (a, b = 1, c) {}`)) + p := New(l) + p.ParseProgram() + + if len(p.Errors()) <= 0 { + t.Fatalf("Expected at least 1 error. Got 0") + } + + if p.Errors()[0] != "require arguments must be on declare first" { + t.Errorf("Expected error to be %s. Got: %s", "require arguments must be on declare first", p.Errors()[0]) + } +} diff --git a/parser/call_test.go b/parser/call_test.go index a07dff9..729a181 100644 --- a/parser/call_test.go +++ b/parser/call_test.go @@ -1,6 +1,7 @@ package parser import ( + "fmt" "github.com/gravataLonga/ninja/ast" "github.com/gravataLonga/ninja/lexer" "strings" @@ -72,33 +73,36 @@ func TestCallExpressionParameterParsing(t *testing.T) { }, } - for _, tt := range tests { - l := lexer.New(strings.NewReader(tt.input)) - p := New(l) - program := p.ParseProgram() - checkParserErrors(t, p) - - stmt := program.Statements[0].(*ast.ExpressionStatement) - exp, ok := stmt.Expression.(*ast.CallExpression) - if !ok { - t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", - stmt.Expression) - } - - if !testIdentifier(t, exp.Function, tt.expectedIdent) { - return - } - - if len(exp.Arguments) != len(tt.expectedArgs) { - t.Fatalf("wrong number of arguments. want=%d, got=%d", - len(tt.expectedArgs), len(exp.Arguments)) - } - - for i, arg := range tt.expectedArgs { - if exp.Arguments[i].String() != arg { - t.Errorf("argument %d wrong. want=%q, got=%q", i, - arg, exp.Arguments[i].String()) + for i, tt := range tests { + t.Run(fmt.Sprintf("TestCallExpressionParameterParsing[%d]", i), func(t *testing.T) { + l := lexer.New(strings.NewReader(tt.input)) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + exp, ok := stmt.Expression.(*ast.CallExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", + stmt.Expression) + } + + if !testIdentifier(t, exp.Function, tt.expectedIdent) { + return + } + + if len(exp.Arguments) != len(tt.expectedArgs) { + t.Fatalf("wrong number of arguments. want=%d, got=%d", + len(tt.expectedArgs), len(exp.Arguments)) + } + + for i, arg := range tt.expectedArgs { + if exp.Arguments[i].String() != arg { + t.Errorf("argument %d wrong. want=%q, got=%q", i, + arg, exp.Arguments[i].String()) + } } - } + }) + } } diff --git a/parser/function.go b/parser/function.go index 863b24d..487e454 100644 --- a/parser/function.go +++ b/parser/function.go @@ -21,7 +21,7 @@ func (p *Parser) parseFunction() ast.Expression { lit := &ast.Function{} p.nextToken() - lit.Name = &ast.Identifier{Token: p.curToken, Value: string(p.curToken.Literal)} + lit.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} if !p.expectPeek(token.LPAREN) { return nil diff --git a/parser/if.go b/parser/if.go index a9cc148..8d35db8 100644 --- a/parser/if.go +++ b/parser/if.go @@ -25,6 +25,12 @@ func (p *Parser) parseIfExpression() ast.Expression { expression.Consequence = p.parseBlockStatement() + if p.peekTokenIs(token.ELSEIF) { + p.nextToken() + expression.Alternative = &ast.BlockStatement{Statements: []ast.Statement{p.parseExpressionStatement()}} + return expression + } + if p.peekTokenIs(token.ELSE) { p.nextToken() diff --git a/parser/if_test.go b/parser/if_test.go index aca50d3..ce74d06 100644 --- a/parser/if_test.go +++ b/parser/if_test.go @@ -106,3 +106,95 @@ func TestIfElseExpression(t *testing.T) { return } } + +func TestElseIfExpression(t *testing.T) { + input := `if (true != 10) { x } elseif (y << 2) { y } else { a }` + + l := lexer.New(strings.NewReader(input)) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Body does not contain %d statements. got=%d\n", 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.IfExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T", stmt.Expression) + } + + // Condition + + if !testInfixExpression(t, exp.Condition, true, "!=", 10) { + return + } + + // Consequence + + if len(exp.Consequence.Statements) != 1 { + t.Errorf("consequence is not 1 statements. got=%d\n", len(exp.Consequence.Statements)) + } + + consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", + exp.Consequence.Statements[0]) + } + + if !testIdentifier(t, consequence.Expression, "x") { + return + } + + if len(exp.Alternative.Statements) != 1 { + t.Errorf("exp.Alternative.Statements does not contain 1 statements. got=%d\n", len(exp.Alternative.Statements)) + } + + alternative, ok := exp.Alternative.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", exp.Alternative.Statements[0]) + } + + // Else If Expression Test + + elseIfExpression, ok := alternative.Expression.(*ast.IfExpression) + + if !ok { + t.Fatalf("Alternative.Expression isn't if expression got: %t", alternative.Expression) + } + + if !testInfixExpression(t, elseIfExpression.Condition, "y", "<<", 2) { + return + } + + if len(elseIfExpression.Consequence.Statements) != 1 { + t.Errorf("consequence is not 1 statements. got=%d\n", len(exp.Consequence.Statements)) + } + + consequenceElsifExpression, ok := elseIfExpression.Consequence.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", elseIfExpression.Consequence.Statements[0]) + } + + if !testIdentifier(t, consequenceElsifExpression.Expression, "y") { + return + } + + if len(elseIfExpression.Alternative.Statements) != 1 { + t.Errorf("exp.Alternative.Statements does not contain 1 statements. got=%d\n", len(elseIfExpression.Alternative.Statements)) + } + + alternativeIfExpression, ok := elseIfExpression.Alternative.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", elseIfExpression.Alternative.Statements[0]) + } + + if !testIdentifier(t, alternativeIfExpression.Expression, "a") { + return + } +} diff --git a/parser/parser.go b/parser/parser.go index 5728e52..82ac615 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -84,6 +84,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.DECRE, p.parsePrefixExpression, PREFIX) p.registerPrefix(token.INCRE, p.parsePrefixExpression, PREFIX) p.registerPrefix(token.IF, p.parseIfExpression, LOWEST) + p.registerPrefix(token.ELSEIF, p.parseIfExpression, LOWEST) p.registerPrefix(token.FUNCTION, p.parseFunction, LOWEST) p.registerPrefix(token.LPAREN, p.parseGroupedExpression, LOWEST) p.registerPrefix(token.LBRACKET, p.parseArrayLiteral, LOWEST) diff --git a/testdata/assert_function.ninja b/testdata/assert_function.ninja new file mode 100644 index 0000000..27390b9 --- /dev/null +++ b/testdata/assert_function.ninja @@ -0,0 +1,31 @@ + +function getNumber() { + return 1; +} + +assert("Function call: getNumber()", function() { + return getNumber() == 1; +}); + + +function passingArguments(a) { + return a; +} + +assert("Function call: passingArguments(3)", function() { + var a = 2; + return passingArguments(3) == 3; +}); + + +function passingRequireArgumentsAndOptional(a, b = 2) { + return a + b; +} + +assert("Function call: passingRequireArgumentsAndOptional(1)", function() { + return passingRequireArgumentsAndOptional(1) == 3; +}); + +assert("Function call: passingRequireArgumentsAndOptional(5, 5)", function() { + return passingRequireArgumentsAndOptional(5, 5) == 10; +}); \ No newline at end of file diff --git a/testdata/assert_variable.ninja b/testdata/assert_variable.ninja index 264d4c9..3a3f826 100644 --- a/testdata/assert_variable.ninja +++ b/testdata/assert_variable.ninja @@ -26,7 +26,9 @@ var tests = [ {"expression": 8 | 2 , "total": 10, "name": "(8 | 2)"}, {"expression": 8 ^ 2 , "total": 10, "name": "(8 ^ 2)"}, {"expression": 8 << 2 , "total": 32, "name": "(8 << 2)"}, - {"expression": 8 >> 2 , "total": 2, "name": "(8 >> 2)"} + {"expression": 8 >> 2 , "total": 2, "name": "(8 >> 2)"}, + {"expression": 1e3, "total": 1000, "name": "1e3"}, + {"expression": "\u006E\u0069\u006E\u006A\u0061", "total": "ninja", "name": "\\u006E\\u0069\\u006E\\u006A\\u0061"} ]; for (var t = 0; t <= len(tests) - 1; t = t + 1) { diff --git a/testdata/assertions.ninja b/testdata/assertions.ninja index 44d1785..b4783ee 100644 --- a/testdata/assertions.ninja +++ b/testdata/assertions.ninja @@ -1,28 +1,46 @@ import "./testdata/helper.ninja"; -/* -Assertion basic variables -*/ - +/** + * Assertion basic variables + */ +puts("## Testing Assertion Variables\n"); import "./testdata/assert_variable.ninja"; +// ============= END ============== + /* Assertion enums */ - +puts("\n\n## Testing Assertion Enum\n"); import "./testdata/assert_enum.ninja"; +// ============= END ============== + /* Assertion Logic Operators */ +puts("\n\n## Testing Assertion Logic Operators\n"); import "./testdata/assert_logic_operators.ninja"; +// ============= END ============== + /* Assertion Index */ +puts("\n\n## Testing Assertion Index\n"); import "./testdata/assert_index.ninja"; +// ============= END ============== + +/* +Assertion for Functions +*/ +puts("\n\n## Testing Assertion Functions Call\n"); +import "./testdata/assert_function.ninja"; +// ============= END ============== /* Assertion advance stuff */ -import "./testdata/assert_advance.ninja"; \ No newline at end of file +puts("\n\n## Testing Assertion Advance Stuffs\n"); +import "./testdata/assert_advance.ninja"; +// ============= END ============== \ No newline at end of file diff --git a/testdata/expected.txt b/testdata/expected.txt index 9129174..874a2ad 100644 --- a/testdata/expected.txt +++ b/testdata/expected.txt @@ -1,3 +1,5 @@ +## Testing Assertion Variables + OK: Assign var a = 0 OK: Reassign var a = 0 OK: Operations (1 + 1) @@ -12,6 +14,12 @@ OK: Operations (8 | 2) OK: Operations (8 ^ 2) OK: Operations (8 << 2) OK: Operations (8 >> 2) +OK: Operations 1e3 +OK: Operations \u006E\u0069\u006E\u006A\u0061 + + +## Testing Assertion Enum + OK: Enum: 1 OK: Enum: 1.0 OK: Enum: Hello @@ -20,6 +28,10 @@ OK: Enum: 2 OK: Enum: 12.5 OK: Enum: Hello World OK: Enum: false + + +## Testing Assertion Logic Operators + OK: Operations Logic Operator true && true OK: Operations Logic Operator true && false OK: Operations Logic Operator false && true @@ -49,7 +61,23 @@ OK: Operations Logic Operator 2 > 1 OK: Operations Logic Operator 1 >= 1 OK: Operations Logic Operator 2 >= 1 OK: Operations Logic Operator 1 >= 2 + + +## Testing Assertion Index + OK: Index Array OK: Index Array + + +## Testing Assertion Functions Call + +OK: Function call: getNumber() +OK: Function call: passingArguments(3) +OK: Function call: passingRequireArgumentsAndOptional(1) +OK: Function call: passingRequireArgumentsAndOptional(5, 5) + + +## Testing Assertion Advance Stuffs + OK: Array Map -> Reduce OK: Array Filter -> >= 3