Conditional Statements
Conditional statements let you execute different blocks of code depending on whether a condition evaluates to true or false. Conditions must evaluate to a bool value.
if (condition) {
// executes if condition is true
} else {
// executes if condition is false
}
Chained conditions:
var grade: char;
if (score >= 90) {
grade = 'A';
} else if (score >= 75) {
grade = 'B';
} else {
grade = 'C';
}
Looping Constructs
A for loop allows repeating a block of code multiple times. It usually consists of three optional parts inside the loop header:
- Initializer - runs once before the loop begins (often used to declare and initialize a counter).
- Condition - checked before each iteration; if false, the loop exits.
- Increment/Update - executed after each iteration.
for (initializer; condition; increment) {
// body
}
Classic Counter Loop
for (var i = 0; i < 10; i++) {
printf("%d\n", i);
}
Runs with i taking values from 0 to 9.
Manual Increment Loop
for (var i = 0; i < 10;) {
// executes while i < 10
// you should manually increment i inside the body
i++;
}
Here, the increment part is omitted. The loop continues until the condition becomes false, but you control when i is updated.
Conditionless Loop with Initializer
for (var attempts = 0;) {
if (attempts > 5) {
break;
}
tryConnect();
attempts++;
}
With no condition provided, the loop is infinite unless exited using break or return.
Pure Infinite Loop
Warning!
An infinite loop can also be written as:
while (true) {
handleEvent();
}
Both forms are semantically equivalent. However, using for { ... } is preferred for infinite loops to keep the codebase stylistically consistent and to avoid mixing multiple looping styles for the same concept.
for {
// infinite loop with no initializer, no condition, no increment
// must be exited with break or return
}
This form has no initializer, no condition, and no update. It must be terminated explicitly.
While Statement
A while loop repeatedly executes a block of code as long as a condition evaluates to true. The condition is checked before each iteration. If the condition is false at the start, the loop body never runs.
while (isRunning) {
tick();
}
var attempts = 0;
while (true) {
printf("attempt %d\n", attempts);
attempts++;
if (attempts >= maxRetries) {
break;
}
}
Switch Statement
Info
This is not pattern matching. Cases in Cyrus are always compared against raw values, not structural patterns or conditions.
The switch statement provides structured control flow based on the value of an expression. It supports enum variants with destructuring, value-based matching, and an optional default branch.
The general form is:
Basic Form
switch (expression) {
case pattern => {
// body
}
default => {
// optional fallback
}
}
Each case is introduced using => and executes its associated block when the pattern matches.
Execution does not fall through; exactly one matching branch is executed.
Switching on Enums
When switching over enum values, each case matches a specific variant.
Variants without payloads match by name, while variants with associated data can destructure their fields directly.
enum Color {
Red,
Green = "green!",
Custom(uint, uint, uint)
}
pub fn main() {
const color = Color.Green;
switch (color) {
case .Red => {
printf("red\n");
}
case .Green(value) => {
printf("%s\n", value);
}
case .Custom(r, g, b) => {
printf("(%d, %d, %d)\n", r, g, b);
}
}
}
The switch is exhaustive: all possible variants of Color are handled.
Switch Enum Without Payloads
enum Color {
Red,
Purple,
Black
}
pub fn main() {
const color = Color.Black;
switch (color) {
case .Red => {
printf("red\n");
}
case .Purple => {
printf("purple\n");
}
case .Black => {
printf("black\n");
}
}
}
Each case matches a concrete enum variant. Since all variants are covered, no default branch is required.
Destructuring Tagged Values
Enums are commonly used as tagged unions. The switch statement allows safe extraction of the stored value based on the active variant.
enum Value {
Int(int64),
Text(char*)
}
pub fn main() {
const value = Value.Text("Cyrus!");
switch (value) {
case .Int(number) => {
printf("int_value(%d)\n", number);
}
case .Text(text) => {
printf("text_value(%s)\n", text);
}
}
}
Here, the bound variable (number, text) is available only within its corresponding case block.
Switching On Non-enum Values
Switch can also be used with non-enum expressions such as integers, strings, or other comparable values.
pub fn main() {
const text = "Cyrus!";
switch (text) {
case "Cyrus!" => {
printf("Cyrus The Great\n");
}
default => {
printf("unknown\n");
}
}
}
In this form:
- Each case compares the switch expression against a concrete value
- Default is executed when no case matches
- Matching is value-based, not pattern-based
Jump Blocks
Cyrus supports jump blocks using the goto statement. Jump blocks allow you to transfer control to a labeled point in the current function. While goto should be used sparingly, it can simplify certain low-level control flows or state-machine-like logic.
A label is an identifier followed by a colon and the goto statement transfers execution to that label:
pub fn main() {
label_name:
// code
goto label_name; // jumps to the labeled block
}
Labels must be defined within the same function.
import std::libc{printf, exit};
pub fn main() {
var x = 0;
increment: // label for incrementing
x++;
check: // label for condition check
if (x < 5) {
goto increment; // jump back to increment
} else {
printf("%d\n", x);
exit(0);
}
}

