From 9aa1c6519932acce049534fc6e43b45d2996b74e Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 17:47:54 +0200 Subject: [PATCH 01/11] Add new question-mark token for ternary operation --- token/token.go | 1 + 1 file changed, 1 insertion(+) 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" ) From fa0b2f2f448de5c8b26db78a91f1535419c010c0 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 17:54:46 +0200 Subject: [PATCH 02/11] Lex "?" correctly. Also bumped test-coverage of some of our other simple tokens, since I was touching this file. --- lexer/lexer.go | 2 ++ lexer/lexer_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) 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, ")"}, From 4271bce6c470774372d8569c4be8b617eeb3b640 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:07:26 +0200 Subject: [PATCH 03/11] Define AST-node for ternary expressions. --- ast/ast.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) 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 From 0443063a31102fff294f60f733cb15de6e032525 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:36:38 +0200 Subject: [PATCH 04/11] Parse the ternary-expression into the new AST-type. --- parser/parser.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/parser/parser.go b/parser/parser.go index 7ccc421..31a3942 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, @@ -148,6 +150,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 +385,27 @@ 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 { + 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) + + return expression +} + // parseGroupedExpression parses a grouped-expression. func (p *Parser) parseGroupedExpression() ast.Expression { p.nextToken() From 426f696744c8bc331c01402056fa14278176f11d Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:38:57 +0200 Subject: [PATCH 05/11] We now handle a ternary operation at run-time. --- evaluator/evaluator.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) 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) { From e410860d9919cf2ddd57a91926a940c0d36a796e Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:41:18 +0200 Subject: [PATCH 06/11] Document ternary expressions. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 3411cd1..f52395a 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) @@ -368,6 +369,18 @@ 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" ); + ## 2.7 For-loop statements `monkey` supports a golang-style for-loop statement. From 48c51534a9fd2f6a4a25a8c3294b73154a7c6f06 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:41:56 +0200 Subject: [PATCH 07/11] Mention ternary-expressions in my list of changes --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f52395a..863c079 100644 --- a/README.md +++ b/README.md @@ -74,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 From 05fea2fd25313a8dd74616d102b4eaffb6877776 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:43:58 +0200 Subject: [PATCH 08/11] Indent ternary subsection --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 863c079..f007ade 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ The same thing works for literal functions: puts( max(1, 2) ); // Outputs: 2 -## 2.6.1 Ternary Expressions +### 2.6.1 Ternary Expressions `monkey` supports the use of ternary expressions, which work as you would expect with a C-background: From f7224add9057aa9b392c5b43868d752b163f811d Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:44:44 +0200 Subject: [PATCH 09/11] Indent ternary subsection --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f007ade..aa874dd 100644 --- a/README.md +++ b/README.md @@ -21,7 +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.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) From 16d23a702fea8a84492c114dfd56ed4771b2e840 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:51:26 +0200 Subject: [PATCH 10/11] Nested ternary expressions are illegal --- parser/parser.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/parser/parser.go b/parser/parser.go index 31a3942..214097a 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -97,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. @@ -387,6 +392,16 @@ func (p *Parser) parseInfixExpression(left ast.Expression) ast.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, @@ -403,6 +418,7 @@ func (p *Parser) parseTernaryExpression(condition ast.Expression) ast.Expression p.nextToken() expression.IfFalse = p.parseExpression(precedence) + p.tern = false return expression } From 30956bdb7adb28e50075c5fb7c89114fbe66fd99 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 18 Dec 2019 20:53:08 +0200 Subject: [PATCH 11/11] Document nested ternary expressions being illegal --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index aa874dd..ee3d524 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,8 @@ would expect with a C-background: 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.