Skip to content

Commit

Permalink
Implemented else if, closes #27
Browse files Browse the repository at this point in the history
This commit adds the ability to add multiple IF
conditions in an IF block by using `if else`:

```
if x {
  ...
} else if y {
  ...
} else if z {
  ...
} else {
  ...
}
```

I decided to change the data structure that represents
IFs because it was very limited, only allowing for
a condition, a consequence and an alternative consequence.

Now IFs are a list of `Scenario`, made of conditions and
consequences. If we encounter a bare `else` we set the condition
of that scenario to true, so that it always evaluates to true.

One funny thing that this structure does is that it allows
for code like this:

```
if x {
  ...
} else {
  ...
} else if y {
  ...
}
```

(`else` before `else if`)

I honestly don't mind having this (it's a fun easter egg), but
we might want to raise a parser error later on if users find it
annoying.
  • Loading branch information
odino committed Jan 26, 2019
1 parent 7ac6ae1 commit 9bc0f8f
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 49 deletions.
30 changes: 20 additions & 10 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,26 +245,36 @@ func (me *MethodExpression) String() string {
return out.String()
}

type IfExpression struct {
Token token.Token // The 'if' token
// A scenario is used to
// represent a path within an
// IF block (if x = 2 { return x}).
// It has a condition (x = 2)
// and a consenquence (return x).
type Scenario struct {
Condition Expression
Consequence *BlockStatement
Alternative *BlockStatement
}

type IfExpression struct {
Token token.Token // The 'if' token
Scenarios []*Scenario
}

func (ie *IfExpression) expressionNode() {}
func (ie *IfExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IfExpression) String() string {
var out bytes.Buffer

out.WriteString("if")
out.WriteString(ie.Condition.String())
out.WriteString(" ")
out.WriteString(ie.Consequence.String())
for i, s := range ie.Scenarios {
if i != 0 {
out.WriteString("else")
out.WriteString(" ")
}

if ie.Alternative != nil {
out.WriteString("else ")
out.WriteString(ie.Alternative.String())
out.WriteString("if")
out.WriteString(s.Condition.String())
out.WriteString(" ")
out.WriteString(s.Consequence.String())
}

return out.String()
Expand Down
21 changes: 11 additions & 10 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,18 +526,19 @@ func evalIfExpression(
ie *ast.IfExpression,
env *object.Environment,
) object.Object {
condition := Eval(ie.Condition, env)
if isError(condition) {
return condition
}
for _, scenario := range ie.Scenarios {
condition := Eval(scenario.Condition, env)

if isTruthy(condition) {
return Eval(ie.Consequence, env)
} else if ie.Alternative != nil {
return Eval(ie.Alternative, env)
} else {
return NULL
if isError(condition) {
return condition
}

if isTruthy(condition) {
return Eval(scenario.Consequence, env)
}
}

return NULL
}

func evalWhileExpression(
Expand Down
4 changes: 4 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ func TestIfElseExpressions(t *testing.T) {
{"if 1 > 2 { 10 }", nil},
{"if 1 > 2 { 10 } else { 20 }", 20},
{"if 1 < 2 { 10 } else { 20 }", 10},
{"if 3 > 2 { 1 } else if 1 > 0 {2} else if 5 > 0 {3} else {4}", 1},
{"if 1 > 2 { 1 } else if 1 > 0 {2} else if 5 > 0 {3} else {4}", 2},
{"if 1 > 2 { 1 } else if 1 > 1 {2} else if 5 > 0 {3} else {4}", 3},
{"if 1 > 2 { 1 } else if 1 > 1 {2} else if 5 > 10 {3} else {4}", 4},
}

for _, tt := range tests {
Expand Down
10 changes: 10 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ result = add(five, ten);
<=>
if (5 < 10) {
return true;
} else if x {
return 0;
} else {
return false;
}
Expand Down Expand Up @@ -144,6 +146,14 @@ $111
{token.SEMICOLON, ";"},
{token.RBRACE, "}"},
{token.ELSE, "else"},
{token.IF, "if"},
{token.IDENT, "x"},
{token.LBRACE, "{"},
{token.RETURN, "return"},
{token.NUMBER, "0"},
{token.SEMICOLON, ";"},
{token.RBRACE, "}"},
{token.ELSE, "else"},
{token.LBRACE, "{"},
{token.RETURN, "return"},
{token.FALSE, "false"},
Expand Down
47 changes: 40 additions & 7 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,29 +500,62 @@ func (p *Parser) parseGroupedExpression() ast.Expression {

// if x {
// return x
// } esle {
// } else if y {
// return y
// } else {
// return z
// }
func (p *Parser) parseIfExpression() ast.Expression {
expression := &ast.IfExpression{Token: p.curToken}
scenarios := []*ast.Scenario{}

p.nextToken()
expression.Condition = p.parseExpression(LOWEST)
scenario := &ast.Scenario{}
scenario.Condition = p.parseExpression(LOWEST)

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

expression.Consequence = p.parseBlockStatement()
scenario.Consequence = p.parseBlockStatement()
scenarios = append(scenarios, scenario)

if p.peekTokenIs(token.ELSE) {
// If we encounter ELSEs then let's add more
// scenarios to our expression.
for p.peekTokenIs(token.ELSE) {
p.nextToken()
if !p.expectPeek(token.LBRACE) {
return nil
p.nextToken()
scenario := &ast.Scenario{}

// ELSE IF
if p.curTokenIs(token.IF) {
p.nextToken()
scenario.Condition = p.parseExpression(LOWEST)

if !p.expectPeek(token.LBRACE) {
return nil
}
} else {
// This is a regular ELSE block.
//
// In order not to have a weird data structure
// representing an IF expression, we simply define
// it as a list of scenarios.
// In case a simple ELSE if encountered, we set the
// condition of this scenario to true, so that it always
// evaluates to true.
tok := &token.Token{Position: -99, Literal: "true", Type: token.LookupIdent(token.TRUE)}
scenario.Condition = &ast.Boolean{Token: *tok, Value: true}
}

expression.Alternative = p.parseBlockStatement()
scenario.Consequence = p.parseBlockStatement()
scenarios = append(scenarios, scenario)

if !p.peekTokenIs(token.ELSE) {
p.nextToken()
}
}
expression.Scenarios = scenarios
return expression
}

Expand Down
Loading

0 comments on commit 9bc0f8f

Please sign in to comment.