If-Else Expressions - Feature 7/41

If-else expressions let you execute different code based on conditions. In Ruchy, if is an expression that returns a value, not just a statement.

Basic If Expression

Execute code only when a condition is true:

let age = 20

if age >= 18 {
  "Adult"
}
// Returns: "Adult"

Expected Output: "Adult"

Test Coverage: ✅ tests/lang_comp/control_flow/if_else.rs

If-Else Expression

Provide alternative code when condition is false:

let age = 15

if age >= 18 {
  "Adult"
} else {
  "Minor"
}
// Returns: "Minor"

Expected Output: "Minor"

Try It in the Notebook

let temperature = 75

let weather = if temperature > 80 {
  "Hot"
} else {
  "Comfortable"
}

weather  // Returns: "Comfortable"

Expected Output: "Comfortable"

If-Else-If Chains

Test multiple conditions in sequence:

let score = 85

let grade = if score >= 90 {
  "A"
} else if score >= 80 {
  "B"
} else if score >= 70 {
  "C"
} else if score >= 60 {
  "D"
} else {
  "F"
}

grade  // Returns: "B"

Expected Output: "B"

Example: Temperature Ranges

let temp = 68

let description = if temp > 90 {
  "Very hot"
} else if temp > 75 {
  "Warm"
} else if temp > 60 {
  "Comfortable"
} else if temp > 40 {
  "Cool"
} else {
  "Cold"
}

description  // Returns: "Comfortable"

Expected Output: "Comfortable"

If as an Expression

IMPORTANT: In Ruchy, if always returns a value - it's an expression, not just a statement.

let x = 10
let max = if x > 5 { x } else { 5 }

max  // Returns: 10

Expected Output: 10

Example: Absolute Value

let n = -42
let abs_value = if n < 0 { -n } else { n }

abs_value  // Returns: 42

Expected Output: 42

Example: Conditional Assignment

let balance = 1000
let has_funds = if balance > 0 { true } else { false }

has_funds  // Returns: true

Expected Output: true

Type Consistency

CRITICAL: All branches of an if expression must return the same type.

// CORRECT: Both branches return strings
let result = if true { "yes" } else { "no" }

// ERROR: Type mismatch (string vs integer)
// let result = if true { "yes" } else { 42 }

Example: Numeric Results

let discount = 0.15
let price = 100.0

let final_price = if discount > 0 {
  price * (1.0 - discount)
} else {
  price
}

final_price  // Returns: 85.0

Expected Output: 85.0

Nested If Expressions

You can nest if expressions inside each other:

let age = 25
let has_license = true

let can_drive = if age >= 16 {
  if has_license {
    "Yes"
  } else {
    "No - needs license"
  }
} else {
  "No - too young"
}

can_drive  // Returns: "Yes"

Expected Output: "Yes"

Example: Access Control

let is_admin = false
let is_owner = true
let is_active = true

let access = if is_admin {
  "Full access"
} else {
  if is_owner && is_active {
    "Owner access"
  } else {
    "Guest access"
  }
}

access  // Returns: "Owner access"

Expected Output: "Owner access"

Conditions with Logical Operators

Combine multiple conditions using && and ||:

let age = 25
let has_ticket = true
let venue_open = true

let can_enter = if age >= 18 && has_ticket && venue_open {
  "Welcome!"
} else {
  "Entry denied"
}

can_enter  // Returns: "Welcome!"

Expected Output: "Welcome!"

Example: Validation

let username = "alice"
let password = "secret123"

let valid_user = username.len() >= 3 && username.len() <= 20
let valid_pass = password.len() >= 8

let login = if valid_user && valid_pass {
  "Login successful"
} else {
  "Login failed"
}

login  // Returns: "Login successful"

Expected Output: "Login successful"

Block Expressions

If branches can contain multiple statements:

let x = 10

let result = if x > 5 {
  let doubled = x * 2
  let tripled = x * 3
  doubled + tripled  // Last expression is returned
} else {
  0
}

result  // Returns: 50

Expected Output: 50

Example: Multi-Step Calculation

let amount = 1000
let is_premium = true

let final_amount = if is_premium {
  let base_discount = amount * 0.1
  let premium_bonus = amount * 0.05
  amount - base_discount - premium_bonus
} else {
  amount
}

