From 09891e162ed4d67c7a204bde21a0016899f3a9e3 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 08:59:31 +0300 Subject: [PATCH 1/7] Define new token-types for switch-handling We need to add `switch`, `case`, and `default`. --- token/token.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/token/token.go b/token/token.go index 23a86fd..cc10d4e 100644 --- a/token/token.go +++ b/token/token.go @@ -19,10 +19,12 @@ const ( ASTERISK_EQUALS = "*=" BACKTICK = "`" BANG = "!" + CASE = "case" COLON = ":" COMMA = "," CONST = "CONST" CONTAINS = "~=" + DEFAULT = "DEFAULT" DEFINE_FUNCTION = "DEFINE_FUNCTION" DOTDOT = ".." ELSE = "ELSE" @@ -36,9 +38,9 @@ const ( GT = ">" GT_EQUALS = ">=" IDENT = "IDENT" - IN = "IN" IF = "IF" ILLEGAL = "ILLEGAL" + IN = "IN" INT = "INT" LBRACE = "{" LBRACKET = "[" @@ -68,12 +70,15 @@ const ( SLASH = "/" SLASH_EQUALS = "/=" STRING = "STRING" + SWITCH = "switch" TRUE = "TRUE" ) // reversed keywords var keywords = map[string]Type{ + "case": CASE, "const": CONST, + "default": DEFAULT, "else": ELSE, "false": FALSE, "fn": FUNCTION, @@ -84,6 +89,7 @@ var keywords = map[string]Type{ "in": IN, "let": LET, "return": RETURN, + "switch": SWITCH, "true": TRUE, } From 14a50dcf125b1fbdec3f612c804cfd956e0e29b7 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 09:00:07 +0300 Subject: [PATCH 2/7] Define AST for the handling of switch/case expressions We don't yet use these, but they should be the kind of thing that we need. We have two sections: * One for the "switch .." which contains a series of * "case ..." expressions, one of which may be a default. --- ast/ast.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/ast/ast.go b/ast/ast.go index 7bd2270..9c4dd02 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -785,3 +785,72 @@ func (as *AssignStatement) String() string { out.WriteString(as.Value.String()) return out.String() } + +// CaseExpression handles the case within a switch statement +type CaseExpression struct { + // Token is the actual token + Token token.Token + + // Default branch? + Default bool + + // The thing we match + Expr Expression + + // The code to execute if there is a match + Block *BlockStatement +} + +func (ce *CaseExpression) expressionNode() {} + +// TokenLiteral returns the literal token. +func (ce *CaseExpression) TokenLiteral() string { return ce.Token.Literal } + +// String returns this object as a string. +func (ce *CaseExpression) String() string { + var out bytes.Buffer + + if ce.Default { + out.WriteString("default ") + } else { + out.WriteString("case ") + out.WriteString(ce.Expr.String()) + } + out.WriteString(ce.Block.String()) + return out.String() +} + +// SwitchExpression handles a switch statement +type SwitchExpression struct { + // Token is the actual token + Token token.Token + + // Value is the thing that is evaluated to determine + // which block should be executed. + Value Expression + + // The branches we handle + Choices []*CaseExpression +} + +func (se *SwitchExpression) expressionNode() {} + +// TokenLiteral returns the literal token. +func (se *SwitchExpression) TokenLiteral() string { return se.Token.Literal } + +// String returns this object as a string. +func (se *SwitchExpression) String() string { + var out bytes.Buffer + out.WriteString("\nswitch (") + out.WriteString(se.Value.String()) + out.WriteString(")\n{\n") + + for _, tmp := range se.Choices { + if tmp != nil { + out.WriteString(tmp.String()) + } + } + out.WriteString("}\n") + + return out.String() +} From 56a2fedc9a3eec460a6e8cf3d747ff37c67d5719 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 11:45:42 +0300 Subject: [PATCH 3/7] Parse switch/case statements --- parser/parser.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/parser/parser.go b/parser/parser.go index b1d74a2..482c397 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -139,6 +139,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.REGEXP, p.parseRegexpLiteral) p.registerPrefix(token.REGEXP, p.parseRegexpLiteral) p.registerPrefix(token.STRING, p.parseStringLiteral) + p.registerPrefix(token.SWITCH, p.parseSwitchStatement) p.registerPrefix(token.TRUE, p.parseBoolean) // Register infix functions @@ -391,6 +392,89 @@ func (p *Parser) parseFloatLiteral() ast.Expression { return flo } +// parseSwitchStatement handles a switch statement +func (p *Parser) parseSwitchStatement() ast.Expression { + + // switch + expression := &ast.SwitchExpression{Token: p.curToken} + if expression == nil { + return nil + } + if !p.expectPeek(token.LPAREN) { + return nil + } + p.nextToken() + expression.Value = p.parseExpression(LOWEST) + if expression.Value == nil { + fmt.Printf("error\n") + return nil + } + if !p.expectPeek(token.RPAREN) { + fmt.Printf("error\n") + return nil + } + + // Now we have a block containing blocks. + if !p.expectPeek(token.LBRACE) { + return nil + } + p.nextToken() + + // Process the block which we think will contain + // various case-statements + for !p.curTokenIs(token.RBRACE) { + + tmp := &ast.CaseExpression{Token: p.curToken} + + // Default will be handled specially + if p.curTokenIs(token.DEFAULT) { + + // We have a default-case here. + tmp.Default = true + + } else if p.curTokenIs(token.CASE) { + + // skip "case" + p.nextToken() + + // parse the match-expression. + tmp.Expr = p.parseExpression(LOWEST) + } + + if !p.expectPeek(token.LBRACE) { + + msg := fmt.Sprintf("expected token to be '{', got %s instead", p.curToken.Type) + p.errors = append(p.errors, msg) + fmt.Printf("error\n") + return nil + } + + // parse the block + tmp.Block = p.parseBlockStatement() + + if !p.curTokenIs(token.RBRACE) { + msg := fmt.Sprintf("Syntax Error: expected token to be '}', got %s instead", p.curToken.Type) + p.errors = append(p.errors, msg) + fmt.Printf("error\n") + return nil + + } + p.nextToken() + + // save the choice away + expression.Choices = append(expression.Choices, tmp) + + } + + // ensure we're at the the closing "}" + if !p.curTokenIs(token.RBRACE) { + return nil + } + + return expression + +} + // parseBoolean parses a boolean token. func (p *Parser) parseBoolean() ast.Expression { return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)} From f05912a45db81d9cca6ba29b359b298fe9da66f5 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 11:45:52 +0300 Subject: [PATCH 4/7] Execute switch/case statements --- evaluator/evaluator.go | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index cf243f8..5af3c51 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -166,6 +166,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object { return evalAssignStatement(node, env) case *ast.HashLiteral: return evalHashLiteral(node, env) + case *ast.SwitchExpression: + return evalSwitchStatement(node, env) } return nil } @@ -731,6 +733,60 @@ func evalAssignStatement(a *ast.AssignStatement, env *object.Environment) (val o return evaluated } +func evalSwitchStatement(se *ast.SwitchExpression, env *object.Environment) object.Object { + + // Get the value. + obj := Eval(se.Value, env) + + // Try all the choices + for _, opt := range se.Choices { + + // skipping the default-case, which we'll + // handle later. + if opt.Default { + continue + } + + // Get the value of the case + val := Eval(opt.Expr, env) + + // Is it a literal match? + if obj.Type() == val.Type() && + (obj.Inspect() == val.Inspect()) { + + // Evaluate the block and return the value + out := evalBlockStatement(opt.Block, env) + return out + } + + // Is it a regexp-match? + if val.Type() == object.REGEXP_OBJ { + + m := matches(obj, val, env) + if m == TRUE { + + // Evaluate the block and return the value + out := evalBlockStatement(opt.Block, env) + return out + + } + } + } + + // No match? Handle default if present + for _, opt := range se.Choices { + + // skip default + if opt.Default { + + out := evalBlockStatement(opt.Block, env) + return out + } + } + + return nil +} + func evalForLoopExpression(fle *ast.ForLoopExpression, env *object.Environment) object.Object { rt := &object.Boolean{Value: true} for { From c57ee2eecbcc89190e5b8ee700a9dea43effa5bd Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 12:27:06 +0300 Subject: [PATCH 5/7] Document switch/case --- README.md | 61 +++++++++++++++++++++++++++++++++------------ examples/switch.mon | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 examples/switch.mon diff --git a/README.md b/README.md index 775cab6..48538c7 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,15 @@ * [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.7.1 Foreach statements](#271-foreach-statements) - * [2.8 Comments](#28-comments) - * [2.9 Postfix Operators](#29-postfix-operators) - * [2.10 Command Execution](#210-command-execution) - * [2.11 Regular Expressions](#211-regular-expressions) - * [2.12 File I/O](#212-file-io) - * [2.13 File Operations](#213-file-operations) + * [2.7 Switch statements](#27-switch-statements) + * [2.8 For-loop statements](#28-for-loop-statements) + * [2.8.1 Foreach statements](#281-foreach-statements) + * [2.9 Comments](#29-comments) + * [2.10 Postfix Operators](#29-postfix-operators) + * [2.11 Command Execution](#211-command-execution) + * [2.12 Regular Expressions](#212-regular-expressions) + * [2.13 File I/O](#213-file-io) + * [2.14 File Operations](#214-file-operations) * [3. Object Methods](#3-object-methods) * [3.1 Defininig New Object Methods](#31-defininig-new-object-methods) * [Github Setup](#github-setup) @@ -80,6 +81,8 @@ The interpreter in _this_ repository has been significantly extended from the st * Added the ability to iterate over the contents of arrays, hashes, and strings via the `foreach` statement. * Added `printf` and `sprintf` primitives, which work as you would expect. * `printf( "%d %s", 3, "Steve" );` +* Added support for `switch` statements, with block-based `case` expressions. + * No bugs due to C-style "fall-through". ## 1. Installation @@ -397,7 +400,33 @@ would expect with a C-background: Note that in the interests of clarity nested ternary-expressions are illegal! -## 2.7 For-loop statements +## 2.7 Switch Statements + +Monkey supports the `switch` and `case` expressions, as the following example demonstrates: + +``` + name = "Steve"; + + switch( name ) { + case /^steve$/i { + printf("Hello Steve - we matched you via a regexp\n"); + } + case "St" + "even" { + printf("Hello SteveN, you were matched via an expression\n" ); + } + case 3 { + printf("Hello number three, we matched you literally.\n"); + } + default { + printf("Default case: %s\n", string(name) ); + } + } +``` + +See also [examples/switch.mon](examples/switch.mon). + + +## 2.8 For-loop statements `monkey` supports a golang-style for-loop statement. @@ -415,7 +444,7 @@ Note that in the interests of clarity nested ternary-expressions are illegal! puts(sum(100)); // Outputs: 4950 -## 2.7.1 Foreach statements +## 2.8.1 Foreach statements In addition to iterating over items with the `for` statement, as shown above, it is also possible to iterate over various items via the `foreach` statement. @@ -437,7 +466,7 @@ The same style of iteration works for Arrays, Hashes, and the characters which m When iterating over hashes you can receive either the keys, or the keys and value at each step in the iteration, otherwise you receive the value and an optional index. -## 2.8 Comments +## 2.9 Comments `monkey` support two kinds of comments: @@ -445,7 +474,7 @@ When iterating over hashes you can receive either the keys, or the keys and valu * Multiline comments between `/*` and `*/`. -## 2.9 Postfix Operators +## 2.10 Postfix Operators The `++` and `--` modifiers are permitted for integer-variables, for example the following works as you would expect showing the numbers from `0` to `5`: @@ -473,7 +502,7 @@ The update-operators work with integers and doubles by default, when it comes to puts( str ); // -> "Forename Surname\n" -## 2.10 Command Execution +## 2.11 Command Execution As with many scripting languages commands may be executed via the backtick operator (`\``). @@ -491,7 +520,7 @@ The output will be a hash with two keys `stdout` and `stderr`. NULL is returned if the execution fails. This can be seen in [examples/exec.mon](examples/exec.mon). -## 2.11 Regular Expressions +## 2.12 Regular Expressions The `match` function allows matching a string against a regular-expression. @@ -511,7 +540,7 @@ You can also perform matching (complete with captures), with a literal regular e printf("Matched! %s.%s.%s.%s\n", $1, $2, $3, $4 ); } -## 2.12 File I/O +## 2.13 File I/O The `open` primitive is used to open files, and can be used to open files for either reading, or writing: @@ -553,7 +582,7 @@ By default three filehandles will be made available, as constants: * Used for writing messages. -## 2.13 File Operations +## 2.14 File Operations The primitive `stat` will return a hash of details about the given file, or directory entry. diff --git a/examples/switch.mon b/examples/switch.mon new file mode 100644 index 0000000..d60ce36 --- /dev/null +++ b/examples/switch.mon @@ -0,0 +1,43 @@ +// A simple test-function for switch-statements. +function test( name ) { + + // Did we match? + m = false; + + switch( name ) { + case /^steve$/i { + printf("Hello Steve - we matched you via a regexp\n"); + m = true; + } + case "St" + "even" { + printf("Hello SteveN, you were matched via an expression\n" ); + m = true; + } + case 3 { + printf("Hello number three, we matched you literally.\n"); + m = true; + } + default { + printf("Default case: %s\n", string(name) ); + } + } + + // Show we matched, if we did. + if ( m ) { printf( "\tMatched!\n"); } +} + +// Test the switch statement +test( "Steve" ); // Regexp match +test( "Steven" ); // Literal match +test( 3 ); // Literal match + +// Unhandled/Default cases +test( "Bob" ); +test( false ); + +// Try some other numbers - only one will match +foreach number in 1..10 { + test(number); +} + +printf( "All done\n" ); \ No newline at end of file From 6f282f70e588d62c9f64618cda697c7196d38ba9 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 14:12:35 +0300 Subject: [PATCH 6/7] Ensure there is only a single default-case in switch expression Added basic test of this. --- parser/parser.go | 35 ++++++++++++++++++++++++++++------- parser/parser_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 482c397..6a1cf38 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -397,20 +397,17 @@ func (p *Parser) parseSwitchStatement() ast.Expression { // switch expression := &ast.SwitchExpression{Token: p.curToken} - if expression == nil { - return nil - } + + // look for (xx) if !p.expectPeek(token.LPAREN) { return nil } p.nextToken() expression.Value = p.parseExpression(LOWEST) if expression.Value == nil { - fmt.Printf("error\n") return nil } if !p.expectPeek(token.RPAREN) { - fmt.Printf("error\n") return nil } @@ -424,6 +421,10 @@ func (p *Parser) parseSwitchStatement() ast.Expression { // various case-statements for !p.curTokenIs(token.RBRACE) { + if p.curTokenIs(token.EOF) { + p.errors = append(p.errors, "unterminated switch statement") + return nil + } tmp := &ast.CaseExpression{Token: p.curToken} // Default will be handled specially @@ -437,8 +438,15 @@ func (p *Parser) parseSwitchStatement() ast.Expression { // skip "case" p.nextToken() - // parse the match-expression. - tmp.Expr = p.parseExpression(LOWEST) + // Here we allow "case default" even though + // most people would prefer to write "default". + if p.curTokenIs(token.DEFAULT) { + tmp.Default = true + } else { + + // parse the match-expression. + tmp.Expr = p.parseExpression(LOWEST) + } } if !p.expectPeek(token.LBRACE) { @@ -471,6 +479,19 @@ func (p *Parser) parseSwitchStatement() ast.Expression { return nil } + // More than one default is a bug + count := 0 + for _, c := range expression.Choices { + if c.Default { + count++ + } + } + if count > 1 { + msg := fmt.Sprintf("A switch-statement should only have one default block") + p.errors = append(p.errors, msg) + return nil + + } return expression } diff --git a/parser/parser_test.go b/parser/parser_test.go index 5e3a538..5d264c5 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -881,6 +881,7 @@ func TestIncompleThings(t *testing.T) { `const x =`, `function foo( a, b ="steve", `, `function foo() {`, + `switch (foo) { `, } for _, str := range input { @@ -897,3 +898,33 @@ func TestIncompleThings(t *testing.T) { } } } + +func TestMultiDefault(t *testing.T) { + input := ` +switch( val ) { + case 1 { + printf("yksi"); + } + case 2 { + printf("kaksi"); + } + case default { + printf("OK\n"); + } + default { + printf("Two default blocks? Oh noes\n" ); + } +}` + + l := lexer.New(input) + p := New(l) + _ = p.ParseProgram() + + if len(p.errors) < 1 { + t.Errorf("unexpected error-count, got %d expected %d", len(p.errors), 1) + } + + if !strings.Contains(p.errors[0], "only have one default block") { + t.Errorf("Unexpected error-message %s\n", p.errors[0]) + } +} From b7861201556111dda210a5da0a313906c4611217 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Sun, 13 Sep 2020 14:25:57 +0300 Subject: [PATCH 7/7] Allow matching multiple-expressions in a case-statement --- ast/ast.go | 9 +++++++-- evaluator/evaluator.go | 34 +++++++++++++++++++--------------- examples/switch.mon | 7 ++++--- parser/parser.go | 13 ++++++++++++- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 9c4dd02..f3f9f39 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -795,7 +795,7 @@ type CaseExpression struct { Default bool // The thing we match - Expr Expression + Expr []Expression // The code to execute if there is a match Block *BlockStatement @@ -814,7 +814,12 @@ func (ce *CaseExpression) String() string { out.WriteString("default ") } else { out.WriteString("case ") - out.WriteString(ce.Expr.String()) + + tmp := []string{} + for _, exp := range ce.Expr { + tmp = append(tmp, exp.String()) + } + out.WriteString(strings.Join(tmp, ",")) } out.WriteString(ce.Block.String()) return out.String() diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 5af3c51..86845dd 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -747,28 +747,32 @@ func evalSwitchStatement(se *ast.SwitchExpression, env *object.Environment) obje continue } - // Get the value of the case - val := Eval(opt.Expr, env) + // Look at any expression we've got in this case. + for _, val := range opt.Expr { - // Is it a literal match? - if obj.Type() == val.Type() && - (obj.Inspect() == val.Inspect()) { + // Get the value of the case + out := Eval(val, env) - // Evaluate the block and return the value - out := evalBlockStatement(opt.Block, env) - return out - } - - // Is it a regexp-match? - if val.Type() == object.REGEXP_OBJ { - - m := matches(obj, val, env) - if m == TRUE { + // Is it a literal match? + if obj.Type() == out.Type() && + (obj.Inspect() == out.Inspect()) { // Evaluate the block and return the value out := evalBlockStatement(opt.Block, env) return out + } + + // Is it a regexp-match? + if out.Type() == object.REGEXP_OBJ { + + m := matches(obj, out, env) + if m == TRUE { + + // Evaluate the block and return the value + out := evalBlockStatement(opt.Block, env) + return out + } } } } diff --git a/examples/switch.mon b/examples/switch.mon index d60ce36..74a706f 100644 --- a/examples/switch.mon +++ b/examples/switch.mon @@ -5,7 +5,7 @@ function test( name ) { m = false; switch( name ) { - case /^steve$/i { + case /^steve$/ , /^STEVE$/i { printf("Hello Steve - we matched you via a regexp\n"); m = true; } @@ -13,8 +13,8 @@ function test( name ) { printf("Hello SteveN, you were matched via an expression\n" ); m = true; } - case 3 { - printf("Hello number three, we matched you literally.\n"); + case 3, 6, 9 { + printf("Hello multiple of three, we matched you literally: %d\n", int(name)); m = true; } default { @@ -28,6 +28,7 @@ function test( name ) { // Test the switch statement test( "Steve" ); // Regexp match +test( "steve" ); // Regexp match test( "Steven" ); // Literal match test( 3 ); // Literal match diff --git a/parser/parser.go b/parser/parser.go index 6a1cf38..461e0a6 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -445,7 +445,18 @@ func (p *Parser) parseSwitchStatement() ast.Expression { } else { // parse the match-expression. - tmp.Expr = p.parseExpression(LOWEST) + tmp.Expr = append(tmp.Expr, p.parseExpression(LOWEST)) + for p.peekTokenIs(token.COMMA) { + + // skip the comma + p.nextToken() + + // setup the expression. + p.nextToken() + + tmp.Expr = append(tmp.Expr, p.parseExpression(LOWEST)) + + } } }