Switch Statements

A switch statement selects one branch from an ordered set of cases. It is intended for value-based branching and may either compare one subject expression against several cases or evaluate boolean conditions directly when no subject is present.

Cases are tested from top to bottom, and the first matching case is selected. After the selected case finishes, execution continues after the switch unless the case returns, otherwise terminates, or uses an explicit fallthrough statement.

Rule: switch is for value-based branching. Structural matching, destructuring, union variants, Result and Option belong to match.

Switch with a Subject

The most common form evaluates one subject expression and compares it with one or more case items.

switch expression {
case value:
    statements

case otherValue:
    statements

default:
    statements
}

Example:

switch status {
case Status.ready:
    Start()

case Status.waiting:
    Wait()

case Status.failed:
    Stop()

default:
    Reset()
}

The switch body is enclosed by braces. Each case begins with case, followed by one or more case items and a colon. The optional default clause handles every value that did not match an earlier case.

Subject Evaluation

The subject expression is evaluated exactly once.

switch ReadStatus() {
case Status.ready:
    Start()

default:
    Stop()
}

In this example, ReadStatus() is called once. Its result is then used for all case comparisons. This rule is important when the subject expression performs work, reads mutable state or has visible side effects.

Case Order

Cases are tested in source order. The first matching case wins. Once a case has matched, later case expressions are not evaluated.

switch value {
case minimum:
    AtMinimum()

case CalculateTarget():
    AtTarget()

default:
    Other()
}

CalculateTarget() is only called when the first case does not match. Case order may therefore affect both program behavior and performance.

Value Cases

A value case compares the switch subject with a compatible case expression.

switch value {
case 1:
    One()

case 2:
    Two()

default:
    Other()
}

Each case value must be equality-compatible with the subject type. Sec does not perform unrelated implicit conversions merely to make a case compile.

let value: int := 1

switch value {
case "one":
    One()
} // Error: switch case must be compatible with subject type int, got string.

Case Expressions

Case items are not limited to literals. A case expression may be a:

  • literal,
  • constant,
  • variable,
  • field or property,
  • function call,
  • method call,
  • or another expression compatible with the subject.
switch value {
case minimum:
    AtMinimum()

case limits.Maximum:
    AtMaximum()

case CalculateTarget():
    AtTarget()
}

Case expressions are evaluated only if execution reaches their case. Expressions in one case are evaluated from left to right.

Multiple Values in One Case

Several alternatives may share one case body by separating them with commas.

switch value {
case 2, 3, 5, 7:
    Prime()

default:
    Other()
}

The case above is equivalent to testing:

value == 2 || value == 3 || value == 5 || value == 7

Alternatives are tested from left to right. The body runs once when any alternative matches.

Range Cases

Ranges may be used directly as case items.

switch score {
case 0..<50:
    Failed()

case 50..<80:
    Passed()

case 80..100:
    Excellent()
}

Normal Sec range forms are supported:

start..end
start..<end
start..
..end
..<end

The subject and the range bounds must have compatible ordered types. Inclusive and exclusive range boundaries retain their normal meaning.

switch temperature {
case ..<0:
    Frozen()

case 0..<100:
    Liquid()

case 100..:
    BoilingOrAbove()
}

Values and ranges may be combined in the same case:

switch value {
case 1, 3, 5, 10..<20:
    Selected()

default:
    Other()
}

Relational Cases

A relational case compares the switch subject without repeating it. The supported operators are <, <=, > and >=.

switch value {
case < 0:
    Negative()

case 0:
    Zero()

case > 0:
    Positive()
}

The following case:

case < limit:

means:

subject < limit

Several relational alternatives may share one body:

switch value {
case < minimum, > maximum:
    Outside()

default:
    Inside()
}

Relational cases require an ordered subject type and an operand compatible with that type.

Subjectless Switch

A switch without a subject evaluates boolean conditions. It is useful when several peer conditions should be tested in order.

switch {
case score < 0:
    Invalid()

case score < 50:
    Failed()

case score < 80:
    Passed()

default:
    Excellent()
}

Every case item in a subjectless switch must have type bool. No implicit truthiness is applied.

switch {
case 10:
    Invalid()
} // Error: subjectless switch case must be bool, got int.

Comma-separated conditions use short-circuit logical OR:

switch {
case user.IsAdmin(), user.IsOwner():
    Allow()

case user.IsBlocked():
    Deny()

default:
    Review()
}

A subjectless switch behaves similarly to an if/else if chain. The switch form is often clearer when all branches are conceptually equal cases of one decision.

default

The optional default clause runs when no case matches. A switch may contain at most one default, and it must be the final clause.

switch value {
case 1:
    One()

case 2:
    Two()

default:
    Other()
}

If no case matches and no default exists, the switch performs no branch body and execution continues after the switch.

switch value {
default:
    Other()

case 1:
    One()
} // Error: default must be the final switch clause.

No Implicit Fallthrough

A case ends automatically. Sec does not require break at the end of each case and does not continue into the next case implicitly.

switch value {
case 1:
    One()

case 2:
    Two()
}

When value is 1, only One() runs.

Explicit fallthrough

fallthrough transfers control directly to the body of the next case. The next case's condition is not tested.

switch value {
case 1:
    One()
    fallthrough

case 2:
    OneOrTwo()

default:
    Other()
}

When value is 1:

  1. One() executes.
  2. Control enters the next case body.
  3. The case 2 condition is not evaluated.
  4. OneOrTwo() executes.

fallthrough must be the final non-empty statement in a case. It is not allowed in the final case or in default. It is only valid directly inside a switch case body.

case 1:
    fallthrough
    MoreWork() // Error: fallthrough must be the final statement.

