Conditional Blocks

S++ conditional blocks combine standard if-else statements with pattern-matching. This simplifies the language by reducing the number of constructs needed to handle conditional logic.

A wide range of pattern matching is supported:

  • Literals: 1, 1.0, true, false, "hello"

  • Wildcards: _

  • Capture variables: x, y, z

  • Or patterns: "yes" | "no"

  • Array destructuring: [1, 2, 3]

  • Tuple destructuring: (1, 2, 3)

  • Object destructuring: Person(name="john", age=30)

  • Guard clauses: and ...

  • Attribute aliasing: Person(name as new_name, age)

  • Skipping multiple elements: [1, .., last], (1, .., last), Person(name="john", ..)

  • Nested destructuring: [a, (0, y, z), Person(name="john", ..), _, other, ..]

Basic Branching

The following example shows the most basic version of the case-else block. It follows the standard if-else syntax of most programming languages.

case some_condition {
    std::console::print("hello world")
}
else case some_other_condition_1 {
    std::console::print("hello galaxy")
}
else case some_other_condition_2 {
    std::console::print("hello universe")
}
else {
    std::console::print("hello multiverse")
}

Imitation Ternary

There is no ?: ternary operator, as the case and else keywords can be used to achieve the same result. This also follows the “left-ro-right” reading order of the language.

let result = case some_condition { "true" } else { "false" }

Partial Fragments (Expressions)

Partial fragments can be used to introduce multiple comparisons or pattern matches against the same value. Each comparison contains a pattern fragment, which when combined with the partial fragment, creates a full pattern. Fragments are joined by a comparison operator: ==, !=, <, >, <=, >=, or is.

This subsection focuses on regular comparison operators, while the is operator is discussed in a later section. Pattern fragments are introduced with the of keyword.

case person of
    == john { "hello john" }
    == jane { "hello jane" }
else { "hello stranger" }
case age of
    <= -1 { "error" }
    == 00 { "newborn" }
    <  18 { "child" }
    <  60 { "adult" }
else { "senior" }

Partial expression are expanded into their full form by the compiler: the first case expression effectively becomes case person.eq(john) { }. See binary operator expansion for more information on expansion.

Multiple Pattern Fragments

Multiple pattern fragments can be used on the same branch, separated by a comma. This allows for more complex pattern matching, and can be used to match multiple values at once.

case person of
    == john, jane { "hello john or jane" }
    == jack, jill { "hello jack or jill" }
else { "hello stranger" }

Multiple pattern fragments per branch can only be used with non-is pattern fragments. The reason behind this is explored in the destructuring sections.

Destructuring Objects

Destructuring objects in a pattern match is done by using the is operator. This follows the standard binary is functionality, where the right-hand-side can be a destructure of any variation.

case person of
    is Person(name="john", ..) { "hello john" }
    is Person(name="jane", ..) { "hello jane" }
else { "hello stranger" }

The .. token is used to mark missing attributes.

Destructuring Variant Objects

Because equality checks of variants can match their inner types (if the variant is the let-hand-side), the is operator can be used for destructuring a vairiant object.

case optional of
    is Some[Str](val) { "some" }
    is None { "none" }

Due to current generic implementations, Some[Str](val) must be used instead of Some(val). There is an issue with generics inside variant types. This will be fixed in a future release.

Destructuring Generic Types

There is no keyword-equivalent to C++’s if constexpr(...) to check generic types inside a function. As such, destructuring generic types was introduced to allow for pattern matching on generic types.

fun func[T](a: T) -> Void {
    case a of
        is Str() { std::console::print("Str") }
        is U32() { std::console::print("U32") }
}

Destructuring Tuples, Arrays

Tuples and arrays can be destructured, because their size is known, and so the number of elements in the pattern match can be verified. Unless the .. token is present, as with object identifiers, all elements must be matched. The .. token can appear anywhere in the pattern match, but only once per pattern.

case tuple of
    is (1, ..) { "tuple starts with 1" }
    is (2, ..) { "tuple starts with 2" }
else { "tuple is something else" }
case array of
    is [1, ..] { "array starts with 1" }
    is [2, ..] { "array starts with 2" }
else { "array is something else" }

Destructuring with Bindings

Variables can be bound during a destructure, and used in the branch. This allows for more complex pattern matching, and can be used to extract values from the object. Destructures that contain bindings are expanded into let statements, as their semantics are almost identical.

Note that for nested bindings, such as Line(point=Point(a, b), ..), the variable point does exist, but in a partially moved state, because let a = point.a and let b = point.b will have both been executed to get the values for a and b.

case tuple of
    is (a, 1) { "tuple is ($a, 1)" }
    is (b, 2) { "tuple is ($b, 2)" }
else { "tuple is something else" }
case array of
    is [a, 1] { "array is [$a, 1]" }
    is [b, 2] { "array is [$b, 2]" }
else { "array is something else" }
case person of
    is Person(name="john", age, ..) { "hello john, you are ${age}" }
    is Person(name="jane", age, ..) { "hello jane, you are ${age}" }
else { "hello stranger" }

Patterns can be nested too, to any depth and with any combination of object/variant/array/tuple destructuring, enabling highly complex pattern matching.

Pattern Guards on Destructures

Pattern guards can only be used on destructures, as the and would conflict with normal expressions.

case person of
    is Person(name="john", age, ..) and age < 18 { "hello john, you are a child" }
    is Person(name="john", age, ..) and age < 60 { "hello john, you are an adult" }
    is Person(name="john", age, ..) { "hello john, you are a senior" }
else { "hello stranger" }