Skip to content

Commit 4ee3459

Browse files
authored
Merge pull request #56 from skx/55-ternary
55 ternary
2 parents bdf37c3 + 30956bd commit 4ee3459

File tree

7 files changed

+142
-1
lines changed

7 files changed

+142
-1
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* [2.4.1 The Standard Library](#241-the-standard-library)
2222
* [2.5 Functions](#25-functions)
2323
* [2.6 If-else statements](#26-if-else-statements)
24+
* [2.6.1 Ternary expressions](#261-ternary-expressions)
2425
* [2.7 For-loop statements](#27-for-loop-statements)
2526
* [2.8 Comments](#28-comments)
2627
* [2.9 Postfix Operators](#29-postfix-operators)
@@ -73,6 +74,7 @@ The interpreter in _this_ repository has been significantly extended from the st
7374
* It will now show the line-number of failures (where possible).
7475
* Added support for regular expressions, both literally and via `match`
7576
* `if ( name ~= /steve/i ) { puts( "Hello Steve\n"); } `
77+
* Added support for [ternary expressions](#261-ternary-expressions).
7678

7779

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

370372

373+
### 2.6.1 Ternary Expressions
374+
375+
`monkey` supports the use of ternary expressions, which work as you
376+
would expect with a C-background:
377+
378+
function max(a,b) {
379+
return( a > b ? a : b );
380+
};
381+
382+
puts( "max(1,2) -> ", max(1, 2), "\n" );
383+
puts( "max(-1,-2) -> ", max(-1, -2), "\n" );
384+
385+
Note that in the interests of clarity nested ternary-expressions are illegal!
386+
371387
## 2.7 For-loop statements
372388

373389
`monkey` supports a golang-style for-loop statement.

ast/ast.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,42 @@ func (ie *IfExpression) String() string {
376376
return out.String()
377377
}
378378

379+
// TernaryExpression holds a ternary-expression.
380+
type TernaryExpression struct {
381+
// Token is the actual token.
382+
Token token.Token
383+
384+
// Condition is the thing that is evaluated to determine
385+
// which expression should be returned
386+
Condition Expression
387+
388+
// IfTrue is the expression to return if the condition is true.
389+
IfTrue Expression
390+
391+
// IFFalse is the expression to return if the condition is not true.
392+
IfFalse Expression
393+
}
394+
395+
func (te *TernaryExpression) expressionNode() {}
396+
397+
// TokenLiteral returns the literal token.
398+
func (te *TernaryExpression) TokenLiteral() string { return te.Token.Literal }
399+
400+
// String returns this object as a string.
401+
func (te *TernaryExpression) String() string {
402+
var out bytes.Buffer
403+
404+
out.WriteString("(")
405+
out.WriteString(te.Condition.String())
406+
out.WriteString(" ? ")
407+
out.WriteString(te.IfTrue.String())
408+
out.WriteString(" : ")
409+
out.WriteString(te.IfFalse.String())
410+
out.WriteString(")")
411+
412+
return out.String()
413+
}
414+
379415
// ForLoopExpression holds a for-loop
380416
type ForLoopExpression struct {
381417
// Token is the actual token

evaluator/evaluator.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
7171
return evalBlockStatement(node, env)
7272
case *ast.IfExpression:
7373
return evalIfExpression(node, env)
74+
case *ast.TernaryExpression:
75+
return evalTernaryExpression(node, env)
7476
case *ast.ForLoopExpression:
7577
return evalForLoopExpression(node, env)
7678
case *ast.ReturnStatement:
@@ -562,6 +564,9 @@ func evalStringInfixExpression(operator string, left, right object.Object) objec
562564
left.Type(), operator, right.Type())
563565
}
564566

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

584+
// evalTernaryExpression handles a ternary-expression. If the condition
585+
// is true we return the contents of evaluating the true-branch, otherwise
586+
// the false-branch. (Unlike an `if` statement we know that we always have
587+
// an alternative/false branch.)
588+
func evalTernaryExpression(te *ast.TernaryExpression, env *object.Environment) object.Object {
589+
590+
condition := Eval(te.Condition, env)
591+
if isError(condition) {
592+
return condition
593+
}
594+
595+
if isTruthy(condition) {
596+
return Eval(te.IfTrue, env)
597+
}
598+
return Eval(te.IfFalse, env)
599+
}
600+
579601
func evalAssignStatement(a *ast.AssignStatement, env *object.Environment) (val object.Object) {
580602
evaluated := Eval(a.Value, env)
581603
if isError(evaluated) {

lexer/lexer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ func (l *Lexer) NextToken() token.Token {
107107
}
108108
case rune(';'):
109109
tok = newToken(token.SEMICOLON, l.ch)
110+
case rune('?'):
111+
tok = newToken(token.QUESTION, l.ch)
110112
case rune('('):
111113
tok = newToken(token.LPAREN, l.ch)
112114
case rune(')'):

lexer/lexer_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77
)
88

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

1212
tests := []struct {
1313
expectedType token.Type
1414
expectedLiteral string
1515
}{
16+
{token.MOD, "%"},
1617
{token.ASSIGN, "="},
1718
{token.PLUS, "+"},
1819
{token.LPAREN, "("},
@@ -21,6 +22,14 @@ func TestNextToken1(t *testing.T) {
2122
{token.RBRACE, "}"},
2223
{token.COMMA, ","},
2324
{token.SEMICOLON, ";"},
25+
{token.QUESTION, "?"},
26+
{token.OR, "||"},
27+
{token.AND, "&&"},
28+
{token.BACKTICK, "/bin/ls"},
29+
{token.PLUS_PLUS, "++"},
30+
{token.MINUS_MINUS, "--"},
31+
{token.POW, "**"},
32+
{token.ASTERISK_EQUALS, "*="},
2433
{token.EOF, ""},
2534
}
2635
l := New(input)
@@ -61,6 +70,8 @@ if(5<10){
6170
0.3
6271
世界
6372
for
73+
2 >= 1
74+
1 <= 3
6475
`
6576
tests := []struct {
6677
expectedType token.Type
@@ -156,6 +167,12 @@ for
156167
{token.FLOAT, "0.3"},
157168
{token.IDENT, "世界"},
158169
{token.FOR, "for"},
170+
{token.INT, "2"},
171+
{token.GT_EQUALS, ">="},
172+
{token.INT, "1"},
173+
{token.INT, "1"},
174+
{token.LT_EQUALS, "<="},
175+
{token.INT, "3"},
159176
{token.EOF, ""},
160177
}
161178
l := New(input)
@@ -497,6 +514,7 @@ func TestRegexp(t *testing.T) {
497514
input := `if ( f ~= /steve/i )
498515
if ( f ~= /steve/m )
499516
if ( f ~= /steve/mi )
517+
if ( f !~ /steve/mi )
500518
if ( f ~= /steve/miiiiiiiiiiiiiiiiimmmmmmmmmmmmmiiiii )`
501519

502520
tests := []struct {
@@ -524,6 +542,12 @@ if ( f ~= /steve/miiiiiiiiiiiiiiiiimmmmmmmmmmmmmiiiii )`
524542
{token.IF, "if"},
525543
{token.LPAREN, "("},
526544
{token.IDENT, "f"},
545+
{token.NOT_CONTAINS, "!~"},
546+
{token.REGEXP, "(?mi)steve"},
547+
{token.RPAREN, ")"},
548+
{token.IF, "if"},
549+
{token.LPAREN, "("},
550+
{token.IDENT, "f"},
527551
{token.CONTAINS, "~="},
528552
{token.REGEXP, "(?mi)steve"},
529553
{token.RPAREN, ")"},

parser/parser.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
LOWEST
2626
COND // OR or AND
2727
ASSIGN // =
28+
TERNARY // ? :
2829
EQUALS // == or !=
2930
REGEXP_MATCH // !~ ~=
3031
LESSGREATER // > or <
@@ -39,6 +40,7 @@ const (
3940

4041
// each token precedence
4142
var precedences = map[token.Type]int{
43+
token.QUESTION: TERNARY,
4244
token.ASSIGN: ASSIGN,
4345
token.EQ: EQUALS,
4446
token.NOT_EQ: EQUALS,
@@ -95,6 +97,11 @@ type Parser struct {
9597
// postfixParseFns holds a map of parsing methods for
9698
// postfix-based syntax.
9799
postfixParseFns map[token.Type]postfixParseFn
100+
101+
// are we inside a ternary expression?
102+
//
103+
// Nested ternary expressions are illegal :)
104+
tern bool
98105
}
99106

100107
// New returns our new parser-object.
@@ -148,6 +155,7 @@ func New(l *lexer.Lexer) *Parser {
148155
p.registerInfix(token.SLASH_EQUALS, p.parseAssignExpression)
149156
p.registerInfix(token.CONTAINS, p.parseInfixExpression)
150157
p.registerInfix(token.NOT_CONTAINS, p.parseInfixExpression)
158+
p.registerInfix(token.QUESTION, p.parseTernaryExpression)
151159

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

393+
// parseTernaryExpression parses a ternary expression
394+
func (p *Parser) parseTernaryExpression(condition ast.Expression) ast.Expression {
395+
396+
if p.tern {
397+
msg := fmt.Sprintf("nested ternary expressions are illegal, around line %d", p.l.GetLine())
398+
p.errors = append(p.errors, msg)
399+
return nil
400+
}
401+
402+
p.tern = true
403+
defer func() { p.tern = false }()
404+
405+
expression := &ast.TernaryExpression{
406+
Token: p.curToken,
407+
Condition: condition,
408+
}
409+
p.nextToken() //skip the '?'
410+
precedence := p.curPrecedence()
411+
expression.IfTrue = p.parseExpression(precedence)
412+
413+
if !p.expectPeek(token.COLON) { //skip the ":"
414+
return nil
415+
}
416+
417+
// Get to next token, then parse the else part
418+
p.nextToken()
419+
expression.IfFalse = p.parseExpression(precedence)
420+
421+
p.tern = false
422+
return expression
423+
}
424+
385425
// parseGroupedExpression parses a grouped-expression.
386426
func (p *Parser) parseGroupedExpression() ast.Expression {
387427
p.nextToken()

token/token.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const (
6262
PERIOD = "."
6363
CONTAINS = "~="
6464
NOT_CONTAINS = "!~"
65+
QUESTION = "?"
6566
ILLEGAL = "ILLEGAL"
6667
)
6768

0 commit comments

Comments
 (0)