Repository: ysugimoto/falco
Pull request #238 (2023-12-30 22:52:21)

Add support for switch statements
While not documented on the Fastly developer portal the Fastly VCL variant supports switch statements.

Simple example:
```vcl
switch (req.http.foo) {
case "bar":
  log "matched bar";
  break;
case "baz":
  log "matched baz";
  fallthrough;
default:
  log "default";
  break;
}
```

Experimentation shows the following grammar ([WSN](https://en.wikipedia.org/wiki/Wirth_syntax_notation)) for the switch statement:
```flex
SwitchStmt = "switch" "(" Control ")" "{" CaseClause { CaseClause } "}" .

// Control value cast to STRING before comparison with case tests.
// User defined function calls must return STRING.
// Built-in function calls must return a value type.
Control = valueTypeIdent | functionCall | stringLiteral | boolLiteral .

CaseClause = SwitchCase ":" StatementList ( "break" | FallThroughStmt ) ";" .

// Case tests must be unique
SwitchCase = "case" Test | Default .

// There can be at most one default case and it can appear anywhere in the case list.
Default = "default" .

StatementList = { statement ";" } .

// Continue execution in the next case clause. May not be used in the final case.
FallThroughStmt = "fallthrough" .

// Tests are either string literals or regular expressions.
Test = stringLiteral | RegexMatch .
RegexMatch = "~" stringLiteral . 

valueTypeIdent = /* builtin or user defined value type variable */ . 
functionCall = /* builtin or user defined function call expression */ .
stringLiteral = /* VCL string literal */ .
boolLiteral = /* VCL bool literal */ .
statement = /* Any VCL statement that is valid in a block statement body. */ .
```

Fiddle showing various aspects of how switch statements behave: https://fiddle.fastly.dev/fiddle/0226bcbb

Since switch statements are valid Fastly VCL, Falco should handle them correctly. This PR adds support for parsing, interpreting, and linting switches.

The linting rules are very basic to keep the scope of the PR reasonable.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 0d35ead..fd38236 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,55 +1,28 @@
 ---
 name: Bug report
 about: Create a report to help us improve
-title: "[BUG]"
-labels: 'bug'
+title: "[BUG] Parser/Linter/Simulator/Testing error"
+labels: ''
 assignees: ysugimoto
 
 ---
 
-Please fill all **required** sections
-
-### Kind of proposals
-
-**required**
-You can turn on multiple checkboxes.
-
-- []Parser
-- []Linter
-- []Simulator
-- []Testing
-
-### Describe the problem
-
-**required**
+**Describe the problem**
 A clear and concise description of what the bug is.
-Please write details to grab the problem 
-
-### VCL code that cause the problem / reproduceable
-
-**required**
-If applicable, put your VCL code to reproduce on our environment.
-At least simplified case even If you cannot show your VCL,
 
+**VCL code that cause the problem / reproduceable**
+If applicable, put your VCL code to reproduce on our environment
 Putting link to [Fastly Fiddle](https://fiddle.fastly.dev/) would be very nice to compare with Fastly actual behavior.
 
-### Expected behavior
-
-**required**
-Corresponding to above reproduceable VCL, describe the difference and what happen falco should behave.
-
-### Screenshots
-
-**optional**
+**Screenshots**
 If applicable, add console output screenshots to help explain your problem.
 
-### Desktop (please complete the following information)
+**Expected behavior**
+A clear and concise description of what you expected to happen.
 
-**required**
+**Desktop (please complete the following information):**
  - OS: [e.g. iOS]
  - Shell [e.g. bash, zsh]
 
-### Additional context
-
-**optional**
-Add any other context (links, documentations, etc) about the problem here.
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 68d394b..70164a3 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,28 +1,17 @@
 ---
 name: Feature request
-about: Request a feature for improving this project
+about: Suggest an idea for this project
 title: "[Feature]"
-labels: 'feature request'
+labels: ''
 assignees: ysugimoto
 
 ---
 
-### Kind of proposals
-
-You can turn on multiple checkboxes.
-
-- []Parser
-- []Linter
-- []Simulator
-- []Testing
-
-### Describe the feature
-
-**required**
+**Describe the feature you'd like**
 Please write solve problem when the feature is added
 
-_Put example VCL after the feature has been implemented_
+**Put example VCL after the feature has been implemented**
 
 
-### Give us your considerations
+**Additional context**
 Write some conditions, related links you've considered
diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md
deleted file mode 100644
index 1680d03..0000000
--- a/.github/ISSUE_TEMPLATE/proposal.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: Proposal
-about: Suggest an idea for this project
-title: "[Proposal]"
-labels: 'proposal'
-assignees: ysugimoto
-
----
-
-Thanks for considering proposal for this project! We welcome your proposals always.
-
-### Kind of proposals
-
-You can turn on multiple checkboxes.
-
-- []Parser
-- []Linter
-- []Simulator
-- []Testing
-
-### Describe your proposal
-
-**required**
-Please write your proposal to improve this project.
-Putting VCL example would be nice to discuss about it.
-
-### Additional context
-Put some related links that are useful to understand. 
diff --git a/ast/break_statement.go b/ast/break_statement.go
new file mode 100644
index 0000000..9c54d93
--- /dev/null
+++ b/ast/break_statement.go
@@ -0,0 +1,22 @@
+package ast
+
+import (
+	"bytes"
+)
+
+type BreakStatement struct {
+	*Meta
+}
+
+func (r *BreakStatement) statement()     {}
+func (r *BreakStatement) GetMeta() *Meta { return r.Meta }
+func (r *BreakStatement) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(r.LeadingComment())
+	buf.WriteString(indent(r.Nest) + "break;")
+	buf.WriteString(r.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/ast/case_statement.go b/ast/case_statement.go
new file mode 100644
index 0000000..ca74503
--- /dev/null
+++ b/ast/case_statement.go
@@ -0,0 +1,37 @@
+package ast
+
+import (
+	"bytes"
+)
+
+type CaseStatement struct {
+	*Meta
+	Test        *InfixExpression
+	Statements  []Statement
+	Fallthrough bool
+}
+
+func (s *CaseStatement) statement()     {}
+func (s *CaseStatement) GetMeta() *Meta { return s.Meta }
+func (s *CaseStatement) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(s.LeadingComment())
+	if s.Test != nil {
+		buf.WriteString("case ")
+		if s.Test.Operator == "~" {
+			buf.WriteString("~")
+		}
+		buf.WriteString(s.Test.Right.String())
+		buf.WriteString(":")
+	} else {
+		buf.WriteString("default:")
+	}
+	buf.WriteString(s.TrailingComment())
+	buf.WriteString("\n")
+	for _, stmt := range s.Statements {
+		buf.WriteString(indent(s.Nest) + stmt.String())
+	}
+
+	return buf.String()
+}
diff --git a/ast/fallthrough_statement.go b/ast/fallthrough_statement.go
new file mode 100644
index 0000000..9133f7e
--- /dev/null
+++ b/ast/fallthrough_statement.go
@@ -0,0 +1,22 @@
+package ast
+
+import (
+	"bytes"
+)
+
+type FallthroughStatement struct {
+	*Meta
+}
+
+func (r *FallthroughStatement) statement()     {}
+func (r *FallthroughStatement) GetMeta() *Meta { return r.Meta }
+func (r *FallthroughStatement) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(r.LeadingComment())
+	buf.WriteString(indent(r.Nest) + "fallthrough;")
+	buf.WriteString(r.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/ast/switch_statement.go b/ast/switch_statement.go
new file mode 100644
index 0000000..4bf19f5
--- /dev/null
+++ b/ast/switch_statement.go
@@ -0,0 +1,30 @@
+package ast
+
+import "bytes"
+
+type SwitchStatement struct {
+	*Meta
+	Control Expression
+	Cases   []*CaseStatement
+	Default int
+}
+
+func (s *SwitchStatement) statement()     {}
+func (s *SwitchStatement) GetMeta() *Meta { return s.Meta }
+func (s *SwitchStatement) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(s.LeadingComment())
+	buf.WriteString("switch (")
+	buf.WriteString(s.Control.String())
+	buf.WriteString(") {\n")
+	for _, stmt := range s.Cases {
+		buf.WriteString(stmt.String())
+	}
+
+	buf.WriteString("}")
+	buf.WriteString(s.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/interpreter/statement.go b/interpreter/statement.go
index 8c18718..87040c7 100644
--- a/interpreter/statement.go
+++ b/interpreter/statement.go
@@ -12,8 +12,10 @@ import (
 	"github.com/ysugimoto/falco/interpreter/function"
 	fe "github.com/ysugimoto/falco/interpreter/function/errors"
 	"github.com/ysugimoto/falco/interpreter/limitations"
+	"github.com/ysugimoto/falco/interpreter/operator"
 	"github.com/ysugimoto/falco/interpreter/process"
 	"github.com/ysugimoto/falco/interpreter/value"
+	"github.com/ysugimoto/falco/types"
 )
 
 // nolint: gocognit
@@ -90,6 +92,13 @@ func (i *Interpreter) ProcessBlockStatement(statements []ast.Statement, ds Debug
 				return state, DebugPass, nil
 			}
 
+		case *ast.SwitchStatement:
+			var state State
+			state, err = i.ProcessSwitchStatement(t, debugState)
+			if state != NONE {
+				return state, DebugPass, nil
+			}
+
 		case *ast.RestartStatement:
 			if !i.ctx.Scope.Is(context.RecvScope, context.HitScope, context.FetchScope, context.ErrorScope, context.DeliverScope) {
 				return NONE, DebugPass, exception.Runtime(
@@ -494,3 +503,101 @@ func (i *Interpreter) ProcessIfStatement(stmt *ast.IfStatement, ds DebugState) (
 	}
 	return NONE, nil
 }
+
+func (i *Interpreter) ProcessSwitchStatement(stmt *ast.SwitchStatement, ds DebugState) (State, error) {
+	// User defined functions used in a switch control statement must have a STRING return type.
+	if fnCall, ok := stmt.Control.(*ast.FunctionCallExpression); ok {
+		fn, ok := i.ctx.SubroutineFunctions[fnCall.Function.Value]
+		if ok && fn.ReturnType.Value != string(value.StringType) {
+			return NONE, errors.WithStack(exception.Runtime(
+				&fnCall.Token,
+				"user defined function has invalid return type (%s) for switch, must be STRING",
+				fn.ReturnType.Value,
+			))
+		}
+	}
+	expr, err := i.ProcessExpression(stmt.Control, false)
+	if err != nil {
+		return NONE, errors.WithStack(err)
+	}
+	// Control expression must evaluate to a value type
+	if _, ok := types.ValueTypeMap[string(expr.Type())]; !ok {
+		return NONE, errors.WithStack(exception.Runtime(
+			&stmt.GetMeta().Token,
+			"switch has invalid control type %s",
+			expr.Type(),
+		))
+	}
+	control := &value.String{Value: expr.String()}
+	for n := range stmt.Cases {
+		if n == stmt.Default {
+			continue
+		}
+		state, matched, err := i.ProcessCaseStatement(stmt, n, control, false, ds)
+		if err != nil {
+			return NONE, errors.WithStack(err)
+		}
+		if matched {
+			return state, nil
+		}
+	}
+	// No cases matched, check if switch has a default case.
+	if stmt.Default != -1 {
+		state, _, err := i.ProcessCaseStatement(stmt, stmt.Default, control, false, ds)
+		if err != nil {
+			return NONE, errors.WithStack(err)
+		}
+		return state, nil
+	}
+
+	return NONE, nil
+}
+
+func (i *Interpreter) ProcessCaseStatement(
+	stmt *ast.SwitchStatement,
+	offset int,
+	control value.Value,
+	isFallthrough bool,
+	ds DebugState,
+) (State, bool, error) {
+
+	var matched bool
+	// If the case offset being processed is the default case or if a fallthrough
+	// is being handled skip processing of case test expression.
+	if stmt.Default == offset || isFallthrough {
+		matched = true
+	} else {
+		right, err := i.ProcessExpression(stmt.Cases[offset].Test.Right, false)
+		if err != nil {
+			return NONE, false, err
+		}
+		var match value.Value
+		if stmt.Cases[offset].Test.Operator == "~" {
+			match, err = operator.Regex(i.ctx, control, right)
+		} else {
+			match, err = operator.Equal(control, right)
+		}
+		if err != nil {
+			return NONE, false, errors.WithStack(err)
+		}
+		matched = value.Unwrap[*value.Boolean](match).Value
+	}
+
+	if matched {
+		state, _, err := i.ProcessBlockStatement(stmt.Cases[offset].Statements, ds)
+		if err != nil {
+			return NONE, false, errors.WithStack(err)
+		}
+		if state == NONE && stmt.Cases[offset].Fallthrough {
+			// This shouldn't be possible as it would be a syntax error from the
+			// parser to have a fallthrough on the last case of a switch.
+			// But better to give the user an error than a panic.
+			if offset+1 >= len(stmt.Cases) {
+				return NONE, false, exception.Runtime(&stmt.Token, "Fallthrough not allowed in final case")
+			}
+			return i.ProcessCaseStatement(stmt, offset+1, control, true, ds)
+		}
+		return state, true, nil
+	}
+	return NONE, false, nil
+}
diff --git a/interpreter/subroutine.go b/interpreter/subroutine.go
index 9dd70a5..068a3a6 100644
--- a/interpreter/subroutine.go
+++ b/interpreter/subroutine.go
@@ -118,6 +118,12 @@ func (i *Interpreter) ProcessFunctionSubroutine(sub *ast.SubroutineDeclaration,
 			if state != NONE {
 				return value.Null, state, nil
 			}
+		case *ast.SwitchStatement:
+			var state State
+			state, err = i.ProcessSwitchStatement(t, debugState)
+			if state != NONE {
+				return value.Null, state, nil
+			}
 		case *ast.RestartStatement:
 			// restart statement force change state to RESTART
 			return value.Null, RESTART, nil
diff --git a/interpreter/value/value.go b/interpreter/value/value.go
index 78fb38e..c27ff8e 100644
--- a/interpreter/value/value.go
+++ b/interpreter/value/value.go
@@ -71,10 +71,9 @@ type String struct {
 }
 
 func (v *String) String() string {
-	// Temporarily comment out to suppress not set output
-	// if v.IsNotSet {
-	// 	return "(null)"
-	// }
+	if v.IsNotSet {
+		return "[not set]"
+	}
 	return v.Value
 }
 func (v *String) Type() Type      { return StringType }
@@ -94,9 +93,8 @@ type IP struct {
 }
 
 func (v *IP) String() string {
-	// Temporarily return empty string if notset
 	if v.IsNotSet {
-		return ""
+		return "[not set]"
 	}
 	return v.Value.String()
 }
diff --git a/lexer/lexer.go b/lexer/lexer.go
index 49c0f95..b6f41f9 100644
--- a/lexer/lexer.go
+++ b/lexer/lexer.go
@@ -292,6 +292,15 @@ func (l *Lexer) NextToken() token.Token {
 		case l.isLetter(l.char):
 			literal := l.readIdentifier()
 
+			// Switch's default case keyword needs special handling due to the header
+			// field access syntax.
+			if literal == "default" {
+				t = newToken(token.DEFAULT, l.char, line, index)
+				t.Literal = literal
+				t.File = l.file
+				return t
+			}
+
 			// Read more neighbor digit, dot, hyphen and colon character
 			// in order to lex digit contained identifier like "version4", "req.http.Cookie:session" string
 			for l.char == '-' || l.char == '.' || l.char == ':' || isDigit(l.char) {
@@ -353,8 +362,8 @@ func (l *Lexer) NextToken() token.Token {
 				t = newToken(token.RTIME, l.char, line, index)
 				t.Literal = num + string(l.char)
 			case '%':
-				// Also "%" is special character which indicates persentage.
-				// Usually it will be used on director.quorum field value and it's okey to treat as string token.
+				// Also "%" is special character which indicates percentage.
+				// Usually it will be used on director.quorum field value and it's okay to treat as string token.
 				t = newToken(token.STRING, l.char, line, index)
 				t.Literal = num + "%"
 			default:
diff --git a/linter/linter.go b/linter/linter.go
index 627375d..8992eb0 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -207,6 +207,8 @@ func (l *Linter) lint(node ast.Node, ctx *context.Context) types.Type {
 		return l.lintRemoveStatement(t, ctx)
 	case *ast.IfStatement:
 		return l.lintIfStatement(t, ctx)
+	case *ast.SwitchStatement:
+		return l.lintSwitchStatement(t, ctx)
 	case *ast.RestartStatement:
 		return l.lintRestartStatement(t, ctx)
 	case *ast.EsiStatement:
@@ -1165,6 +1167,41 @@ func (l *Linter) lintIfCondition(cond ast.Expression, ctx *context.Context) {
 	}
 }
 
+func (l *Linter) lintSwitchStatement(stmt *ast.SwitchStatement, ctx *context.Context) types.Type {
+	if c, ok := stmt.Control.(*ast.FunctionCallExpression); ok {
+		fn, err := ctx.GetFunction(c.Function.Value)
+		if err != nil {
+			l.Error(&LintError{
+				Severity: ERROR,
+				Token:    c.Function.Token,
+				Message:  err.Error(),
+			})
+		} else if fn.IsUserDefinedFunction && !expectType(fn.Return, types.StringType) {
+			// Fastly VCL only permits user defined functions that return STRING in a
+			// switch control. Built-in function return values will be coerced into a
+			// STRING.
+			l.Error(&LintError{
+				Severity: ERROR,
+				Token:    c.Token,
+				Message:  fmt.Sprintf("Switch condition function return type must be STRING %s returns %s", c.Function.Value, fn.Return.String()),
+			})
+		}
+	}
+
+	for _, c := range stmt.Cases {
+		for _, s := range c.Statements {
+			switch s.(type) {
+			case *ast.BreakStatement, *ast.FallthroughStatement:
+				break // parser already made sure break/fallthrough is at the end.
+			default:
+				l.lint(s, ctx)
+			}
+		}
+	}
+
+	return types.NeverType
+}
+
 func (l *Linter) lintRestartStatement(stmt *ast.RestartStatement, ctx *context.Context) types.Type {
 	// restart statement enables in RECV, HIT, FETCH, ERROR and DELIVER scope
 	if ctx.Mode()&(context.RECV|context.HIT|context.FETCH|context.ERROR|context.DELIVER) == 0 {
diff --git a/parser/error.go b/parser/error.go
index d2846fe..41cb355 100644
--- a/parser/error.go
+++ b/parser/error.go
@@ -35,6 +35,13 @@ func MissingSemicolon(m *ast.Meta) *ParseError {
 	}
 }
 
+func MissingColon(m *ast.Meta) *ParseError {
+	return &ParseError{
+		Token:   m.Token,
+		Message: "Missing colon",
+	}
+}
+
 func UnexpectedToken(m *ast.Meta, expects ...string) *ParseError {
 	message := fmt.Sprintf(`Unexpected token "%s"`, m.Token.Literal)
 	if len(expects) > 0 {
@@ -59,3 +66,31 @@ func TypeConversionError(m *ast.Meta, tt string) *ParseError {
 		Message: fmt.Sprintf("Failed type conversion for token %s to %s ", m.Token.Literal, tt),
 	}
 }
+
+func DuplicateCase(m *ast.Meta) *ParseError {
+	return &ParseError{
+		Token:   m.Token,
+		Message: "Duplicate case label: " + m.Token.Literal,
+	}
+}
+
+func MultipleDefaults(m *ast.Meta) *ParseError {
+	return &ParseError{
+		Token:   m.Token,
+		Message: "Multiple default cases",
+	}
+}
+
+func FinalFallthrough(m *ast.Meta) *ParseError {
+	return &ParseError{
+		Token:   m.Token,
+		Message: "Final case cannot have fallthrough",
+	}
+}
+
+func EmptySwitch(m *ast.Meta) *ParseError {
+	return &ParseError{
+		Token:   m.Token,
+		Message: "Switch must have at least one case",
+	}
+}
diff --git a/parser/parser.go b/parser/parser.go
index c393fec..cf733d9 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -132,6 +132,10 @@ func (p *Parser) trailing() ast.Comments {
 	return cs
 }
 
+func (p *Parser) prevTokenIs(t token.TokenType) bool {
+	return p.prevToken.Token.Type == t
+}
+
 func (p *Parser) curTokenIs(t token.TokenType) bool {
 	return p.curToken.Token.Type == t
 }
diff --git a/parser/statement_parser.go b/parser/statement_parser.go
index c50ed33..d754289 100644
--- a/parser/statement_parser.go
+++ b/parser/statement_parser.go
@@ -6,6 +6,78 @@ import (
 	"github.com/ysugimoto/falco/token"
 )
 
+func (p *Parser) parseStatement() (ast.Statement, error) {
+	var stmt ast.Statement
+	var err error
+
+	p.nextToken() // point to statement
+	switch p.curToken.Token.Type {
+	// https://github.com/ysugimoto/falco/issues/17
+	// VCL accepts block syntax:
+	// ```
+	// sub vcl_recv {
+	//   {
+	//      log "recv";
+	//   }
+	// }
+	// ```
+	case token.LEFT_BRACE:
+		stmt, err = p.parseBlockStatement()
+	case token.SET:
+		stmt, err = p.parseSetStatement()
+	case token.UNSET:
+		stmt, err = p.parseUnsetStatement()
+	case token.REMOVE:
+		stmt, err = p.parseRemoveStatement()
+	case token.ADD:
+		stmt, err = p.parseAddStatement()
+	case token.CALL:
+		stmt, err = p.parseCallStatement()
+	case token.DECLARE:
+		stmt, err = p.parseDeclareStatement()
+	case token.ERROR:
+		stmt, err = p.parseErrorStatement()
+	case token.ESI:
+		stmt, err = p.parseEsiStatement()
+	case token.LOG:
+		stmt, err = p.parseLogStatement()
+	case token.RESTART:
+		stmt, err = p.parseRestartStatement()
+	case token.RETURN:
+		stmt, err = p.parseReturnStatement()
+	case token.SYNTHETIC:
+		stmt, err = p.parseSyntheticStatement()
+	case token.SYNTHETIC_BASE64:
+		stmt, err = p.parseSyntheticBase64Statement()
+	case token.IF:
+		stmt, err = p.parseIfStatement()
+	case token.SWITCH:
+		stmt, err = p.parseSwitchStatement()
+	case token.GOTO:
+		stmt, err = p.parseGotoStatement()
+	case token.INCLUDE:
+		stmt, err = p.parseIncludeStatement()
+	case token.BREAK:
+		stmt, err = p.parseBreakStatement()
+	case token.FALLTHROUGH:
+		stmt, err = p.parseFallthroughStatement()
+	case token.IDENT:
+		// Check if the current ident is a function call
+		if p.peekTokenIs(token.LEFT_PAREN) {
+			stmt, err = p.parseFunctionCall()
+		} else {
+			// Could be a goto destination
+			stmt, err = p.parseGotoDestination()
+		}
+	default:
+		err = UnexpectedToken(p.curToken)
+	}
+	if err != nil {
+		return nil, errors.WithStack(err)
+	}
+	return stmt, nil
+}
+
 func (p *Parser) parseImportStatement() (*ast.ImportStatement, error) {
 	i := &ast.ImportStatement{
 		Meta: p.curToken,
@@ -45,80 +117,24 @@ func (p *Parser) parseIncludeStatement() (ast.Statement, error) {
 	return i, nil
 }
 
-// nolint: dupl
 func (p *Parser) parseBlockStatement() (*ast.BlockStatement, error) {
 	// Note: block statement is used for declaration/statement inside like subroutine, if, elseif, else
 	// on start this statement, current token must point start of LEFT_BRACE
-	// and after on end this statement, current token must poinrt end of RIGHT_BRACE
+	// and after on end this statement, current token must point end of RIGHT_BRACE
 	b := &ast.BlockStatement{
 		Meta:       p.curToken,
 		Statements: []ast.Statement{},
 	}
 
 	for !p.peekTokenIs(token.RIGHT_BRACE) {
-		var stmt ast.Statement
-		var err error
-
-		p.nextToken() // point to statement
-		switch p.curToken.Token.Type {
-		// https://github.com/ysugimoto/falco/issues/17
-		// VCL accepts block syntax:
-		// ```
-		// sub vcl_recv {
-		//   {
-		//      log "recv";
-		//   }
-		// }
-		// ```
-		case token.LEFT_BRACE:
-			stmt, err = p.parseBlockStatement()
-		case token.SET:
-			stmt, err = p.parseSetStatement()
-		case token.UNSET:
-			stmt, err = p.parseUnsetStatement()
-		case token.REMOVE:
-			stmt, err = p.parseRemoveStatement()
-		case token.ADD:
-			stmt, err = p.parseAddStatement()
-		case token.CALL:
-			stmt, err = p.parseCallStatement()
-		case token.DECLARE:
-			stmt, err = p.parseDeclareStatement()
-		case token.ERROR:
-			stmt, err = p.parseErrorStatement()
-		case token.ESI:
-			stmt, err = p.parseEsiStatement()
-		case token.LOG:
-			stmt, err = p.parseLogStatement()
-		case token.RESTART:
-			stmt, err = p.parseRestartStatement()
-		case token.RETURN:
-			stmt, err = p.parseReturnStatement()
-		case token.SYNTHETIC:
-			stmt, err = p.parseSyntheticStatement()
-		case token.SYNTHETIC_BASE64:
-			stmt, err = p.parseSyntheticBase64Statement()
-		case token.IF:
-			stmt, err = p.parseIfStatement()
-		case token.GOTO:
-			stmt, err = p.parseGotoStatement()
-		case token.INCLUDE:
-			stmt, err = p.parseIncludeStatement()
-		case token.IDENT:
-			// Check if the current ident is a function call
-			if p.peekTokenIs(token.LEFT_PAREN) {
-				stmt, err = p.parseFunctionCall()
-			} else {
-				// Could be a goto destination
-				stmt, err = p.parseGotoDestination()
-			}
-		default:
-			err = UnexpectedToken(p.peekToken)
-		}
-
+		stmt, err := p.parseStatement()
 		if err != nil {
 			return nil, errors.WithStack(err)
 		}
+		switch stmt.(type) {
+		case *ast.BreakStatement, *ast.FallthroughStatement:
+			return nil, UnexpectedToken(stmt.GetMeta())
+		}
 		b.Statements = append(b.Statements, stmt)
 	}
 
@@ -131,7 +147,6 @@ func (p *Parser) parseBlockStatement() (*ast.BlockStatement, error) {
 	return b, nil
 }
 
-// nolint: dupl
 func (p *Parser) parseSetStatement() (*ast.SetStatement, error) {
 	stmt := &ast.SetStatement{
 		Meta: p.curToken,
@@ -207,7 +222,6 @@ func (p *Parser) parseRemoveStatement() (*ast.RemoveStatement, error) {
 	return stmt, nil
 }
 
-// nolint: dupl
 func (p *Parser) parseAddStatement() (*ast.AddStatement, error) {
 	stmt := &ast.AddStatement{
 		Meta: p.curToken,
@@ -493,7 +507,6 @@ func (p *Parser) parseSyntheticBase64Statement() (*ast.SyntheticBase64Statement,
 	return stmt, nil
 }
 
-// nolint: gocognit
 func (p *Parser) parseIfStatement() (*ast.IfStatement, error) {
 	stmt := &ast.IfStatement{
 		Meta: p.curToken,
@@ -603,6 +616,193 @@ func (p *Parser) parseAnotherIfStatement() (*ast.IfStatement, error) {
 	return stmt, nil
 }
 
+func (p *Parser) parseSwitchStatement() (*ast.SwitchStatement, error) {
+	stmt := &ast.SwitchStatement{
+		Meta:    p.curToken,
+		Default: -1, // -1 is used to represent a switch without a default case.
+	}
+
+	if !p.expectPeek(token.LEFT_PAREN) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_PAREN"))
+	}
+
+	p.nextToken() // point at control expression
+
+	// Switch control expression can be a literal, variable identifier, or
+	// function call.
+	var control ast.Expression
+	var err error
+	if p.peekTokenIs(token.LEFT_PAREN) {
+		i := p.parseIdent()
+		p.nextToken()
+		control, err = p.parseFunctionCallExpression(i)
+		if err != nil {
+			return nil, errors.WithStack(err)
+		}
+	} else {
+		if p.curTokenIs(token.IDENT) {
+			control = p.parseIdent()
+		} else {
+			// Only string and bool literals can be used as a switch control
+			// expression.
+			if !p.curTokenIs(token.TRUE) && !p.curTokenIs(token.FALSE) && !p.curTokenIs(token.STRING) {
+				return nil, UnexpectedToken(
+					p.curToken,
+					"invalid literal %s for switch control, expect BOOL or STRING",
+					string(p.curToken.Token.Type))
+			}
+			control, err = p.parseExpression(LOWEST)
+			if err != nil {
+				return nil, errors.WithStack(err)
+			}
+		}
+	}
+	stmt.Control = control
+
+	if !p.expectPeek(token.RIGHT_PAREN) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "RIGHT_PAREN"))
+	}
+
+	if !p.expectPeek(token.LEFT_BRACE) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_BRACE"))
+	}
+
+	// Parse case clauses
+	for !p.peekTokenIs(token.RIGHT_BRACE) {
+		t := p.peekToken
+		clause, err := p.parseCaseStatement()
+		if err != nil {
+			return nil, errors.WithStack(err)
+		}
+
+		if clause.Test == nil {
+			// There cannot be multiple default clauses
+			if stmt.Default != -1 {
+				return nil, errors.WithStack(MultipleDefaults(t))
+			}
+			stmt.Default = len(stmt.Cases)
+		}
+
+		// Case tests must be unique
+		for _, o := range stmt.Cases {
+			if clause.Test == nil || o.Test == nil || clause.Test.Operator != o.Test.Operator {
+				continue
+			}
+			if clause.Test.Right.String() == o.Test.Right.String() {
+				return nil, errors.WithStack(DuplicateCase(clause.Test.Meta))
+			}
+		}
+		stmt.Cases = append(stmt.Cases, clause)
+	}
+
+	// There must be at least one case
+	if len(stmt.Cases) == 0 {
+		return nil, errors.WithStack(EmptySwitch(p.peekToken))
+	}
+
+	// Final case can't be a fallthrough case.
+	lc := stmt.Cases[len(stmt.Cases)-1]
+	ls := lc.Statements[len(lc.Statements)-1]
+	if _, ok := ls.(*ast.FallthroughStatement); ok {
+		return nil, errors.WithStack(FinalFallthrough(ls.GetMeta()))
+	}
+
+	p.nextToken()
+	swapLeadingTrailing(p.peekToken, stmt.Meta)
+	return stmt, nil
+}
+
+func (p *Parser) parseBreakStatement() (*ast.BreakStatement, error) {
+	stmt := &ast.BreakStatement{
+		Meta: p.curToken,
+	}
+
+	if !p.peekTokenIs(token.SEMICOLON) {
+		return nil, errors.WithStack(MissingSemicolon(p.curToken))
+	}
+	stmt.Meta.Trailing = p.trailing()
+	p.nextToken() // point to SEMICOLON
+
+	return stmt, nil
+}
+
+func (p *Parser) parseFallthroughStatement() (*ast.FallthroughStatement, error) {
+	stmt := &ast.FallthroughStatement{
+		Meta: p.curToken,
+	}
+
+	if !p.peekTokenIs(token.SEMICOLON) {
+		return nil, errors.WithStack(MissingSemicolon(p.curToken))
+	}
+	stmt.Meta.Trailing = p.trailing()
+	p.nextToken() // point to SEMICOLON
+
+	return stmt, nil
+}
+
+func (p *Parser) parseCaseStatement() (*ast.CaseStatement, error) {
+	p.nextToken()
+	stmt := &ast.CaseStatement{
+		Meta:       p.curToken,
+		Statements: []ast.Statement{},
+	}
+	caseLeading := p.curToken.Leading
+	switch p.curToken.Token.Type {
+	case token.CASE:
+		p.nextToken() // match expression
+
+		matchExp := &ast.InfixExpression{
+			Meta: p.curToken,
+		}
+		switch p.curToken.Token.Type {
+		case token.STRING:
+			exp, err := p.parseExpression(LOWEST)
+			if err != nil {
+				return nil, errors.WithStack(err)
+			}
+			matchExp.Operator = "=="
+			matchExp.Right = exp
+		case token.REGEX_MATCH:
+			exp, err := p.parsePrefixExpression()
+			if err != nil {
+				return nil, errors.WithStack(err)
+			}
+			matchExp.Operator = "~"
+			matchExp.Right = exp.Right
+		default:
+			return nil, UnexpectedToken(p.curToken, "string", "~")
+		}
+		stmt.Test = matchExp
+	case token.DEFAULT:
+		// nothing to do
+	default:
+		return nil, UnexpectedToken(p.curToken, "case", "default")
+	}
+
+	caseTrailing := p.trailing()
+	if !p.expectPeek(token.COLON) {
+		return nil, errors.WithStack(MissingColon(p.curToken))
+	}
+	for !p.peekTokenIs(token.CASE) && !p.peekTokenIs(token.DEFAULT) && !p.peekTokenIs(token.RIGHT_BRACE) {
+		s, err := p.parseStatement()
+		if err != nil {
+			return nil, errors.WithStack(err)
+		}
+		stmt.Statements = append(stmt.Statements, s)
+	}
+
+	if !p.prevTokenIs(token.BREAK) {
+		if !p.prevTokenIs(token.FALLTHROUGH) {
+			return nil, errors.WithStack(UnexpectedToken(p.prevToken, "break", "fallthrough"))
+		}
+		stmt.Fallthrough = true
+	}
+
+	stmt.Meta.Leading = caseLeading
+	stmt.Meta.Trailing = caseTrailing
+	return stmt, nil
+}
+
 func (p *Parser) parseGotoStatement() (*ast.GotoStatement, error) {
 	stmt := &ast.GotoStatement{
 		Meta: p.curToken,
diff --git a/plugin/io.go b/plugin/io.go
index 4a81603..13c2959 100644
--- a/plugin/io.go
+++ b/plugin/io.go
@@ -36,6 +36,10 @@ func init() {
 	gob.Register(&ast.Ident{})
 	gob.Register(&ast.IfExpression{})
 	gob.Register(&ast.IfStatement{})
+	gob.Register(&ast.SwitchStatement{})
+	gob.Register(&ast.CaseStatement{})
+	gob.Register(&ast.BreakStatement{})
+	gob.Register(&ast.FallthroughStatement{})
 	gob.Register(&ast.ImportStatement{})
 	gob.Register(&ast.IncludeStatement{})
 	gob.Register(&ast.InfixExpression{})
diff --git a/token/token.go b/token/token.go
index 562c5cb..227c6af 100644
--- a/token/token.go
+++ b/token/token.go
@@ -119,6 +119,11 @@ const (
 	PENALTYBOX       = "PENALTYBOX"       // penaltybox
 	RATECOUNTER      = "RATECOUNTER"      // ratecounter
 	GOTO             = "GOTO"             // goto
+	SWITCH           = "SWITCH"           // switch
+	CASE             = "CASE"             // case
+	DEFAULT          = "DEFAULT"          // default
+	BREAK            = "BREAK"            // break
+	FALLTHROUGH      = "FALLTHROUGH"      // fallthrough
 )
 
 var keywords = map[string]TokenType{
@@ -152,6 +157,11 @@ var keywords = map[string]TokenType{
 	"penaltybox":       PENALTYBOX,
 	"ratecounter":      RATECOUNTER,
 	"goto":             GOTO,
+	"switch":           SWITCH,
+	"case":             CASE,
+	"default":          DEFAULT,
+	"break":            BREAK,
+	"fallthrough":      FALLTHROUGH,
 }
 
 func LookupIdent(ident string) TokenType {


Test patch:
diff --git a/ast/break_statement_test.go b/ast/break_statement_test.go
new file mode 100644
index 0000000..5559240
--- /dev/null
+++ b/ast/break_statement_test.go
@@ -0,0 +1,19 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestBreakStatement(t *testing.T) {
+	brk := &BreakStatement{
+		Meta: New(T, 0, comments("// This is comment"), comments("// This is comment")),
+	}
+
+	expect := `// This is comment
+break; // This is comment
+`
+
+	if brk.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, brk.String())
+	}
+}
diff --git a/ast/fallthrough_statement_test.go b/ast/fallthrough_statement_test.go
new file mode 100644
index 0000000..eaeb119
--- /dev/null
+++ b/ast/fallthrough_statement_test.go
@@ -0,0 +1,19 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestFallthroughStatement(t *testing.T) {
+	ft := &FallthroughStatement{
+		Meta: New(T, 0, comments("// This is comment"), comments("// This is comment")),
+	}
+
+	expect := `// This is comment
+fallthrough; // This is comment
+`
+
+	if ft.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, ft.String())
+	}
+}
diff --git a/ast/switch_statement_test.go b/ast/switch_statement_test.go
new file mode 100644
index 0000000..8e95812
--- /dev/null
+++ b/ast/switch_statement_test.go
@@ -0,0 +1,118 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestSwitchStatement(t *testing.T) {
+	switchs := &SwitchStatement{
+		Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+		Control: &Ident{
+			Meta:  New(T, 0),
+			Value: "req.http.host",
+		},
+		Cases: []*CaseStatement{
+			{
+				Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+				Test: &InfixExpression{
+					Left:     &Ident{Value: "req.http.Host"},
+					Operator: "==",
+					Right: &String{
+						Meta:  New(T, 0),
+						Value: "1",
+					},
+				},
+				Statements: []Statement{
+					&BreakStatement{
+						Meta: New(T, 1, comments("// This is comment"), comments("/* This is comment */")),
+					},
+				},
+			},
+			{
+				Test: &InfixExpression{
+					Left:     &Ident{Value: "req.http.Host"},
+					Operator: "==",
+					Right: &String{
+						Meta:  New(T, 0),
+						Value: "2",
+					},
+				},
+				Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+				Statements: []Statement{
+					&FallthroughStatement{
+						Meta: New(T, 1, comments("// This is comment"), comments("/* This is comment */")),
+					},
+				},
+			},
+			{
+				Test: &InfixExpression{
+					Left:     &Ident{Value: "req.http.Host"},
+					Operator: "~",
+					Right: &String{
+						Meta:  New(T, 0),
+						Value: "[3-4]",
+					},
+				},
+				Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+				Statements: []Statement{
+					&BreakStatement{
+						Meta: New(T, 1, comments("// This is comment"), comments("/* This is comment */")),
+					},
+				},
+			},
+			{
+				Test: &InfixExpression{
+					Left:     &Ident{Value: "req.http.Host"},
+					Operator: "==",
+					Right: &String{
+						Meta:  New(T, 0),
+						Value: "5",
+					},
+				},
+				Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+				Statements: []Statement{
+					&BreakStatement{
+						Meta: New(T, 1, comments("// This is comment"), comments("/* This is comment */")),
+					},
+				},
+			},
+			{
+				Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+				Statements: []Statement{
+					&BreakStatement{
+						Meta: New(T, 1, comments("// This is comment"), comments("/* This is comment */")),
+					},
+				},
+			},
+		},
+	}
+
+	expect := `// This is comment
+switch (req.http.host) {
+// This is comment
+case "1": /* This is comment */
+  // This is comment
+  break; /* This is comment */
+// This is comment
+case "2": /* This is comment */
+  // This is comment
+  fallthrough; /* This is comment */
+// This is comment
+case ~"[3-4]": /* This is comment */
+  // This is comment
+  break; /* This is comment */
+// This is comment
+case "5": /* This is comment */
+  // This is comment
+  break; /* This is comment */
+// This is comment
+default: /* This is comment */
+  // This is comment
+  break; /* This is comment */
+} /* This is comment */
+`
+
+	if switchs.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, switchs.String())
+	}
+}
diff --git a/examples/testing/default_values.test.vcl b/examples/testing/default_values.test.vcl
index 340d075..347cf7c 100644
--- a/examples/testing/default_values.test.vcl
+++ b/examples/testing/default_values.test.vcl
@@ -84,7 +84,7 @@ sub test_default_local_var_recv {
     set var.is_null = false;
   }
   assert.true(var.is_null);
-  assert.equal(var.string, "");
+  assert.not_equal(var.string, "");
 }
 
 // @scope: recv
diff --git a/interpreter/operator/operator_concat_test.go b/interpreter/operator/operator_concat_test.go
index 8758f80..4d6cd94 100644
--- a/interpreter/operator/operator_concat_test.go
+++ b/interpreter/operator/operator_concat_test.go
@@ -121,7 +121,6 @@ func TestConcatOperator(t *testing.T) {
 			{left: &value.String{Value: "example"}, right: &value.Float{Value: 10.0, Literal: true}, isError: true},
 			{left: &value.String{Value: "example"}, right: &value.String{Value: "example"}, expect: "exampleexample"},
 			{left: &value.String{Value: "example"}, right: &value.String{Value: "example", Literal: true}, expect: "exampleexample"},
-			{left: &value.String{Value: "example"}, right: &value.String{IsNotSet: true}, expect: "example"},
 			{left: &value.String{Value: "example"}, right: &value.RTime{Value: 100 * time.Second}, expect: "example100.000"},
 			{left: &value.String{Value: "example"}, right: &value.RTime{Value: 100 * time.Second, Literal: true}, isError: true},
 			{left: &value.String{Value: "example"}, right: &value.Time{Value: now}, expect: "example" + now.Format(http.TimeFormat)},
@@ -129,7 +128,6 @@ func TestConcatOperator(t *testing.T) {
 			{left: &value.String{Value: "example"}, right: &value.Boolean{Value: true}, expect: "example1"},
 			{left: &value.String{Value: "example"}, right: &value.Boolean{Value: false, Literal: true}, expect: "example0"},
 			{left: &value.String{Value: "example"}, right: &value.IP{Value: net.ParseIP("127.0.0.1")}, expect: "example127.0.0.1"},
-			{left: &value.String{Value: "example"}, right: &value.IP{IsNotSet: true}, expect: "example"},
 			{left: &value.String{Value: "example", Literal: true}, right: &value.Integer{Value: 100}, expect: "example100"},
 			{left: &value.String{Value: "example", Literal: true}, right: &value.Integer{Value: 100, Literal: true}, isError: true},
 		}
@@ -364,7 +362,6 @@ func TestConcatOperator(t *testing.T) {
 			{left: &value.IP{Value: v}, right: &value.Float{Value: 10.0, Literal: true}, isError: true},
 			{left: &value.IP{Value: v}, right: &value.String{Value: "example"}, expect: "127.0.0.1example"},
 			{left: &value.IP{Value: v}, right: &value.String{Value: "example", Literal: true}, expect: "127.0.0.1example"},
-			{left: &value.IP{Value: v}, right: &value.String{IsNotSet: true}, expect: "127.0.0.1"},
 			{left: &value.IP{Value: v}, right: &value.RTime{Value: time.Second}, expect: "127.0.0.11.000"},
 			{left: &value.IP{Value: v}, right: &value.RTime{Value: time.Second, Literal: true}, isError: true},
 			{left: &value.IP{Value: v}, right: &value.Time{Value: now}, expect: "127.0.0.1" + now.Format(http.TimeFormat)},
@@ -372,7 +369,6 @@ func TestConcatOperator(t *testing.T) {
 			{left: &value.IP{Value: v}, right: &value.Boolean{Value: true}, expect: "127.0.0.11"},
 			{left: &value.IP{Value: v}, right: &value.Boolean{Value: false, Literal: true}, expect: "127.0.0.10"},
 			{left: &value.IP{Value: v}, right: &value.IP{Value: net.ParseIP("127.0.0.1")}, expect: "127.0.0.1127.0.0.1"},
-			{left: &value.IP{Value: v}, right: &value.IP{IsNotSet: true}, expect: "127.0.0.1"},
 		}
 
 		for i, tt := range tests {
diff --git a/interpreter/statement_test.go b/interpreter/statement_test.go
index cff9e06..995adb2 100644
--- a/interpreter/statement_test.go
+++ b/interpreter/statement_test.go
@@ -373,3 +373,360 @@ func TestIfStatement(t *testing.T) {
 		})
 	}
 }
+
+func TestSwitchStatement(t *testing.T) {
+	tests := []struct {
+		name       string
+		vcl        string
+		assertions map[string]value.Value
+		isError    bool
+	}{
+		{
+			name: "String control value",
+			vcl: `
+			sub vcl_recv {
+				set req.http.control = "2";
+				switch (req.http.control) {
+				case "1":
+					set req.http.case = "1";
+					break;
+				case "2":
+					set req.http.case = "2";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "2"},
+			},
+			isError: false,
+		},
+		{
+			name: "Integer control value",
+			vcl: `
+			sub vcl_recv {
+				declare local var.control INTEGER;
+				set var.control = 2;
+				switch (var.control) {
+				case "1":
+					set req.http.case = "1";
+					break;
+				case "2":
+					set req.http.case = "2";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "2"},
+			},
+			isError: false,
+		},
+		{
+			name: "Bool control value",
+			vcl: `
+			sub vcl_recv {
+				declare local var.control BOOL;
+				set var.control = true;
+				switch (var.control) {
+				case "0":
+					set req.http.case = "0";
+					break;
+				case "1":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "Backend control value",
+			vcl: `
+			sub vcl_recv {
+				switch (req.backend) {
+				case "test":
+					set req.http.case = "0";
+					break;
+				case "example":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "Float control value",
+			vcl: `
+			sub vcl_recv {
+				declare local var.control FLOAT;
+				set var.control = 1.03;
+				switch (var.control) {
+				case "0.000":
+					set req.http.case = "0";
+					break;
+				case "1.030":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "IP control value",
+			vcl: `
+			sub vcl_recv {
+				declare local var.control IP;
+				set var.control = "127.0.0.1";
+				switch (var.control) {
+				case "10.10.0.5":
+					set req.http.case = "0";
+					break;
+				case "127.0.0.1":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "RTIME control value",
+			vcl: `
+			sub vcl_recv {
+				declare local var.control RTIME;
+				set var.control = 5s;
+				set req.http.case = "";
+				switch (var.control) {
+				case "2.000":
+					set req.http.case = "0";
+					break;
+				case "5.000":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "TIME control value",
+			vcl: `
+			sub vcl_recv {
+				declare local var.control TIME;
+				set var.control = std.time("Mon, 02 Jan 2006 22:04:05 GMT", now);
+				switch (var.control) {
+				case "Mon, 02 Jan 2006 22:04:06 GMT":
+					set req.http.case = "0";
+					break;
+				case "Mon, 02 Jan 2006 22:04:05 GMT":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "Builtin function call control value",
+			vcl: `
+			sub vcl_recv {
+				switch (randomint(1,1)) {
+				case "0":
+					set req.http.case = "0";
+					break;
+				case "1":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "User defined function call control value",
+			vcl: `
+			sub fn STRING { return "1"; }
+			sub vcl_recv {
+				switch (fn()) {
+				case "0":
+					set req.http.case = "0";
+					break;
+				case "1":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "literal control value",
+			vcl: `
+			sub vcl_recv {
+				switch ("1") {
+				case "0":
+					set req.http.case = "0";
+					break;
+				case "1":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "regex case match",
+			vcl: `
+			sub vcl_recv {
+				switch ("foo") {
+				case ~"ar$":
+					set req.http.case = "0";
+					break;
+				case ~"oo$":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "case ordering",
+			vcl: `
+			sub vcl_recv {
+				switch ("foo") {
+				case "foo":
+					set req.http.case = "0";
+					break;
+				case ~"oo$":
+					set req.http.case = "1";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "0"},
+			},
+			isError: false,
+		},
+		{
+			name: "default case",
+			vcl: `
+			sub vcl_recv {
+				switch ("foo") {
+				case "bar":
+					set req.http.case = "0";
+					break;
+				case ~"az$":
+					set req.http.case = "1";
+					break;
+				default:
+					set req.http.case = "2";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case": &value.String{Value: "2"},
+			},
+			isError: false,
+		},
+		{
+			name: "fallthrough case",
+			vcl: `
+			sub vcl_recv {
+				switch ("baz") {
+				case "bar":
+					set req.http.case = "0";
+					break;
+				case ~"az$":
+					set req.http.case = "1";
+					set req.http.fallthrough = "1";
+					fallthrough;
+				case "foo":
+					set req.http.case = "2";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case":        &value.String{Value: "2"},
+				"req.http.fallthrough": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "return in fallthrough case",
+			vcl: `
+			sub vcl_recv {
+				switch ("baz") {
+				case "bar":
+					set req.http.case = "0";
+					break;
+				case ~"az$":
+					set req.http.case = "1";
+					set req.http.fallthrough = "1";
+					return (pass);
+					fallthrough;
+				case "foo":
+					set req.http.case = "2";
+					break;
+				}
+			}`,
+			assertions: map[string]value.Value{
+				"req.http.case":        &value.String{Value: "1"},
+				"req.http.fallthrough": &value.String{Value: "1"},
+			},
+			isError: false,
+		},
+		{
+			name: "ID control value",
+			vcl: `
+			table foo {}
+			sub vcl_recv {
+				switch (foo) {
+				case "1":
+					break;
+				}
+			}`,
+			isError: true,
+		},
+		{
+			name: "user defined function with non-string return type control value",
+			vcl: `
+			sub string_fn INTEGER { return 1; }
+			sub vcl_recv {
+				switch (string_fn()) {
+				case "1":
+					break;
+				}
+			}`,
+			isError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assertInterpreter(t, tt.vcl, context.RecvScope, tt.assertions, tt.isError)
+		})
+	}
+}
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 0e86564..8037e9a 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -80,6 +80,20 @@ sub vcl_recv {
 	unset req.http.Cookie;
 	return(pass);
 	synthetic.base64 {"foo bar"};
+
+	switch (req.url) {
+	case "/":
+		esi;
+		break;
+	case ~ "[2-3]":
+		esi;
+		fallthrough;
+	default:
+		esi;
+		break;
+	}
+
+	set req.http.default:foo = "bar";
 }`
 
 	expects := []token.Token{
@@ -435,6 +449,55 @@ sub vcl_recv {
 		{Type: token.STRING, Literal: "foo bar"},
 		{Type: token.SEMICOLON, Literal: ";"},
 		{Type: token.LF, Literal: "\n"},
+		{Type: token.LF, Literal: "\n"},
+
+		{Type: token.SWITCH, Literal: "switch"},
+		{Type: token.LEFT_PAREN, Literal: "("},
+		{Type: token.IDENT, Literal: "req.url"},
+		{Type: token.RIGHT_PAREN, Literal: ")"},
+		{Type: token.LEFT_BRACE, Literal: "{"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.CASE, Literal: "case"},
+		{Type: token.STRING, Literal: "/"},
+		{Type: token.COLON, Literal: ":"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.ESI, Literal: "esi"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.BREAK, Literal: "break"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.CASE, Literal: "case"},
+		{Type: token.REGEX_MATCH, Literal: "~"},
+		{Type: token.STRING, Literal: "[2-3]"},
+		{Type: token.COLON, Literal: ":"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.ESI, Literal: "esi"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.FALLTHROUGH, Literal: "fallthrough"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.DEFAULT, Literal: "default"},
+		{Type: token.COLON, Literal: ":"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.ESI, Literal: "esi"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.BREAK, Literal: "break"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.RIGHT_BRACE, Literal: "}"},
+		{Type: token.LF, Literal: "\n"},
+
+		{Type: token.LF, Literal: "\n"},
+		{Type: token.SET, Literal: "set"},
+		{Type: token.IDENT, Literal: "req.http.default:foo"},
+		{Type: token.ASSIGN, Literal: "="},
+		{Type: token.STRING, Literal: "bar"},
+		{Type: token.SEMICOLON, Literal: ";"},
+		{Type: token.LF, Literal: "\n"},
+
 		{Type: token.RIGHT_BRACE, Literal: "}"},
 		{Type: token.EOF, Literal: ""},
 	}
diff --git a/linter/linter_test.go b/linter/linter_test.go
index cc8ad20..1074ca8 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -870,6 +870,42 @@ sub foo {
 	})
 }
 
+func TestLintSwitchStatement(t *testing.T) {
+	t.Run("pass: simple switch", func(t *testing.T) {
+		input := `
+sub foo {
+	switch (req.http.foo) {
+	case "bar":
+		break;
+	}
+}`
+		assertNoError(t, input)
+	})
+
+	t.Run("undefined function in control", func(t *testing.T) {
+		input := `
+sub foo {
+	switch (undefined()) {
+	case "bar":
+		break;
+	}
+}`
+		assertError(t, input)
+	})
+
+	t.Run("bool function in control", func(t *testing.T) {
+		input := `
+sub boolfn BOOL { return true; }
+sub foo {
+	switch (boolfn()) {
+	case "1":
+		break;
+	}
+}`
+		assertError(t, input)
+	})
+}
+
 func TestLintBangPrefixExpression(t *testing.T) {
 	t.Run("pass: use with variable identity", func(t *testing.T) {
 		input := `
diff --git a/parser/statement_parser_test.go b/parser/statement_parser_test.go
index 2a924b3..4a78e66 100644
--- a/parser/statement_parser_test.go
+++ b/parser/statement_parser_test.go
@@ -779,6 +779,359 @@ sub vcl_recv {
 	})
 }
 
+func TestParseSwitchStatement(t *testing.T) {
+	t.Run("switch statement", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	case "1":
+		esi;
+		break;
+	default:
+		esi;
+		fallthrough;
+	case ~/* infix */"[2-3]":
+		esi;
+		break;
+	}
+}`
+		expect := &ast.VCL{
+			Statements: []ast.Statement{
+				&ast.SubroutineDeclaration{
+					Meta: ast.New(T, 0, comments("// Subroutine")),
+					Name: &ast.Ident{
+						Meta:  ast.New(T, 0),
+						Value: "vcl_recv",
+					},
+					Block: &ast.BlockStatement{
+						Meta: ast.New(T, 1),
+						Statements: []ast.Statement{
+							&ast.SwitchStatement{
+								Meta: ast.New(T, 1, comments("// Switch")),
+								Control: &ast.Ident{
+									Meta:  ast.New(T, 1),
+									Value: "req.http.host",
+								},
+								Default: 1,
+								Cases: []*ast.CaseStatement{
+									{
+										Meta: ast.New(T, 2),
+										Test: &ast.InfixExpression{
+											Meta:     ast.New(T, 2),
+											Operator: "==",
+											Right: &ast.String{
+												Meta:  ast.New(T, 2),
+												Value: "1"},
+										},
+										Statements: []ast.Statement{
+											&ast.EsiStatement{
+												Meta: ast.New(T, 2),
+											},
+											&ast.BreakStatement{
+												Meta: ast.New(T, 2),
+											},
+										},
+									},
+									{
+										Meta: ast.New(T, 2),
+										Statements: []ast.Statement{
+											&ast.EsiStatement{
+												Meta: ast.New(T, 2),
+											},
+											&ast.FallthroughStatement{
+												Meta: ast.New(T, 2),
+											},
+										},
+										Fallthrough: true,
+									},
+									{
+										Meta: ast.New(T, 2),
+										Test: &ast.InfixExpression{
+											Meta:     ast.New(T, 2),
+											Operator: "~",
+											Right: &ast.String{
+												Meta:  ast.New(T, 2, comments("/* infix */")),
+												Value: "[2-3]"},
+										},
+										Statements: []ast.Statement{
+											&ast.EsiStatement{
+												Meta: ast.New(T, 2),
+											},
+											&ast.BreakStatement{
+												Meta: ast.New(T, 2),
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		vcl, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("%+v", err)
+		}
+		assert(t, vcl, expect)
+	})
+	t.Run("switch statement with function control statement", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (uuid.version4()) {
+	case "xxx-xxx-xxx":
+		esi;
+		break;
+	}
+}`
+		expect := &ast.VCL{
+			Statements: []ast.Statement{
+				&ast.SubroutineDeclaration{
+					Meta: ast.New(T, 0, comments("// Subroutine")),
+					Name: &ast.Ident{
+						Meta:  ast.New(T, 0),
+						Value: "vcl_recv",
+					},
+					Block: &ast.BlockStatement{
+						Meta: ast.New(T, 1),
+						Statements: []ast.Statement{
+							&ast.SwitchStatement{
+								Meta: ast.New(T, 1, comments("// Switch")),
+								Control: &ast.FunctionCallExpression{
+									Meta: ast.New(T, 1),
+									Function: &ast.Ident{
+										Meta:  ast.New(T, 1),
+										Value: "uuid.version4",
+									},
+									Arguments: []ast.Expression{},
+								},
+								Default: -1,
+								Cases: []*ast.CaseStatement{
+									{
+										Meta: ast.New(T, 2),
+										Test: &ast.InfixExpression{
+											Meta:     ast.New(T, 2),
+											Operator: "==",
+											Right: &ast.String{
+												Meta:  ast.New(T, 2),
+												Value: "xxx-xxx-xxx"},
+										},
+										Statements: []ast.Statement{
+											&ast.EsiStatement{
+												Meta: ast.New(T, 2),
+											},
+											&ast.BreakStatement{
+												Meta: ast.New(T, 2),
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		vcl, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("%+v", err)
+		}
+		assert(t, vcl, expect)
+	})
+	t.Run("switch statement with bool literal control statement", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (true) {
+	case "1":
+		esi;
+		break;
+	default:
+		esi;
+		break;
+	}
+}`
+		expect := &ast.VCL{
+			Statements: []ast.Statement{
+				&ast.SubroutineDeclaration{
+					Meta: ast.New(T, 0, comments("// Subroutine")),
+					Name: &ast.Ident{
+						Meta:  ast.New(T, 0),
+						Value: "vcl_recv",
+					},
+					Block: &ast.BlockStatement{
+						Meta: ast.New(T, 1),
+						Statements: []ast.Statement{
+							&ast.SwitchStatement{
+								Meta: ast.New(T, 1, comments("// Switch")),
+								Control: &ast.Boolean{
+									Meta:  ast.New(T, 1),
+									Value: true,
+								},
+								Default: 1,
+								Cases: []*ast.CaseStatement{
+									{
+										Meta: ast.New(T, 2),
+										Test: &ast.InfixExpression{
+											Meta:     ast.New(T, 2),
+											Operator: "==",
+											Right: &ast.String{
+												Meta:  ast.New(T, 2),
+												Value: "1"},
+										},
+										Statements: []ast.Statement{
+											&ast.EsiStatement{
+												Meta: ast.New(T, 2),
+											},
+											&ast.BreakStatement{
+												Meta: ast.New(T, 2),
+											},
+										},
+									},
+									{
+										Meta: ast.New(T, 2),
+										Statements: []ast.Statement{
+											&ast.EsiStatement{
+												Meta: ast.New(T, 2),
+											},
+											&ast.BreakStatement{
+												Meta: ast.New(T, 2),
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		vcl, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("%+v", err)
+		}
+		assert(t, vcl, expect)
+	})
+	t.Run("float literal as control should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (1.0) {
+	case "1.00":
+		esi;
+	default:
+		esi;
+		break;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+	t.Run("case with missing break should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	case "1":
+		esi;
+	default:
+		esi;
+		break;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+	t.Run("duplicate cases should should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	case "1":
+		esi;
+		break;
+	case "1":
+		esi;
+		break;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+	t.Run("duplicate default cases should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	default:
+		esi;
+		break;
+	default:
+		esi;
+		break;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+	t.Run("case with non-string match expression should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	case 1:
+		esi;
+		break;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+	t.Run("fallthrough on final case should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	case "1":
+		esi;
+		fallthrough;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+	t.Run("break in nested block statement should fail", func(t *testing.T) {
+		input := `// Subroutine
+sub vcl_recv {
+	// Switch
+	switch (req.http.host) {
+	case "1":
+		if ("") { break; }
+		esi;
+		break;
+	}
+}`
+		_, err := New(lexer.NewFromString(input)).ParseVCL()
+		if err == nil {
+			t.Errorf("expected error")
+		}
+	})
+}
+
 func TestParseUnsetStatement(t *testing.T) {
 	input := `// Subroutine
 sub vcl_recv {