If Statements

An if statement conditionally executes a block of code. The condition is evaluated once. If it evaluates to true, the first branch is executed. If it evaluates to false, execution continues with an optional else if or else branch.

Sec requires conditions to have type bool. Numbers, strings, references and other values are never interpreted as true or false implicitly.

Basic Syntax

The simplest form contains one condition and one block:

if condition {
    statements
}

Example:

if rawResult < 0 {
    return Err(_decodeError(-rawResult))
}

Parentheses are not required around the condition. Braces are required around the branch body, including when the body contains only one statement.

if ready {
    Start()
}

The following form is not valid Sec syntax:

if (ready)
    Start()
Rule: An if condition is written without mandatory parentheses, and every branch body is enclosed by braces.

Conditions

The expression following if must produce a bool. A boolean variable may be used directly:

let connected := IsConnected()

if connected {
    SendData()
}

A function call returning bool may also be used directly:

if IsConnected() {
    SendData()
}

More commonly, the condition is produced by a comparison or a logical expression.

No Implicit Truthiness

Sec has no truthy or falsy values. An integer is not false when it is zero, a string is not false when it is empty, and a reference is not true merely because it exists.

let count := 3

if count {
    Process()
} // Error: if condition must be bool, got int.

The intended test must be written explicitly:

if count > 0 {
    Process()
}

Similarly, strings must be compared or inspected explicitly:

let name := "Anna"

if name != "" {
    PrintName(name)
}

Explicit conditions make the programmer's intent visible and prevent values from acquiring unrelated boolean meanings.

Comparison Conditions

Comparison operators produce bool values and may therefore be used as conditions:

==
!=
<
<=
>
>=

Example:

if rawResult < 0 {
    return Err(_decodeError(-rawResult))
}

The operands must support the selected comparison. Named types retain their distinct type identity, so unrelated named types cannot be compared merely because they share the same underlying representation.

type Speed int
type Money int

let speed: Speed := 50
let money: Money := 50

if speed == money {
} // Error: Speed and Money are different types.

Logical Conditions

Boolean expressions may be combined with the logical operators:

!condition
left && right
left || right

Both operands of && and || must be bool.

if connected && authenticated {
    SendPrivateData()
}
if isAdmin || isOwner {
    AllowEdit()
}
if !finished {
    ContinueWork()
}

Logical expressions may be grouped with parentheses when grouping is needed for clarity or precedence:

if connected && (isAdmin || isOwner) {
    AllowEdit()
}

else

An else branch runs when the preceding if condition is false.

if temperature < FreezingPoint {
    StartHeater()
} else {
    StopHeater()
}

Exactly one of the two branches is executed. After the selected branch finishes, execution continues after the complete if statement unless the branch has already returned or otherwise terminated control flow.

else if Chains

Several conditions may be tested in order by using else if. Conditions are evaluated from top to bottom. The first condition that evaluates to true selects its branch.

if score >= 90 {
    grade = Grade.A
} else if score >= 80 {
    grade = Grade.B
} else if score >= 70 {
    grade = Grade.C
} else {
    grade = Grade.F
}

Once a branch has been selected, later conditions in the chain are not evaluated.

A final else is optional. Without it, execution simply continues after the chain when no condition matches.

Nested if Statements

An if statement may appear inside any branch:

if connected {
    if authenticated {
        SendPrivateData()
    } else {
        RequestAuthentication()
    }
}

Nesting is useful when the second condition is meaningful only after the first condition has succeeded. When several conditions belong to one decision, a combined boolean expression may be clearer.

if connected && authenticated {
    SendPrivateData()
}

Branch Scope

Every branch body is a block and therefore creates a child scope. Variables declared inside a branch are visible only inside that branch and its nested scopes.

if hasInput {
    let value := ReadInput()
    Process(value)
}

Process(value) // Error: unknown symbol value.

Separate branches may use the same local variable name because each branch has its own scope:

if usePrimary {
    let connection := OpenPrimary()
    Use(connection)
} else {
    let connection := OpenSecondary()
    Use(connection)
}

