Skip to content

Commit

Permalink
add arguments with default values && elseif conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
gravataLonga committed Aug 3, 2022
1 parent b639354 commit f6c90cf
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 51 deletions.
49 changes: 42 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,16 +390,51 @@ puts(STATUS::OK);
## Conditions


### If / Else
`if (<condition>) { <consequence> } else { <alternative> }`
### If / ElseIf / Else
`if (<condition>) { <consequence> } elseif (<condition1>) { <consequence1> } else { <alternative> }`

```
#### 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

Expand Down Expand Up @@ -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
Expand Down
69 changes: 60 additions & 9 deletions evaluator/function.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package evaluator

import (
"errors"
"fmt"
"github.com/gravataLonga/ninja/ast"
"github.com/gravataLonga/ninja/object"
)
Expand All @@ -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)
Expand All @@ -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)))
}
42 changes: 42 additions & 0 deletions evaluator/function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions parser/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
28 changes: 28 additions & 0 deletions parser/arguments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
58 changes: 31 additions & 27 deletions parser/call_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package parser

import (
"fmt"
"github.com/gravataLonga/ninja/ast"
"github.com/gravataLonga/ninja/lexer"
"strings"
Expand Down Expand Up @@ -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())
}
}
}
})

}
}
2 changes: 1 addition & 1 deletion parser/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions parser/if.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading

0 comments on commit f6c90cf

Please sign in to comment.