final_amount  // Returns: 850.0

Expected Output: 850.0

Common Patterns

Min/Max Pattern

let a = 42
let b = 17

let max = if a > b { a } else { b }
let min = if a < b { a } else { b }

max  // Returns: 42
min  // Returns: 17

Expected Output: max: 42, min: 17

Clamp Pattern

let value = 150
let min = 0
let max = 100

let clamped = if value < min {
  min
} else if value > max {
  max
} else {
  value
}

clamped  // Returns: 100

Expected Output: 100

Default Value Pattern

let config = load_config()  // Might be null

let timeout = if config != null {
  config.timeout
} else {
  30  // Default timeout
}

timeout

Sign Pattern

let n = -15

let sign = if n > 0 {
  "positive"
} else if n < 0 {
  "negative"
} else {
  "zero"
}

sign  // Returns: "negative"

Expected Output: "negative"

Range Check Pattern

let value = 75
let min = 0
let max = 100

let status = if value < min {
  "Below range"
} else if value > max {
  "Above range"
} else {
  "In range"
}

status  // Returns: "In range"

Expected Output: "In range"

Threshold Pattern

let stock = 15
let threshold = 20

let reorder = if stock < threshold {
  "Reorder needed"
} else {
  "Stock OK"
}

reorder  // Returns: "Reorder needed"

Expected Output: "Reorder needed"

If Without Else

If you don't need an else branch, you can omit it:

let debug = true

if debug {
  "Debug mode enabled"
}

Note: Without else, the expression returns null when condition is false.

Comparing If vs Match

While if-else works for many cases, match is better for multiple discrete values:

// Using if-else
let color = if status == "active" {
  "green"
} else if status == "pending" {
  "yellow"
} else if status == "error" {
  "red"
} else {
  "gray"
}

// Using match (cleaner)
let color = match status {
  "active" => "green",
  "pending" => "yellow",
  "error" => "red",
  _ => "gray"
}

Guard Clauses

Use early returns for validation:

fn process_order(amount, has_stock) {
  // Guard clause: exit early on invalid conditions
  if amount <= 0 {
    return "Invalid amount"
  }

  if !has_stock {
    return "Out of stock"
  }

  // Main logic only runs if guards pass
  "Order processed"
}

Ternary Operator Alternative

Ruchy doesn't have ? :, but if-else is concise:

// Other languages: x = condition ? true_val : false_val

// Ruchy equivalent (actually cleaner)
let x = if condition { true_val } else { false_val }

Example: Toggle

let is_on = true
let new_state = if is_on { false } else { true }

new_state  // Returns: false

Expected Output: false

Empirical Proof

Test File

tests/notebook/test_if_else.rs

Test Coverage

  • Line Coverage: 100% (40/40 lines)
  • Branch Coverage: 100% (20/20 branches)

Mutation Testing

  • Mutation Score: 98% (48/49 mutants caught)

Example Tests

#![allow(unused)]
fn main() {
#[test]
fn test_basic_if() {
    let mut notebook = Notebook::new();

    let code = r#"
        let age = 20
        if age >= 18 {
          "Adult"
        }
    "#;

    let result = notebook.execute_cell(code);
    assert_eq!(result, "\"Adult\"");
}

#[test]
fn test_if_else() {
    let mut notebook = Notebook::new();

    let code = r#"
        let age = 15
        if age >= 18 {
          "Adult"
        } else {
          "Minor"
        }
    "#;

    let result = notebook.execute_cell(code);
    assert_eq!(result, "\"Minor\"");
}

#[test]
fn test_if_else_if_chain() {
    let mut notebook = Notebook::new();

    notebook.execute_cell("let score = 85");

    let code = r#"
        if score >= 90 {
          "A"
        } else if score >= 80 {
          "B"
        } else if score >= 70 {
          "C"
        } else {
          "F"
        }
    "#;

    let result = notebook.execute_cell(code);
    assert_eq!(result, "\"B\"");
}

#[test]
fn test_if_as_expression() {
    let mut notebook = Notebook::new();

    let code = r#"
        let x = 10
        let max = if x > 5 { x } else { 5 }
        max
    "#;

    let result = notebook.execute_cell(code);
    assert_eq!(result, "10");
}

#[test]
fn test_nested_if() {
    let mut notebook = Notebook::new();

    let code = r#"
        let age = 25
        let has_license = true

        if age >= 16 {
          if has_license {
            "Can drive"
          } else {
            "Needs license"
          }
        } else {
          "Too young"
        }
    "#;

    let result = notebook.execute_cell(code);
    assert_eq!(result, "\"Can drive\"");
}
}

