diff --git a/README.md b/README.md index 3411cd1..ee3d524 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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. diff --git a/ast/ast.go b/ast/ast.go index 51d5434..ff37ecd 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -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 diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 216a137..9b34a44 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -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: @@ -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) { @@ -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) { diff --git a/lexer/lexer.go b/lexer/lexer.go index f981517..b2be721 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -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(')'): diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 253c9b4..a50fbdc 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -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, "("}, @@ -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) @@ -61,6 +70,8 @@ if(5<10){ 0.3 世界 for +2 >= 1 +1 <= 3 ` tests := []struct { expectedType token.Type @@ -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) @@ -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 { @@ -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, ")"}, diff --git a/parser/parser.go b/parser/parser.go index 7ccc421..214097a 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -25,6 +25,7 @@ const ( LOWEST COND // OR or AND ASSIGN // = + TERNARY // ? : EQUALS // == or != REGEXP_MATCH // !~ ~= LESSGREATER // > or < @@ -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, @@ -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. @@ -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) @@ -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() diff --git a/token/token.go b/token/token.go index c2231a8..275b0d6 100644 --- a/token/token.go +++ b/token/token.go @@ -62,6 +62,7 @@ const ( PERIOD = "." CONTAINS = "~=" NOT_CONTAINS = "!~" + QUESTION = "?" ILLEGAL = "ILLEGAL" )