Empty Cases

An empty case is legal and does not fall through implicitly.

switch value {
case 1:

case 2:
    Two()
}

When value is 1, no statements execute and control continues after the switch. Use explicit fallthrough when entering the next case body is intended.

Case Scope

Each case body creates an independent lexical scope.

switch value {
case 1:
    let message: string := "one"
    Print(message)

case 2:
    let message: string := "two"
    Print(message)
}

The two variables named message are legal because the sibling case scopes do not overlap. A variable declared in one case is not visible in another case or after the switch.

switch value {
case 1:
    let local: int := 10
}

Print(local) // Error: local is outside its scope.

Variables declared before the switch are visible in all cases, and normal mutability rules apply.

let mut result: int := 0

switch value {
case 1:
    result = 10

case 2:
    result = 20

default:
    result = 30
}

Switch Termination

A switch is considered terminating when:

  • it has a default clause,
  • every reachable case body terminates,
  • every fallthrough chain eventually reaches a terminating case,
  • and no reachable path continues after the switch.
fn Classify(value: int) int {
    switch value {
    case < 0:
        return -1

    case 0:
        return 0

    default:
        return 1
    }
}

This function satisfies the return requirement because every possible switch path returns.

fn Invalid(value: int) int {
    switch value {
    case 0:
        return 0
    }
} // Error: value may not match any case.

Definite Assignment

Assignment analysis merges only the paths that can continue after the switch.

fn Select(value: int) int {
    let mut result: int

    switch value {
    case 1:
        result = 10

    case 2:
        result = 20

    default:
        result = 30
    }

    return result
}

This is valid because every possible continuing path assigns result before it is used.

fn Invalid(value: int) int {
    let mut result: int

    switch value {
    case 1:
        result = 10
    }

    return result
} // Error: value may not match case 1.

Duplicate and Overlapping Cases

Compile-time duplicate case values are invalid.

switch value {
case 1:
    A()

case 1:
    B()
} // Error: duplicate switch case value 1.

Compile-time ranges must not overlap.

switch score {
case 0..50:
    A()

case 40..100:
    B()
} // Error: switch case range overlaps previous case.

A value may not be covered by an earlier compile-time range.

switch score {
case 0..100:
    InRange()

case 50:
    Exact()
} // Error: switch case value 50 is already covered by previous case.

Clearly unreachable relational cases are also rejected.

switch value {
case >= 0:
    NonNegative()

case > 10:
    Large()
} // Error: previous case already covers this condition.

When case expressions or range bounds are dynamic, overlap may not be provable at compile time. Such cases are legal, and the first runtime match wins.

Enums and Strings

Enum values may be used as switch subjects.

switch direction {
case Direction.north:
    North()

case Direction.south:
    South()

default:
    Other()
}

An enum switch is not required to be exhaustive. Use match when every enum value must be handled explicitly.

Strings may be used when normal string equality is available:

switch command {
case "start":
    Start()

case "stop":
    Stop()

default:
    Unknown()
}

A boolean subject is legal, although if/else is often clearer:

switch ready {
case true:
    Start()

case false:
    Wait()
}

Semantic Types and Units

Named types retain their normal identity inside switch statements.

type Speed int

let speed: Speed := 50
let minimumSpeed: Speed := 10
let maximumSpeed: Speed := 100

switch speed {
case minimumSpeed..maximumSpeed:
    Normal()
}

Plain values of an unrelated type are not accepted implicitly:

switch speed {
case 0..100:
    Normal()
} // Error when int is not implicitly compatible with Speed.

Unit values also retain unit compatibility rules.

switch distance {
case 0<m>..<100<m>:
    Near()

case 100<m>..:
    Far()
}

Values with incompatible units cannot be compared in the same switch.

switch versus match

switch performs ordered value-based branching. It does not perform structural matching.

Use switch for:

  • values,
  • ranges,
  • relational comparisons,
  • ordered boolean conditions.

Use match for:

  • union variants,
  • Result,
  • Option,
  • destructuring,
  • type patterns,
  • exhaustive structural branching.
match result {
    Ok(value) => Use(value)
    Err(error) => Handle(error)
}

The following is not switch syntax:

switch result {
case Ok(value):
}
Rule: switch is initially a statement, not an expression. Use match for exhaustive value-producing branching or assign explicitly inside a switch.

Common Errors

Incompatible case type

let value: int := 10

switch value {
case "10":
    Handle()
}

string is not compatible with an int subject.

Non-boolean subjectless case

switch {
case 10:
    Handle()
}

Every subjectless case expression must be bool.

Multiple defaults

switch value {
default:
    A()

default:
    B()
}

A switch may contain only one default.

Implicit fallthrough assumption

switch value {
case 1:

case 2:
    Handle()
}

Case 1 is empty. It does not continue into case 2. Add fallthrough explicitly when that is intended.

Using continue for cases

continue belongs to loops. It does not advance to the next switch case. Use fallthrough to enter the next case body.

Summary of Rules

  • A switch may have a subject expression or no subject.
  • The subject is evaluated exactly once.
  • Cases are tested from top to bottom.
  • The first matching case wins.
  • Comma-separated case items mean logical OR.
  • Value, range and relational cases must be compatible with the subject type.
  • Every case in a subjectless switch must be bool.
  • default is optional, unique and always final.
  • Cases do not fall through implicitly.
  • fallthrough enters only the next case body and must be the final statement.
  • Each case body has its own lexical scope.
  • Duplicate and provably overlapping compile-time cases are errors.
  • switch is value-based and non-structural.
  • match owns variants, destructuring and exhaustive structural branching.
  • switch is a statement, not an expression.