Variables declared before the if statement remain visible inside its branches. Mutating such a variable requires that it was declared with let mut.

let mut result := 0

if useAlternative {
    result = 10
}

Returning from Branches

A branch may return from the enclosing function. Once return is executed, no later statement in that function invocation is reached.

fn Normalize(value: int) int {
    if value < 0 {
        return -value
    }

    return value
}

This form is often clearer than wrapping the remainder of the function in an else block. The early-return branch handles the exceptional or special case, while the normal path continues without additional nesting.

Required Return Analysis

A non-void function must return a value on every reachable path. An if statement satisfies this requirement only when every possible branch returns.

Valid:

fn Pick(value: int) int {
    if value >= 10 {
        return 5
    } else {
        return 6
    }
}

Both possible paths return an int, so no additional return statement is required.

Also valid:

fn Pick(value: int) int {
    if value >= 10 {
        return 5
    }

    return 6
}

When the condition is false, execution continues to the final return statement.

Invalid:

fn Pick(value: int) int {
    if value >= 10 {
        return 5
    }
} // Error: function Pick must return int.

The condition may be false, leaving a reachable path that does not return a value.

In an else if chain, every branch must return and the chain must end with else before the chain alone can satisfy the function's required return.

Unreachable Code

Statements following an unconditional return in the same control-flow path are unreachable and are rejected.

fn Pick(value: int) int {
    if value >= 10 {
        return 5
        Log("unreachable") // Error: unreachable statement.
    }

    return 6
}

When every branch of an if statement returns, statements following the complete statement are also unreachable:

fn Pick(value: int) int {
    if value >= 10 {
        return 5
    } else {
        return 6
    }

    return 7 // Error: unreachable statement.
}

An if statement without an else does not by itself make following code unreachable, because its condition may be false.

if Is a Statement

In the current language model, if controls which statements execute. It does not itself produce a value.

Use assignment to a mutable variable when a branch must select a later value:

let mut label := "normal"

if value < 0 {
    label = "negative"
} else if value > 0 {
    label = "positive"
}

Use a function with explicit returns when the decision naturally computes a value:

fn Describe(value: int) string {
    if value < 0 {
        return "negative"
    } else if value > 0 {
        return "positive"
    }

    return "zero"
}

Result-returning Function

The following function combines unsafe system interaction, an if statement and a typed Result return value:

fn Write(fd: int, data: []byte) Result[uint, LinuxError] {
    let rawResult: int

    unsafe {
        rawResult = _rawSyscall3(
            _sysWrite,
            uint(fd),
            uint(data.ptr),
            data.len,
        )
    }

    if rawResult < 0 {
        return Err(_decodeError(-rawResult))
    }

    return Ok(uint(rawResult))
}

The function has two reachable outcomes:

  • A negative system-call result is converted into LinuxError and returned as Err(...).
  • A non-negative result is converted to uint and returned as Ok(...).

The error path returns early. The normal path therefore continues without an else branch. This keeps the successful path visually direct and avoids unnecessary nesting.

Common Errors

Using a non-boolean condition

let count := 10

if count {
} // Error: if condition must be bool, got int.

Omitting braces

if ready
    Start()

Reading a branch-local variable outside its scope

if ready {
    let value := Load()
}

Use(value) // Error: unknown symbol value.

Failing to return from every path

fn Value(valid: bool) int {
    if valid {
        return 1
    }
} // Error: function Value must return int.

Writing code after an unconditional return

if failed {
    return Err(error)
    Log(error) // Error: unreachable statement.
}

Summary of Rules

  • An if condition must have type bool.
  • Sec does not implicitly convert other values to bool.
  • Parentheses around the condition are optional and normally unnecessary.
  • Every branch body requires braces.
  • else if conditions are evaluated from top to bottom.
  • Only the first matching branch is executed.
  • Every branch creates its own local scope.
  • Variables declared inside a branch are not visible outside that branch.
  • An if statement may return from the enclosing function.
  • A non-void function must return on every reachable path.
  • An if chain guarantees a return only when all possible branches return.
  • Code after an unconditional return path is unreachable.
  • if is currently a control-flow statement and does not produce a value.