Repository: skx/monkey (go)

Issue:
Implement switch-statements
I want to allow this to succeed:

```

function test( name ) {
  switch( name ) {
    case /^steve$/i {
       printf("I'm a regexp\n");
    }
    case "Steven" {
	printf("I know you!\n" );
    }
    default {
	printf("I don't know who you are\n" );
    }
  }
}

test( "Steve" );    // Regexp match
test( "Steven" );   // Literal match
test( "Bob" );      // Unhandled case

```

**NOTE**: I explicitly use blocks here, because fall-throughs are evil :)

Pull request:
70 switch
Once complete this pull-request will implement support for `switch` statements, which will close #70.

Our switch statement will allow an arbitrary number of `case` expressions, allowing different blocks of code to be executed depending upon the match.

We allow one `default` branch in case nothing matches, but this is optional (it is an error to have multiple `default` cases).

Note that we avoid any confusion with C due to using blocks explicitly, there can be no "fall-thru".

Patch:
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/ast/ast.go b/ast/ast.go
index 7bd2270..f3f9f39 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -785,3 +785,77 @@ 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 ")
+
+		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()
+}
+
+// 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()
+}
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index cf243f8..86845dd 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,64 @@ 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
+		}
+
+		// Look at any expression we've got in this case.
+		for _, val := range opt.Expr {
+
+			// Get the value of the case
+			out := Eval(val, env)
+
+			// 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
+
+				}
+			}
+		}
+	}
+
+	// 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 {
diff --git a/examples/switch.mon b/examples/switch.mon
new file mode 100644
index 0000000..74a706f
--- /dev/null
+++ b/examples/switch.mon
@@ -0,0 +1,44 @@
+// A simple test-function for switch-statements.
+function test( name ) {
+
+  // Did we match?
+  m = false;
+
+  switch( name ) {
+    case /^steve$/ , /^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, 6, 9 {
+        printf("Hello multiple of three, we matched you literally: %d\n", int(name));
+        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( "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
diff --git a/parser/parser.go b/parser/parser.go
index b1d74a2..461e0a6 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,121 @@ 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}
+
+	// look for (xx)
+	if !p.expectPeek(token.LPAREN) {
+		return nil
+	}
+	p.nextToken()
+	expression.Value = p.parseExpression(LOWEST)
+	if expression.Value == nil {
+		return nil
+	}
+	if !p.expectPeek(token.RPAREN) {
+		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) {
+
+		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
+		if p.curTokenIs(token.DEFAULT) {
+
+			// We have a default-case here.
+			tmp.Default = true
+
+		} else if p.curTokenIs(token.CASE) {
+
+			// skip "case"
+			p.nextToken()
+
+			// 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 = 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))
+
+				}
+			}
+		}
+
+		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
+	}
+
+	// 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
+
+}
+
 // parseBoolean parses a boolean token.
 func (p *Parser) parseBoolean() ast.Expression {
 	return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)}
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,
 }
 


Test patch:
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])
+	}
+}