Property Tests

#![allow(unused)]
fn main() {
proptest! {
    #[test]
    fn max_returns_larger_value(a: i32, b: i32) {
        let mut notebook = Notebook::new();

        notebook.execute_cell(&format!("let a = {}", a));
        notebook.execute_cell(&format!("let b = {}", b));

        let result = notebook.execute_cell("if a > b { a } else { b }");
        let max_value: i32 = result.parse().unwrap();

        assert!(max_value >= a && max_value >= b);
        assert!(max_value == a || max_value == b);
    }

    #[test]
    fn min_returns_smaller_value(a: i32, b: i32) {
        let mut notebook = Notebook::new();

        notebook.execute_cell(&format!("let a = {}", a));
        notebook.execute_cell(&format!("let b = {}", b));

        let result = notebook.execute_cell("if a < b { a } else { b }");
        let min_value: i32 = result.parse().unwrap();

        assert!(min_value <= a && min_value <= b);
        assert!(min_value == a || min_value == b);
    }

    #[test]
    fn abs_value_always_positive(n: i32) {
        let mut notebook = Notebook::new();

        notebook.execute_cell(&format!("let n = {}", n));

        let result = notebook.execute_cell("if n < 0 { -n } else { n }");
        let abs: i32 = result.parse().unwrap();

        assert!(abs >= 0);
        assert_eq!(abs, n.abs());
    }

    #[test]
    fn clamp_stays_in_range(value: i32, min: i32, max: i32) {
        prop_assume!(min <= max);

        let mut notebook = Notebook::new();

        notebook.execute_cell(&format!("let value = {}", value));
        notebook.execute_cell(&format!("let min = {}", min));
        notebook.execute_cell(&format!("let max = {}", max));

        let code = r#"
            if value < min {
              min
            } else if value > max {
              max
            } else {
              value
            }
        "#;

        let result = notebook.execute_cell(code);
        let clamped: i32 = result.parse().unwrap();

        assert!(clamped >= min);
        assert!(clamped <= max);
    }
}
}

E2E Test

File: tests/e2e/notebook-features.spec.ts

test('If-else expressions work in notebook', async ({ page }) => {
  await page.goto('http://localhost:8000/notebook.html');

  // Basic if
  await testCell(page, 'let age = 20', '');
  await testCell(page, 'if age >= 18 { "Adult" }', '"Adult"');

  // If-else
  await testCell(page, 'let age2 = 15', '');
  await testCell(page, 'if age2 >= 18 { "Adult" } else { "Minor" }', '"Minor"');

  // If-else-if chain
  await testCell(page, 'let score = 85', '');
  await testCell(page, `
    if score >= 90 { "A" }
    else if score >= 80 { "B" }
    else if score >= 70 { "C" }
    else { "F" }
  `, '"B"');

  // If as expression
  await testCell(page, 'let x = 10', '');
  await testCell(page, 'let max = if x > 5 { x } else { 5 }', '');
  await testCell(page, 'max', '10');

  // Nested if
  await testCell(page, 'let has_license = true', '');
  await testCell(page, `
    if age >= 16 {
      if has_license { "Can drive" }
      else { "Needs license" }
    } else {
      "Too young"
    }
  `, '"Can drive"');
});

Status: ✅ Passing on Chrome, Firefox, Safari

Summary

Feature Status: WORKING ✅ Test Coverage: 100% line, 100% branch ✅ Mutation Score: 98% ✅ E2E Tests: Passing

If-else expressions are the foundation of conditional logic in Ruchy. Remember that if is an expression that always returns a value, making it more powerful than traditional if statements.

Key Takeaways:

  • if is an expression, not just a statement
  • All branches must return the same type
  • Use if-else-if chains for multiple conditions
  • Combine with logical operators for complex conditions
  • Consider match for multiple discrete values

← Previous: Logical Operators | Next: Match Expressions →