Skip to content

Commit

Permalink
Merge pull request #56 from skx/55-ternary
Browse files Browse the repository at this point in the history
55 ternary
  • Loading branch information
skx committed Dec 18, 2019
2 parents bdf37c3 + 30956bd commit 4ee3459
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 1 deletion.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* [2.4.1 The Standard Library](#241-the-standard-library)
* [2.5 Functions](#25-functions)
* [2.6 If-else statements](#26-if-else-statements)
* [2.6.1 Ternary expressions](#261-ternary-expressions)
* [2.7 For-loop statements](#27-for-loop-statements)
* [2.8 Comments](#28-comments)
* [2.9 Postfix Operators](#29-postfix-operators)
Expand Down Expand Up @@ -73,6 +74,7 @@ The interpreter in _this_ repository has been significantly extended from the st
* It will now show the line-number of failures (where possible).
* Added support for regular expressions, both literally and via `match`
* `if ( name ~= /steve/i ) { puts( "Hello Steve\n"); } `
* Added support for [ternary expressions](#261-ternary-expressions).


## 1. Installation
Expand Down Expand Up @@ -368,6 +370,20 @@ The same thing works for literal functions:
puts( max(1, 2) ); // Outputs: 2


### 2.6.1 Ternary Expressions

`monkey` supports the use of ternary expressions, which work as you
would expect with a C-background:

function max(a,b) {
return( a > b ? a : b );
};

puts( "max(1,2) -> ", max(1, 2), "\n" );
puts( "max(-1,-2) -> ", max(-1, -2), "\n" );

Note that in the interests of clarity nested ternary-expressions are illegal!

## 2.7 For-loop statements

`monkey` supports a golang-style for-loop statement.
Expand Down
36 changes: 36 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,42 @@ func (ie *IfExpression) String() string {
return out.String()
}

// TernaryExpression holds a ternary-expression.
type TernaryExpression struct {
// Token is the actual token.
Token token.Token

// Condition is the thing that is evaluated to determine
// which expression should be returned
Condition Expression

// IfTrue is the expression to return if the condition is true.
IfTrue Expression

// IFFalse is the expression to return if the condition is not true.
IfFalse Expression
}

func (te *TernaryExpression) expressionNode() {}

// TokenLiteral returns the literal token.
func (te *TernaryExpression) TokenLiteral() string { return te.Token.Literal }

// String returns this object as a string.
func (te *TernaryExpression) String() string {
var out bytes.Buffer

out.WriteString("(")
out.WriteString(te.Condition.String())
out.WriteString(" ? ")
out.WriteString(te.IfTrue.String())
out.WriteString(" : ")
out.WriteString(te.IfFalse.String())
out.WriteString(")")

return out.String()
}

// ForLoopExpression holds a for-loop
type ForLoopExpression struct {
// Token is the actual token
Expand Down
22 changes: 22 additions & 0 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
return evalBlockStatement(node, env)
case *ast.IfExpression:
return evalIfExpression(node, env)
case *ast.TernaryExpression:
return evalTernaryExpression(node, env)
case *ast.ForLoopExpression:
return evalForLoopExpression(node, env)
case *ast.ReturnStatement:
Expand Down Expand Up @@ -562,6 +564,9 @@ func evalStringInfixExpression(operator string, left, right object.Object) objec
left.Type(), operator, right.Type())
}

// evalIfExpression handles an `if` expression, running the block
// if the condition matches, and running any optional else block
// otherwise.
func evalIfExpression(ie *ast.IfExpression, env *object.Environment) object.Object {
condition := Eval(ie.Condition, env)
if isError(condition) {
Expand All @@ -576,6 +581,23 @@ func evalIfExpression(ie *ast.IfExpression, env *object.Environment) object.Obje
}
}

// evalTernaryExpression handles a ternary-expression. If the condition
// is true we return the contents of evaluating the true-branch, otherwise
// the false-branch. (Unlike an `if` statement we know that we always have
// an alternative/false branch.)
func evalTernaryExpression(te *ast.TernaryExpression, env *object.Environment) object.Object {

condition := Eval(te.Condition, env)
if isError(condition) {
return condition
}

if isTruthy(condition) {
return Eval(te.IfTrue, env)
}
return Eval(te.IfFalse, env)
}

func evalAssignStatement(a *ast.AssignStatement, env *object.Environment) (val object.Object) {
evaluated := Eval(a.Value, env)
if isError(evaluated) {
Expand Down
2 changes: 2 additions & 0 deletions lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ func (l *Lexer) NextToken() token.Token {
}
case rune(';'):
tok = newToken(token.SEMICOLON, l.ch)
case rune('?'):
tok = newToken(token.QUESTION, l.ch)
case rune('('):
tok = newToken(token.LPAREN, l.ch)
case rune(')'):
Expand Down
26 changes: 25 additions & 1 deletion lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import (
)

func TestNextToken1(t *testing.T) {
input := `=+(){},;`
input := "%=+(){},;?|| &&`/bin/ls`++--***="

tests := []struct {
expectedType token.Type
expectedLiteral string
}{
{token.MOD, "%"},
{token.ASSIGN, "="},
{token.PLUS, "+"},
{token.LPAREN, "("},
Expand All @@ -21,6 +22,14 @@ func TestNextToken1(t *testing.T) {
{token.RBRACE, "}"},
{token.COMMA, ","},
{token.SEMICOLON, ";"},
{token.QUESTION, "?"},
{token.OR, "||"},
{token.AND, "&&"},
{token.BACKTICK, "/bin/ls"},
{token.PLUS_PLUS, "++"},
{token.MINUS_MINUS, "--"},
{token.POW, "**"},
{token.ASTERISK_EQUALS, "*="},
{token.EOF, ""},
}
l := New(input)
Expand Down Expand Up @@ -61,6 +70,8 @@ if(5<10){
0.3
世界
for
2 >= 1
1 <= 3
`
tests := []struct {
expectedType token.Type
Expand Down Expand Up @@ -156,6 +167,12 @@ for
{token.FLOAT, "0.3"},
{token.IDENT, "世界"},
{token.FOR, "for"},
{token.INT, "2"},
{token.GT_EQUALS, ">="},
{token.INT, "1"},
{token.INT, "1"},
{token.LT_EQUALS, "<="},
{token.INT, "3"},
{token.EOF, ""},
}
l := New(input)
Expand Down Expand Up @@ -497,6 +514,7 @@ func TestRegexp(t *testing.T) {
input := `if ( f ~= /steve/i )
if ( f ~= /steve/m )
if ( f ~= /steve/mi )
if ( f !~ /steve/mi )
if ( f ~= /steve/miiiiiiiiiiiiiiiiimmmmmmmmmmmmmiiiii )`

tests := []struct {
Expand Down Expand Up @@ -524,6 +542,12 @@ if ( f ~= /steve/miiiiiiiiiiiiiiiiimmmmmmmmmmmmmiiiii )`
{token.IF, "if"},
{token.LPAREN, "("},
{token.IDENT, "f"},
{token.NOT_CONTAINS, "!~"},
{token.REGEXP, "(?mi)steve"},
{token.RPAREN, ")"},
{token.IF, "if"},
{token.LPAREN, "("},
{token.IDENT, "f"},
{token.CONTAINS, "~="},
{token.REGEXP, "(?mi)steve"},
{token.RPAREN, ")"},
Expand Down
40 changes: 40 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
LOWEST
COND // OR or AND
ASSIGN // =
TERNARY // ? :
EQUALS // == or !=
REGEXP_MATCH // !~ ~=
LESSGREATER // > or <
Expand All @@ -39,6 +40,7 @@ const (

// each token precedence
var precedences = map[token.Type]int{
token.QUESTION: TERNARY,
token.ASSIGN: ASSIGN,
token.EQ: EQUALS,
token.NOT_EQ: EQUALS,
Expand Down Expand Up @@ -95,6 +97,11 @@ type Parser struct {
// postfixParseFns holds a map of parsing methods for
// postfix-based syntax.
postfixParseFns map[token.Type]postfixParseFn

// are we inside a ternary expression?
//
// Nested ternary expressions are illegal :)
tern bool
}

// New returns our new parser-object.
Expand Down Expand Up @@ -148,6 +155,7 @@ func New(l *lexer.Lexer) *Parser {
p.registerInfix(token.SLASH_EQUALS, p.parseAssignExpression)
p.registerInfix(token.CONTAINS, p.parseInfixExpression)
p.registerInfix(token.NOT_CONTAINS, p.parseInfixExpression)
p.registerInfix(token.QUESTION, p.parseTernaryExpression)

p.postfixParseFns = make(map[token.Type]postfixParseFn)
p.registerPostfix(token.PLUS_PLUS, p.parsePostfixExpression)
Expand Down Expand Up @@ -382,6 +390,38 @@ func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression {
return expression
}

// parseTernaryExpression parses a ternary expression
func (p *Parser) parseTernaryExpression(condition ast.Expression) ast.Expression {

if p.tern {
msg := fmt.Sprintf("nested ternary expressions are illegal, around line %d", p.l.GetLine())
p.errors = append(p.errors, msg)
return nil
}

p.tern = true
defer func() { p.tern = false }()

expression := &ast.TernaryExpression{
Token: p.curToken,
Condition: condition,
}
p.nextToken() //skip the '?'
precedence := p.curPrecedence()
expression.IfTrue = p.parseExpression(precedence)

if !p.expectPeek(token.COLON) { //skip the ":"
return nil
}

// Get to next token, then parse the else part
p.nextToken()
expression.IfFalse = p.parseExpression(precedence)

p.tern = false
return expression
}

// parseGroupedExpression parses a grouped-expression.
func (p *Parser) parseGroupedExpression() ast.Expression {
p.nextToken()
Expand Down
1 change: 1 addition & 0 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
PERIOD = "."
CONTAINS = "~="
NOT_CONTAINS = "!~"
QUESTION = "?"
ILLEGAL = "ILLEGAL"
)

Expand Down

0 comments on commit 4ee3459

Please sign in to comment.