Control Flow Without break/continue/return

Question: How do I find the first match and stop searching without break?

How do I do early termination search without break in EK9?

How do I return the first matching item from a list in EK9?

How do I search with transformation without break in EK9?

Answer:
For simple first-match, use filter + head 1 in a stream pipeline. For complex search with transformation or context, use a function with unset return and a for loop.

SIMPLE FIRST MATCH: FILTER + HEAD
The basic pattern for finding the first match:
  first <- cat items | filter by predicate | head 1 | collect as List of String
This stops processing after the first match. No break needed.

PATTERN 1: SEARCH WITH TRANSFORMATION
Find the first match and return a TRANSFORMED result, not the match itself:
  findAndTransform()
    -> items as List of String
    <- result as String: String()
    for item in items
      if length item > 5 and ~result?
        result :=? item + " (long)"
The :=? ensures only the first long item gets transformed and assigned.

PATTERN 2: SEARCH WITH INDEX
Find an item AND its position:
  findWithIndex()
    -> items as List of String, target as String
    <- position as Integer: Integer()
    idx <- 0
    for item in items
      if item == target and ~position?
        position :=? idx
      idx: idx + 1
The caller gets the index of the first occurrence, or an unset Integer if not found.

PATTERN 3: MULTI-FIELD SEARCH
Search objects by multiple criteria using a helper predicate:
  isMatch() as pure
    -> item as String
    <- matched as Boolean: item contains "admin" and length item > 7
  found <- cat users | filter by isMatch | head 1 | collect as List of String

KEY INSIGHT
Stream filter + head for simple cases. Function decomposition with :=? for complex ones where you need transformation, context, or multi-step logic.

See Q145 for basic filter + head. See Q125 for head, tail, and skip. See Q89 for stream pipelines. See Q50 for unset return as signal.

defines module qa.without.findmatch

  defines function

    isLong() as pure
      -> item as String
      <- long as Boolean?
      minLongLength <- 5
      long: length item > minLongLength

    findAndTransform()
      -> items as List of String
      <- result as String: String()
      minLongLength <- 5
      for item in items
        if length item > minLongLength and ~result?
          result :=? item + " (long)"

    findWithIndex()
      ->
        items as List of String
        target as String
      <- position as Integer: Integer()
      idx <- 0
      for item in items
        if item == target and ~position?
          position :=? idx
        idx: idx + 1

    containsAdmin() as pure
      -> item as String
      <- matched as Boolean?
      minAdminLength <- 7
      matched: item contains "admin" and length item > minAdminLength

  defines program

    FindFirstDemo()
      stdout <- Stdout()

      items <- ["hi", "apple", "banana", "cherry", "date", "elderberry"]

      // === SIMPLE: FILTER + HEAD ===

      firstLong <- cat items | filter by isLong | head 1 | collect as List of String
      stdout.println(`First long item: ${firstLong}`)

      // === SEARCH WITH TRANSFORMATION ===

      transformed <- findAndTransform(items)
      if transformed?
        stdout.println(`Transformed: ${transformed}`)

      // === SEARCH WITH INDEX ===

      pos <- findWithIndex(items, "cherry")
      if pos?
        stdout.println(`Cherry found at index: ${pos}`)

      notFoundPos <- findWithIndex(items, "mango")
      if ~notFoundPos?
        stdout.println("Mango not found in list")

      // === MULTI-FIELD SEARCH ===

      users <- ["user_joe", "admin_alice", "user_bob", "admin_charlie"]
      admins <- cat users | filter by containsAdmin | head 1 | collect as List of String
      stdout.println(`First admin: ${admins}`)

Java: for (item : list) { if (match) { result = item; break; } } or stream().filter().findFirst(). Python: for item in list: if match: result = item; break, or next(x for x in list if match, None). Rust: iter().find(|x| predicate). Go: for _, item := range list { if match { return item } }. Kotlin: list.firstOrNull { predicate }. EK9: cat items | filter by predicate | head 1 | collect, or function with :=? for complex search.

alternative early filter find first head index linear lookup match no-break no-return search stop termination transform

Explanation: EK9 has no break statement. To find the first match, use guard assignment (:=?) which only assigns when the variable is unset, replacing break-on-found. See ek9 -h E01070 for details.


Control Flow Without break/continue/return

Question: How do I handle multiple preconditions without early return?

How do I validate multiple conditions without return in EK9?

What replaces guard clauses with early return in EK9?

How do I write a validation chain without return statements in EK9?

Answer:
In Java you write: if (!valid1) return error1; if (!valid2) return error2; doWork();. EK9 has no return. Use structured if/else chains or decompose into validator functions.

THE PROBLEM
Early return guards look clean in small functions but create hidden exits. Each return is a path the reader must trace. Cleanup code can be skipped. In large functions, the number of hidden exits becomes unmanageable.

PATTERN 1: NESTED IF/ELSE CHAIN
Check each condition in sequence with all paths visible:
  validate()
    -> name as String, age as Integer, email as String
    <- message as String: "unknown error"
    if length name == 0
      message: "name is required"
    else if age < 0 or age > 150
      message: "age must be between 0 and 150"
    else if not (email contains "@")
      message: "email must contain @"
    else
      message: "valid"
Every path is explicit. No hidden exits.

PATTERN 2: VALIDATOR FUNCTIONS WITH GUARDS
Decompose each check into its own function:
  validateName() as pure
    -> name as String
    <- error as String: String()
    if length name == 0
      error: "name is required"
Then compose with guarded assignment:
  problem <- String()
  problem :=? validateName(name)
  problem :=? validateAge(age)
  problem :=? validateEmail(email)
The :=? means the first error wins. Subsequent checks are still evaluated but their results are ignored once problem is set.

PATTERN 3: VALIDATE THEN PROCESS
Separate validation from processing entirely:
  isValid() as pure
    -> name as String, age as Integer
    <- valid as Boolean: length name > 0 and age >= 0 and age <= 150
Guard on the result:
  if isValid(name, age)
    process(name, age)
  else
    reportError()

KEY INSIGHT
Early return creates hidden exits. Explicit if/else makes ALL paths visible. Decomposition into validator functions keeps each check focused and testable.

See Q50 for declared returns. See Q274 for AI return patterns. See Q146 for decomposition. See Q74 for guard expressions.

defines module qa.without.preconditions

  defines function

    // Pattern 1: Nested if/else chain
    validateAll() as pure
      ->
        name as String
        age as Integer
        email as String
      <- message as String: "unknown error"
      maxAge <- 150
      if length name == 0
        message: "name is required"
      else if age < 0 or age > maxAge
        message: "age must be between 0 and 150"
      else if not (email contains "@")
        message: "email must contain @"
      else
        message: "valid"

    // Pattern 2: Individual validators
    validateName() as pure
      -> name as String
      <- error as String: String()
      if length name == 0
        error: "name is required"

    validateAge() as pure
      -> age as Integer
      <- error as String: String()
      maxAge <- 150
      if age < 0 or age > maxAge
        error: "age must be between 0 and 150"

    validateEmail() as pure
      -> email as String
      <- error as String: String()
      if not (email contains "@")
        error: "email must contain @"

    // Pattern 3: Boolean validator
    isAllValid() as pure
      ->
        name as String
        age as Integer
      <- valid as Boolean?
      maxAge <- 150
      valid: length name > 0 and age >= 0 and age <= maxAge

  defines program

    PreconditionDemo()
      stdout <- Stdout()

      // === PATTERN 1: NESTED IF/ELSE ===

      testName <- "Alice"
      testEmail <- "a@b.com"

      stdout.println(`Empty name: ${validateAll("", 25, testEmail)}`)
      stdout.println(`Bad age: ${validateAll(testName, -5, testEmail)}`)
      stdout.println(`Bad email: ${validateAll(testName, 25, "nope")}`)
      stdout.println(`All valid: ${validateAll(testName, 25, testEmail)}`)

      // === PATTERN 2: COMPOSED VALIDATORS ===

      problem <- String()
      problem :=? validateName(testName)
      problem :=? validateAge(200)
      problem :=? validateEmail(testEmail)
      if problem?
        stdout.println(`Composed error: ${problem}`)

      problem2 <- String()
      problem2 :=? validateName("Bob")
      problem2 :=? validateAge(30)
      problem2 :=? validateEmail("b@c.com")
      if ~problem2?
        stdout.println("Composed: all valid")

      // === PATTERN 3: VALIDATE THEN PROCESS ===

      if isAllValid("Charlie", 40)
        stdout.println("Charlie is valid, processing")
      else
        stdout.println("Charlie is invalid")

      if isAllValid("", 40)
        stdout.println("Empty name is valid")
      else
        stdout.println("Empty name rejected as expected")

Java: if (!valid) return error; guard clauses at top of method. Python: if not valid: return error early return guards. Rust: early return with ? operator for Result, pattern matching for validation. Go: if err := validate(); err != nil { return err } very common pattern. Kotlin: require() and check() for preconditions, early return. Swift: guard let for unwrapping, early return. EK9: no return, nested if/else chains, validator functions with :=? composition, validate-then-process decomposition.

alternative chain check condition early error guard isset migrate multiple no-break no-return null-safe precondition return safe validate validation

validateName() as pure
      -> name as String
      <- error as String: String()
      stdout.println(name)
      if length name == 0
        error: "name is required"

Explanation: A pure function cannot call non-pure methods like stdout.println(). Pure functions must have no side effects. See ek9 -h E50001 for details.


Control Flow Without break/continue/return

Question: How do I accumulate partial results without break?

How do I build a result from the first matching condition without break in EK9?

How does guarded assignment replace break-on-found in EK9?

How do I implement first-wins priority lookup in EK9?

Answer:
In Java you write: result = null; for (...) { if (match) { result = x; break; } }. In EK9, the :=? guarded assignment operator is purpose-built for first-assignment-wins.

THE PROBLEM
You want to find a value from the first matching source in priority order. Other languages use a loop with break-on-found. The break is the mechanism for stopping after the first match.

PATTERN 1: PRIORITY CHAIN WITH :=?
Try each source in priority order. The first one that sets the value wins:
  resolveConfig()
    -> envSetting as String, fileSetting as String, hardcoded as String
    <- setting as String: String()
    setting :=? envSetting
    setting :=? fileSetting
    setting :=? hardcoded
The :=? operator only assigns when the target is unset. Once setting gets a value from envSetting, the subsequent assignments are no-ops. No loop, no break.

PATTERN 2: STREAM COLLECT FOR BATCH
When accumulating ALL matching items (not first-wins), use stream collect:
  matches <- cat items | filter by isValid | collect as List of String
No break needed because you want everything that matches.

PATTERN 3: SWITCH WITH DECLARED RETURN
When dispatching on a single value, switch with a declared return replaces break-based dispatch:
  classify() as pure
    -> score as Integer
    <- label as String: "average"
    switch score
      case >= 90
        label: "excellent"
      case >= 70
        label: "good"
      case < 40
        label: "poor"
      default
        label: "average"

KEY INSIGHT
The :=? operator IS the break-on-found replacement. It was designed specifically for the first-assignment-wins pattern. No loops needed for priority lookup. No break needed for accumulation.

See Q50 for guarded assignment. See Q29 for :=? semantics. See Q79 for guarded assignment details. See Q122 for collect.

defines module qa.without.accumulate

  defines function

    // Pattern 1: Priority chain
    resolveConfig()
      ->
        envSetting as String
        fileSetting as String
        hardcoded as String
      <- setting as String: String()
      setting :=? envSetting
      setting :=? fileSetting
      setting :=? hardcoded

    isValid() as pure
      -> item as String
      <- valid as Boolean?
      minValidLength <- 3
      valid: length item > minValidLength

    // Pattern 3: Switch with declared return
    classify() as pure
      -> score as Integer
      <- label as String: "average"
      switch score
        case >= 90
          label: "excellent"
        case >= 70
          label: "good"
        case < 40
          label: "poor"
        default
          label: "average"

  defines program

    AccumulateDemo()
      stdout <- Stdout()

      // === PATTERN 1: PRIORITY CHAIN ===

      defaultSetting <- "hardcoded"

      // Environment wins (first non-empty source)
      envResult <- resolveConfig("from-env", "from-file", defaultSetting)
      stdout.println(`Priority (env set): ${envResult}`)

      // File wins when env is unset
      fileResult <- resolveConfig(String(), "from-file", defaultSetting)
      stdout.println(`Priority (env unset): ${fileResult}`)

      // Hardcoded wins when both are unset
      defaultResult <- resolveConfig(String(), String(), defaultSetting)
      stdout.println(`Priority (both unset): ${defaultResult}`)

      // === PATTERN 2: STREAM COLLECT ===

      items <- ["hi", "apple", "banana", "ok", "cherry"]
      allValid <- cat items | filter by isValid | collect as List of String
      stdout.println(`All valid items: ${allValid}`)

      // === PATTERN 3: SWITCH DISPATCH ===

      scores <- [95, 72, 55, 38]
      for score in scores
        stdout.println(`Score ${score}: ${classify(score)}`)

      // === FIRST-WINS IN A LOOP ===

      names <- ["", "", "Alice", "Bob"]
      chosen <- String()
      for name in names
        if length name > 0
          chosen :=? name
      stdout.println(`First non-empty name: ${chosen}`)

Java: result = null; for (...) { if (match) { result = x; break; } } or Optional.orElse chain. Python: result = None; for ...: if match: result = x; break, or next() with generator. Rust: iter().find() or match with first arm. Go: for loop with break on found. Kotlin: firstOrNull() or generateSequence. JavaScript: find() or for loop with break. EK9: :=? guarded assignment for priority chain, stream collect for batch, switch for dispatch.

accumulate alternative assignment build collect config error first guarded isset lookup migrate no-break no-return null-safe ok partial priority result safe wins

envResultXYZ <- resolveConfig("from-env", "from-file", defaultSetting)

Explanation: Renaming the variable means later references to 'envResult' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.


Control Flow Without break/continue/return

Question: How do I process items until a condition is met without break?

How do I replace while(true) with break-on-condition in EK9?

How do I consume items until done without break in EK9?

How do I write a processing loop that stops on a condition in EK9?

Answer:
In Java you write: while(true) { item = next(); if (done(item)) break; process(item); }. EK9 replaces this with guard-driven termination: the while loop condition itself controls when to stop.

THE PROBLEM
Processing items until a termination condition is the most common use of while(true) with break. The break is buried inside the loop body, making the termination condition hard to find.

PATTERN 1: WHILE WITH GUARD VARIABLE
Use a Boolean flag that controls the while loop:
  done <- Boolean(false)
  idx <- 0
  while ~done? and idx < length items
    item <- items.getOrDefault(idx, "")
    if item == "STOP"
      done: true
    else
      process(item)
    idx: idx + 1
The termination condition is in the while expression, not buried in the body.

PATTERN 2: COUNTING WITH FOR-RANGE
When processing up to a known maximum:
  for count in 1 ... maxItems
    if ~finished?
      item <- getItem(count)
      if isTerminal(item)
        finished: true
      else
        process(item)

PATTERN 3: STREAM WITH HEAD
When you want the first N items that satisfy a condition:
  results <- cat items | filter by isActive | head 10 | collect as List of String
The pipeline stops after 10 active items. No loop, no break, no counter.

GUARD-DRIVEN VS BREAK-DRIVEN
Break-driven: the loop runs forever, break is the only way out. The termination logic is scattered through the body.
Guard-driven: the while condition states exactly when the loop ends. The body focuses on processing.

See Q66 for while loops. See Q144 for no break. See Q125 for head.

defines module qa.without.processuntil

  defines function

    isActive() as pure
      -> item as String
      <- active as Boolean: item <> "STOP" and length item > 0

    processWithLimit()
      ->
        dataItems as List of String
        readLimit as Integer
      <- itemsRead as Integer: 0

      for pos in 0 ... length dataItems - 1
        if itemsRead < readLimit
          itemsRead: itemsRead + 1

  defines program

    ProcessUntilDemo()
      stdout <- Stdout()

      // === PATTERN 1: WHILE WITH GUARD VARIABLE ===

      items <- ["alpha", "bravo", "charlie", "STOP", "delta", "echo"]
      done <- Boolean(false)
      idx <- 0
      processed <- 0
      while ~done and idx < length items
        item <- items.getOrDefault(idx, "")
        if item == "STOP"
          done: true
        else
          stdout.println(`Processing: ${item}`)
          processed: processed + 1
        idx: idx + 1
      stdout.println(`Processed ${processed} items before STOP`)

      // === PATTERN 2: COUNTING WITH FOR-RANGE ===

      numbers <- [10, 20, 30, 40, 50, 60, 70]
      runningTotal <- 0
      totalLimit <- 100
      exceeded <- Boolean(false)
      for count in 0 ... length numbers - 1
        if ~exceeded
          currentNum <- numbers.getOrDefault(count, 0)
          if runningTotal + currentNum > totalLimit
            exceeded: true
          else
            runningTotal: runningTotal + currentNum
      stdout.println(`Running total (stopped before exceeding 100): ${runningTotal}`)

      // === PATTERN 3: STREAM WITH HEAD ===

      allItems <- ["", "alpha", "bravo", "", "charlie", "STOP", "delta", "echo", "foxtrot"]
      activeItems <- cat allItems | filter by isActive | head 3 | collect as List of String
      stdout.println(`First 3 active items: ${activeItems}`)

      // === BOUNDED PROCESSING WITH INDEX ===

      dataItems <- ["read1", "read2", "read3", "read4", "read5"]
      itemsRead <- processWithLimit(dataItems, 3)
      stdout.println(`Bounded read processed ${itemsRead} items`)

Java: while(true) { if (done) break; } or do-while with condition. Python: while True: if done: break. Rust: loop { if done { break; } } or while !done. Go: for { if done { break } }. Kotlin: while(true) { if (done) break }. JavaScript: while(true) { if (done) break; }. EK9: while with guard expression or flag variable, for-range for bounded processing, stream head for take-first-N.

alternative condition consume done exhaust guard isset loop migrate no-break no-return null-safe process safe stop terminate until while

Explanation: EK9 has no break statement. To stop processing when a condition is met, set a guard flag variable and check it in the while condition. See ek9 -h E01070 for details.


Control Flow Without break/continue/return

Question: How do I convert break-based loops from Java or Python to EK9?

What are before and after examples for converting break loops to EK9?

How do I migrate a for loop with break to EK9 idioms?

What are the EK9 equivalents of common break loop patterns?

Answer:
This shows four common break-based loop patterns and their EK9 equivalents side by side.

SCENARIO 1: FIND FIRST X
Java: for (String s : items) { if (s.length() > 5) { result = s; break; } }
Python: for s in items: if len(s) > 5: result = s; break
EK9 equivalent uses filter + head:
  results <- cat items | filter by isLong | head 1 | collect as List of String
The pipeline stops after the first long item. No loop variable, no break.

SCENARIO 2: PROCESS FIRST N MATCHING
Java: int count = 0; for (String s : items) { if (isValid(s)) { process(s); if (++count >= 3) break; } }
EK9 equivalent uses filter + head N:
  topN <- cat items | filter by isValid | head 3 | collect as List of String
  for item in topN
    process(item)
The stream selects, the loop processes. Each part has one responsibility.

SCENARIO 3: ACCUMULATE UNTIL LIMIT
Java: int sum = 0; for (int n : numbers) { sum += n; if (sum > 100) break; }
EK9 equivalent uses a guard flag:
  total <- 0
  exceeded <- Boolean(false)
  for num in numbers
    if ~exceeded
      if total + num > 100
        exceeded: true
      else
        total: total + num
The flag replaces break. The termination condition is checked at loop entry.

SCENARIO 4: SKIP ITEMS THEN TAKE SOME
Java: boolean started = false; int taken = 0; for (String s : items) { if (!started) { if (s.equals("START")) started = true; continue; } process(s); if (++taken >= 2) break; }
EK9 equivalent uses skip + head:
  selected <- cat items | skip 2 | head 3 | collect as List of String
For conditional skip, use filter to remove unwanted prefix items.

THE PATTERN
Every break loop decomposes into: what do I want? Express the want as a stream pipeline or a guard-controlled loop.

See Q145 for replacing break and continue. See Q148 for migration rules. See Q275 for AI break patterns.

defines module qa.without.migratebreak

  defines function

    isLong() as pure
      -> item as String
      <- long as Boolean?
      minLongLength <- 5
      long: length item > minLongLength

    isValid() as pure
      -> item as String
      <- valid as Boolean?
      minValidLength <- 2
      valid: length item > minValidLength

    formatItem() as pure
      -> item as String
      <- formatted as String: `[${item}]`

  defines program

    MigrateBreakDemo()
      stdout <- Stdout()

      items <- ["hi", "apple", "banana", "fig", "cherry", "date", "elderberry"]

      // === SCENARIO 1: FIND FIRST LONG ITEM ===
      // Java: for (s : items) { if (s.length() > 5) { result = s; break; } }

      firstLong <- cat items | filter by isLong | head 1 | collect as List of String
      stdout.println(`First long item: ${firstLong}`)

      // === SCENARIO 2: PROCESS FIRST 3 VALID ===
      // Java: count = 0; for (s : items) { if (valid(s)) { process(s); if (++count >= 3) break; } }

      topThree <- cat items | filter by isValid | head 3 | collect as List of String
      for item in topThree
        stdout.println(`Processing valid: ${formatItem(item)}`)

      // === SCENARIO 3: ACCUMULATE UNTIL LIMIT ===
      // Java: sum = 0; for (n : numbers) { sum += n; if (sum > 100) break; }

      numbers <- [10, 25, 30, 45, 50, 60]
      total <- 0
      totalLimit <- 100
      exceeded <- Boolean(false)
      for num in numbers
        if ~exceeded
          if total + num > totalLimit
            exceeded: true
          else
            total: total + num
      stdout.println(`Accumulated total (under 100): ${total}`)

      // === SCENARIO 4: SKIP ITEMS THEN TAKE SOME ===
      // Java: started = false; taken = 0; for (s : items) { if (!started) { continue; } process(s); if (++taken >= 2) break; }

      selected <- cat items | skip 2 | head 3 | collect as List of String
      stdout.println(`Skipped 2, took 3: ${selected}`)

      // === ALL SCENARIOS COMBINED ===

      fruits <- ["fig", "apple", "banana", "cherry", "date", "elderberry", "grape"]
      longFruits <- cat fruits | filter by isLong | skip 1 | head 2 | collect as List of String
      stdout.println(`Skip 1 long, take 2: ${longFruits}`)

Java: for loop with break on condition, counter + break for first-N, accumulator + break on limit. Python: for with break, enumerate + break, itertools.islice as alternative. Rust: for with break, iter().take(n).filter(), iter().scan() for accumulation. Go: for with break, manual counter. Kotlin: for with break, take() and filter() on sequences. JavaScript: for with break, Array.find(), Array.some(). EK9: filter + head for selection, guard variables for accumulation, skip + head for windowing.

after alternative before break convert imperative loop migrate migration no-break no-return refactor side stream

Explanation: EK9 has no break statement. When migrating accumulate-until-limit loops from Java or Python, replace break with a Boolean flag checked in the loop condition. See ek9 -h E01070 for details.


Control Flow Without break/continue/return

Question: How do I convert functions with multiple return statements to EK9?

What are before and after examples for converting multiple returns to EK9?

How do I migrate early return guard clauses to EK9?

How do I replace ternary chains and multi-return functions in EK9?

Answer:
This shows four common multi-return patterns and their EK9 equivalents side by side.

SCENARIO 1: GUARD CLAUSE RETURNS
Java: if (name == null || name.isEmpty()) return "invalid"; if (name.length() > 50) return "too long"; return "ok";
EK9 equivalent uses if/else with declared return:
  validateName() as pure
    -> name as String
    <- status as String: "ok"
    if length name == 0
      status: "invalid"
    else if length name > 50
      status: "too long"
The declared return starts with a default. Conditions modify it. Every path is visible.

SCENARIO 2: LOOKUP WITH DEFAULT
Java: for (String s : items) { if (s.equals(target)) return s; } return "not found";
EK9 equivalent uses unset return + for loop + :=? default:
  lookup()
    -> items as List of String, target as String
    <- found as String: String()
    for item in items
      if item == target and ~found?
        found :=? item
The caller checks if found? and uses a default if not. No loop break, no return inside loop.

SCENARIO 3: MULTI-BRANCH CLASSIFICATION
Java: if (score >= 90) return "A"; if (score >= 80) return "B"; if (score >= 70) return "C"; return "F";
EK9 equivalent uses declared return with conditional assignment:
  grade() as pure
    -> score as Integer
    <- letter as String: "F"
    if score >= 90
      letter: "A"
    else if score >= 80
      letter: "B"
    else if score >= 70
      letter: "C"

SCENARIO 4: TERNARY CHAIN
Java: return a > 0 ? "positive" : a < 0 ? "negative" : "zero";
EK9 equivalent uses :=? chain:
  describe() as pure
    -> num as Integer
    <- label as String?
    if num > 0
      label :=? "positive"
    else if num < 0
      label :=? "negative"
    else
      label :=? "zero"
Each branch assigns exactly once via :=?. The compiler verifies all paths set the value.

THE PATTERN
Every multi-return function becomes: declare the return variable with a sensible default, then use if/else to modify it. The compiler verifies every path initialises the return.

See Q50 for declared returns. See Q146 for decomposition. See Q274 for AI return patterns. See Q148 for migration.

defines module qa.without.migratereturn

  defines function

    // Scenario 1: Guard clause returns
    validateName() as pure
      -> name as String
      <- status as String: "ok"
      maxNameLength <- 50
      if length name == 0
        status: "invalid"
      else if length name > maxNameLength
        status: "too long"

    // Scenario 2: Lookup with default
    lookup()
      ->
        items as List of String
        target as String
      <- found as String: String()
      for item in items
        if item == target and ~found?
          found :=? item

    // Scenario 3: Multi-branch classification
    grade() as pure
      -> score as Integer
      <- letter as String: "F"
      excellentMinScore <- 90
      goodMinScore <- 80
      averageMinScore <- 70
      if score >= excellentMinScore
        letter: "A"
      else if score >= goodMinScore
        letter: "B"
      else if score >= averageMinScore
        letter: "C"

    // Scenario 4: Ternary chain
    describeSign() as pure
      -> num as Integer
      <- label as String?
      if num > 0
        label :=? "positive"
      else if num < 0
        label :=? "negative"
      else
        label :=? "zero"

  defines program

    MigrateReturnDemo()
      stdout <- Stdout()

      // === SCENARIO 1: GUARD CLAUSE RETURNS ===

      stdout.println(`Empty name: ${validateName("")}`)
      stdout.println(`Long name: ${validateName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")}`)
      stdout.println(`Valid name: ${validateName("Alice")}`)

      // === SCENARIO 2: LOOKUP WITH DEFAULT ===

      fruits <- ["apple", "banana", "cherry"]
      if result <- lookup(fruits, "banana")
        stdout.println(`Found: ${result}`)

      missing <- lookup(fruits, "mango")
      if ~missing?
        stdout.println("Mango not found, using default")

      // === SCENARIO 3: MULTI-BRANCH CLASSIFICATION ===

      scores <- [95, 85, 75, 55]
      for score in scores
        stdout.println(`Score ${score}: grade ${grade(score)}`)

      // === SCENARIO 4: TERNARY CHAIN ===

      numbers <- [42, -7, 0]
      for num in numbers
        stdout.println(`${num} is ${describeSign(num)}`)

Java: return in every branch, ternary operator for simple cases, guard clauses at top. Python: return in each branch, or pattern for defaults. Rust: implicit return from last expression in each branch, or early return with ?. Go: return in each branch, error return pattern. Kotlin: return or when expression. Swift: return or switch expression. EK9: declared return variable, if/else modifies it, :=? for pure functions, compiler verifies all paths.

after alternative before classification convert declared function guard isset migrate multiple named no-break no-return null-safe refactor return safe ternary

grade() as pure
      -> score as Integer
      <- letter as String: "F"
      stdout.println(score)

Explanation: A pure function cannot call non-pure methods like stdout.println(). Pure functions must be free of side effects. See ek9 -h E50001 for details.


Question: What variable names are banned in EK9?

Which identifiers are forbidden as variable names in EK9?

What names trigger E11031 or E11032 in EK9?

What are the reserved and restricted variable names in EK9?

Answer:
EK9 enforces naming quality at compile time across four tiers of restriction. No other mainstream language does this. Every banned name has a specific reason and error code.

TIER 1: NON-DESCRIPTIVE NAMES (E11031)
The following names are rejected because they carry no semantic meaning: temp, tmp, flag, flg, data, dat, object, obj, value, val, buffer, buf. The check is case-insensitive, so Data, DATA, and data are all rejected. Research shows these names correlate with 2.7x higher defect density.

OPERATOR KEYWORDS (E11032)
The following names shadow built-in operators and are rejected: empty, length, contains, abs, sqrt, close, matches. Using these as variable names would make the corresponding operator unusable in scope.

RESERVED WORDS (E01060)
The following are reserved for the EK9 testing framework: assert, require, assertThrows, assertDoesNotThrow. These are not available as variable or type names.

EXCLUDED KEYWORDS (E01070-E01080)
Keywords from other languages that do not exist in EK9: break, continue, return, null, goto, new, def, elif, None, self. These are excluded by design to prevent confusion when migrating from other languages.

SINGLE-CHARACTER EXEMPTION
Single-character names are always allowed: x, y, z, i, j, k, T, K, V. The compiler trusts these as intentional shorthand for mathematical variables, loop counters, and generic type parameters.

See Q291 for troubleshooting rejected names. See Q296 for research evidence behind these bans. See Q297 for a renaming guide. See Q22 for variable declarations. See Q248 for error code lookup. See Q311 for full quality checks catalog.

defines module qa.naming.banned

  defines function

    calculateArea() as pure
      ->
        width as Float
        height as Float
      <- area as Float: width * height

    formatGreeting() as pure
      -> customerName as String
      <- greeting as String: "Hello, " + customerName

    isRetryNeeded() as pure
      -> attemptCount as Integer
      <- shouldRetry as Boolean?

      maxAttempts <- 3
      shouldRetry :=? attemptCount < maxAttempts

  defines program

    BannedNamesDemo()
      stdout <- Stdout()

      // === SINGLE-CHARACTER NAMES: ALWAYS ALLOWED ===

      x <- 10.0
      y <- 20.0
      z <- calculateArea(x, y)
      stdout.println(`Area: ${z}`)

      // === DESCRIPTIVE CAMELCASE: ALWAYS ALLOWED ===

      customerName <- "Alice"
      orderCount <- 5
      isActive <- true
      stdout.println(`Customer: ${customerName}, Orders: ${orderCount}, Active: ${isActive}`)

      // === COMPOUND NAMES: ALWAYS ALLOWED ===

      totalPrice <- 99.95
      retryAttempt <- 1
      outputMessage <- formatGreeting(customerName)
      stdout.println(`${outputMessage}, Price: ${totalPrice}`)

      // === GOOD ALTERNATIVES TO BANNED NAMES ===

      swapHolder <- x
      isComplete <- true
      sensorReading <- 42.5
      currentItem <- "widget"
      computedScore <- 88
      readChunk <- "packet contents"
      stdout.println(`Swap: ${swapHolder}, Done: ${isComplete}`)
      stdout.println(`Sensor: ${sensorReading}, Item: ${currentItem}`)
      stdout.println(`Score: ${computedScore}, Chunk: ${readChunk}`)

      // === GOOD ALTERNATIVES TO OPERATOR KEYWORD NAMES ===

      isBlank <- length customerName == 0
      nameLength <- length customerName
      targetItem <- 2
      hasItem <- [1, 2, 3] contains targetItem
      positiveAmount <- 42
      needsRetry <- isRetryNeeded(retryAttempt)
      stdout.println(`Blank: ${isBlank}, Length: ${nameLength}`)
      stdout.println(`Has item: ${hasItem}, Amount: ${positiveAmount}`)
      stdout.println(`Retry: ${needsRetry}`)

Java: allows any valid identifier, relies on linters like Checkstyle or SonarQube for naming rules. Python: allows any valid identifier, PEP 8 naming conventions are voluntary. Rust: allows any identifier, clippy provides optional naming warnings. Go: allows any identifier, golint suggests naming conventions. C/C++: allows any non-keyword identifier, no naming enforcement. Kotlin: allows any identifier, detekt provides optional rules. JavaScript: allows any valid identifier, ESLint rules are optional. EK9: compiler enforces naming quality with four tiers of restriction, no configuration needed, cannot be disabled.

E01060 E11031 E11032 banned compile convention error forbidden identifier migrate name naming reserved restricted variable

banned names
reserved names
naming rules

Explanation: The name 'data' is non-descriptive and banned by the compiler. Use a name that describes what the variable represents, such as 'sensorReading'. See ek9 -h E11031 for details.

Explanation: The name 'empty' shadows a built-in operator keyword and is rejected. Use a descriptive alternative like 'isBlank'. See ek9 -h E11032 for details.

Explanation: The name 'value' is non-descriptive and banned by the compiler. Use a name that communicates what the value represents. See ek9 -h E11031 for details.


Question: Why does the compiler reject my variable name?

How do I fix E11031 non-descriptive variable name error?

How do I fix E11032 operator keyword as variable name error?

What should I rename my rejected variable to in EK9?

Answer:
EK9 rejects variable names that are non-descriptive or shadow operators. Each error code tells you exactly what category of restriction you hit.

E11031: NON-DESCRIPTIVE VARIABLE NAME
You used one of: temp, tmp, flag, flg, data, dat, object, obj, value, val, buffer, buf. The check is case-insensitive. Fix: describe what the variable represents. Instead of temp use swapHolder or intermediateResult. Instead of flag use isComplete or shouldRetry. Instead of data use customerRecord or sensorReading. Instead of value use totalPrice or measuredWeight. Instead of buffer use outputBuilder or readChunk.

E11032: OPERATOR KEYWORD AS VARIABLE NAME
You used one of: empty, length, contains, abs, sqrt, close, matches. These shadow built-in operators. Fix: add context. Instead of empty use isBlank or emptyBasket. Instead of length use nameLength or pathSize. Instead of contains use hasItem or includesKey. Instead of matches use foundItems or searchHits. Instead of abs use absoluteDistance. Instead of close use closeFile or shutdownConnection.

E01060: RESERVED WORD AS IDENTIFIER
You used assert, require, assertThrows, or assertDoesNotThrow. These are reserved for the testing framework. Fix: use testResult, precondition, or validationCheck instead.

E01070-E01080: EXCLUDED KEYWORD
You used break, continue, return, null, goto, new, def, elif, None, or self. These keywords do not exist in EK9. Fix: these are not just naming issues, they indicate you are thinking in another language. See Q144 for why EK9 has no break, continue, or return.

QUICK FIX TABLE
  temp/tmp -> swapHolder, intermediateResult, pendingEntry
  flag/flg -> isComplete, hasPermission, shouldRetry
  data/dat -> customerRecord, sensorReading, responsePayload
  object/obj -> currentItem, targetEntity, parsedElement
  value/val -> totalPrice, measuredWeight, userInput
  buffer/buf -> outputBuilder, messageAccumulator, readChunk
  empty -> isBlank, emptyBasket, noResults
  length -> nameLength, pathSize, messageCount
  contains -> hasItem, includesKey, foundMatch
  matches -> foundItems, matchingRecords, searchHits

See Q290 for the complete banned list. See Q297 for a comprehensive renaming guide. See Q248 for all error codes.

defines module qa.naming.troubleshoot

  defines function

    processOrder() as pure
      ->
        orderTotal as Float
        customerName as String
      <- receiptMessage as String: `Order for ${customerName}: ${orderTotal}`

    calculateDiscount() as pure
      ->
        originalPrice as Float
        discountRate as Float
      <- discountedPrice as Float: originalPrice * (1.0 - discountRate)

    retryUntilDone()
      -> maxAttempts as Integer
      <- attemptCount as Integer: 0

      isRetryNeeded <- Boolean(true)
      while isRetryNeeded and attemptCount < maxAttempts
        attemptCount++
        if attemptCount >= maxAttempts
          isRetryNeeded: false

  defines program

    TroubleshootNamingDemo()
      stdout <- Stdout()

      // === FIXED E11031: temp -> swapHolder ===

      firstNumber <- 10
      secondNumber <- 20
      swapHolder <- firstNumber
      firstNumber := secondNumber
      secondNumber := swapHolder
      stdout.println(`After swap: ${firstNumber}, ${secondNumber}`)

      // === FIXED E11031: flag -> isRetryNeeded ===

      attemptCount <- retryUntilDone(3)
      stdout.println(`Attempts: ${attemptCount}`)

      // === FIXED E11031: data -> customerRecord ===

      customerRecord <- "Alice:Premium:2024"
      stdout.println(`Record: ${customerRecord}`)

      // === FIXED E11032: empty -> emptyBasket ===

      emptyBasket <- List() of String
      stdout.println(`Empty basket: ${emptyBasket}`)

      // === FIXED E11032: length -> nameLength ===

      greeting <- "Hello, World"
      nameLength <- length greeting
      stdout.println(`Name length: ${nameLength}`)

      // === COMBINED EXAMPLE: GOOD NAMING THROUGHOUT ===

      originalPrice <- 100.0
      discountRate <- 0.15
      finalPrice <- calculateDiscount(originalPrice, discountRate)
      receiptMessage <- processOrder(finalPrice, "Alice")
      stdout.println(receiptMessage)

Java: no compile-time naming enforcement, Checkstyle rules are optional and configurable. Python: no naming enforcement, PEP 8 is advisory. Rust: no naming enforcement beyond snake_case warnings. Go: no naming enforcement, golint is advisory. C/C++: no naming enforcement. Kotlin: no naming enforcement. JavaScript: ESLint naming rules are optional. EK9: compiler enforces naming at compile time, cannot be disabled, four tiers of restrictions with specific error codes.

E01060 E11031 E11032 banned compile compiler convention error fix identifier naming reject rename troubleshoot variable why

Explanation: The name 'temp' is non-descriptive and banned by the compiler. Use 'swapHolder' or another name that describes the variable's purpose. See ek9 -h E11031 for details.

Explanation: The name 'length' shadows a built-in operator keyword and is rejected. Use a descriptive alternative like 'nameLength'. See ek9 -h E11032 for details.

Explanation: The name 'data' is non-descriptive and banned. Use a name like 'customerRecord' that describes what the data represents. See ek9 -h E11031 for details.


Question: What are the naming conventions for variables in EK9?

What style should variable names follow in EK9?

What naming pattern does EK9 use for fields and parameters?

Answer:
EK9 uses camelCase for all variable names: local variables, fields, parameters, and return values. The compiler enforces descriptive naming, so every variable name should communicate its purpose.

LOCAL VARIABLES
Use camelCase with descriptive intent: customerName, orderCount, isActive, totalPrice. Infer types with <- for concise declarations. Avoid abbreviations: use customer not cust, message not msg.

FIELDS
Same as local variables. Fields are private by default and must be initialised inline: greeting as String: "Hello". Use descriptive names that describe the data, not the type: accountBalance not floatBalance.

PARAMETERS
Parameters require explicit types. Use names that describe what the parameter represents: -> firstName as String, not -> s as String. For multiple parameters, each on its own line under ->.

RETURN VALUES
Return values are named and describe the output: <- formattedName as String. The name documents what the function produces. Choose names like calculatedTotal, validatedInput, formattedOutput.

BOOLEAN NAMING
Use is/has/can/should prefixes: isActive, hasPermission, canEdit, shouldRetry. These make conditions read naturally: if isActive, while hasPermission.

COLLECTION NAMING
Use plural nouns: customers, orderItems, errorMessages. This distinguishes collections from single items and makes for-in loops read naturally: for customer in customers.

AVOID
Abbreviations (use customer not cust), Hungarian notation (not strName), type-in-name (not nameString), generic names (not item1, item2).

See Q22 for variable declarations. See Q293 for type naming. See Q294 for function naming.

defines module qa.naming.variables

  defines class

    CustomerOrder
      customerName as String: String()
      orderItems as List of String: List() of String
      totalPrice as Float: 0.0
      isComplete as Boolean: false

      CustomerOrder()
        ->
          name as String
          items as List of String
          price as Float
        this.customerName :=: name
        this.orderItems :=: items
        this.totalPrice :=: price

      getCustomerName() as pure
        <- rtn as String: customerName

      getOrderItems() as pure
        <- rtn as List of String: orderItems

      getTotalPrice() as pure
        <- rtn as Float: totalPrice

      markComplete()
        isComplete: true

      hasItems() as pure
        <- rtn as Boolean: length orderItems > 0

      operator $ as pure
        <- rtn as String: `Order(${customerName}, ${length orderItems} items, ${totalPrice})`

      override operator ? as pure
        <- rtn as Boolean: customerName? and totalPrice?

  defines function

    formatOrderSummary() as pure
      ->
        customerName as String
        itemCount as Integer
        totalPrice as Float
      <- formattedSummary as String: `${customerName}: ${itemCount} items, total ${totalPrice}`

    isHighValueOrder() as pure
      -> orderTotal as Float
      <- isHighValue as Boolean?

      highValueThreshold <- 500.0
      isHighValue :=? orderTotal > highValueThreshold

  defines program

    VariableConventionsDemo()
      stdout <- Stdout()

      // === LOCAL VARIABLES: camelCase, descriptive ===

      customerName <- "Alice"
      orderCount <- 3
      isActive <- true
      totalPrice <- 299.95
      stdout.println(`Customer: ${customerName}, Orders: ${orderCount}`)
      stdout.println(`Active: ${isActive}, Total: ${totalPrice}`)

      // === BOOLEAN NAMING: is/has/can/should prefixes ===

      hasPermission <- true
      canEdit <- isActive and hasPermission
      maxRetries <- 5
      shouldRetry <- orderCount < maxRetries
      stdout.println(`Can edit: ${canEdit}, Should retry: ${shouldRetry}`)

      // === COLLECTION NAMING: plural nouns ===

      orderItems <- ["Widget", "Gadget", "Sprocket"]
      errorMessages <- List() of String
      stdout.println(`Items: ${orderItems}`)
      stdout.println(`Errors: ${errorMessages}`)

      // === FIELDS AND METHODS: descriptive throughout ===

      order <- CustomerOrder(customerName, orderItems, totalPrice)
      stdout.println($order)
      stdout.println(`Has items: ${order.hasItems()}`)

      // === RETURN VALUES: describe the output ===

      formattedSummary <- formatOrderSummary(customerName, length orderItems, totalPrice)
      stdout.println(formattedSummary)

      // === HIGH VALUE CHECK ===

      isExpensive <- isHighValueOrder(totalPrice)
      stdout.println(`High value: ${isExpensive}`)

Java: camelCase by convention (not enforced), fields often prefixed with m_ in Android. Python: snake_case for variables and functions (PEP 8), not enforced. Rust: snake_case for variables and functions (enforced as warning). Go: camelCase, exported names start with uppercase. C/C++: no standard, varies by project (camelCase, snake_case, Hungarian). Kotlin: camelCase by convention, similar to Java. JavaScript: camelCase by convention, not enforced. Swift: camelCase for variables. EK9: camelCase enforced by convention, descriptive naming enforced by compiler, no abbreviations or generic names allowed.

boolean camelCase collection convention descriptive field identifier naming parameter pattern style variable

naming convention
variable naming
code style

obj <- true
      canEdit <- isActive and obj

Explanation: The name 'obj' is non-descriptive and banned by the compiler. Use a name that describes what the variable represents. See ek9 -h E11031 for details.

editSummary <- `Can edit: ${canEdit}, Should retry: ${shouldRetry}`

Explanation: If 'canEdit' is declared but never used in any output or expression, the compiler rejects it as an unused variable. See ek9 -h E50050 for details.


Question: What are the naming conventions for types in EK9?

How should I name classes, records, and traits in EK9?

How do I name modules and generic type parameters in EK9?

Answer:
EK9 uses PascalCase for all type names: classes, records, traits, enumerations, and functions used as types. Module names use lowercase dot-separated segments.

CLASSES
PascalCase nouns describing what the class represents: CustomerOrder, PaymentProcessor, EmailValidator. Classes model entities or processors.

RECORDS
PascalCase nouns describing the data structure: Coordinate, Temperature, HttpResponse. Records are data-focused types with public fields, constructors, and operators (but no methods). Records are mutable — use 'as pure' to control mutation.

TRAITS
PascalCase adjectives or capability names: Printable, Sortable, Configurable. Traits describe what a type can do, not what it is.

ENUMERATIONS
PascalCase singular nouns: Colour, DayOfWeek, HttpStatus. Each value represents one instance of the concept.

FUNCTIONS AS TYPES
PascalCase verb phrases when used as abstract function types: FormatName, CalculateTotal, ValidateInput. Standalone functions use camelCase (see Q294).

GENERIC TYPE PARAMETERS
Single uppercase letters: T for general type, K for key, V for value, E for element. These are universally understood conventions.

MODULES
Lowercase dot-separated: com.example.banking, org.ek9.core. Module names follow reverse domain convention.

CONSTRAINED TYPES
Named for the constraint they represent: EmailAddress as String constrain, PositiveInteger as Integer constrain. The name describes what the constraint enforces.

See Q292 for variable naming. See Q294 for function naming. See Q93 for classes. See Q97 for records.

defines module qa.naming.types

  defines trait

    Printable
      printTo() as pure abstract
        -> target as String
        <- formattedOutput as String?

  defines record

    Coordinate
      xPosition as Float: 0.0
      yPosition as Float: 0.0

      Coordinate()
        ->
          initialX as Float
          initialY as Float
        this.xPosition :=: initialX
        this.yPosition :=: initialY

      operator $ as pure
        <- rtn as String: `(${xPosition}, ${yPosition})`

      operator <=> as pure
        -> other as Coordinate
        <- rtn as Integer: xPosition <=> other.xPosition

      override operator ? as pure
        <- rtn as Boolean: xPosition? and yPosition?

  defines class

    ShapeRenderer with trait of Printable
      shapeName as String: String()

      ShapeRenderer()
        -> name as String
        this.shapeName :=: name

      override printTo() as pure
        -> target as String
        <- formattedOutput as String: `${shapeName} -> ${target}`

      operator $ as pure
        <- rtn as String: `Renderer(${shapeName})`

      override operator ? as pure
        <- rtn as Boolean: shapeName?

  defines type

    DayOfWeek
      Monday
      Tuesday
      Wednesday
      Thursday
      Friday
      Saturday
      Sunday

  defines function

    FormatCoordinate() as pure abstract
      -> point as Coordinate
      <- formattedPoint as String?

  defines program

    TypeConventionsDemo()
      stdout <- Stdout()

      // === RECORD: PascalCase noun ===

      origin <- Coordinate(0.0, 0.0)
      destination <- Coordinate(3.5, 7.2)
      stdout.println(`Origin: ${origin}`)
      stdout.println(`Destination: ${destination}`)

      // === CLASS WITH TRAIT: PascalCase ===

      renderer <- ShapeRenderer("Circle")
      renderedOutput <- renderer.printTo("Canvas")
      stdout.println(renderedOutput)

      // === ENUMERATION: PascalCase singular ===

      today <- DayOfWeek.Wednesday
      stdout.println(`Today: ${today}`)

      // === GENERIC TYPE PARAMETER CONVENTION ===

      names <- ["Alice", "Bob", "Charlie"]
      for n in names
        stdout.println(`Name: ${n}`)

      // === FUNCTION TYPE: PascalCase verb phrase ===

      formatter <- (capturedOrigin: origin) is FormatCoordinate as pure function
        formattedPoint :=? `Point at ${point} from ${capturedOrigin}`

      stdout.println(formatter(destination))

Java: PascalCase for classes (convention, not enforced), package names lowercase. Python: PascalCase for classes (PEP 8, not enforced), snake_case for modules. Rust: PascalCase for types (enforced as warning), snake_case for modules. Go: PascalCase for exported types, lowercase for unexported. C++: no standard, varies widely. Kotlin: PascalCase for classes (convention), package names lowercase. Swift: PascalCase for types (convention). EK9: PascalCase for all types, lowercase dot-separated for modules, single uppercase for generics, descriptive naming enforced by compiler.

PascalCase class constrained convention enumeration function generic identifier module naming record trait type

Explanation: The method is named 'printTo' not 'render'. Calling a method that does not exist on the type produces a resolution error. See ek9 -h E50060 for details.

Explanation: If the variable 'today' is declared but never referenced in any expression, the compiler rejects it as unused. See ek9 -h E50050 for details.


Question: What are the naming conventions for functions in EK9?

How should I name functions and methods in EK9?

What naming style do EK9 programs and services use?

How do I name standalone functions versus methods in EK9?

Answer:
EK9 uses camelCase for standalone functions and methods, and PascalCase for programs, services, and abstract function types.

STANDALONE FUNCTIONS
camelCase, verb-first when performing an action: calculateTotal, formatDate, validateEmail. The name describes what the function does.

ABSTRACT FUNCTIONS
PascalCase because they serve as type contracts: Predicate, Comparator, Transformer, FormatName. Abstract functions define the signature that concrete implementations must follow.

METHODS
camelCase, verb-first: getName, setStatus, calculateDiscount, processPayment. Methods describe operations on the object.

PURE FUNCTIONS
Same camelCase naming. The 'as pure' modifier communicates purity; the name communicates intent. Use verbs that suggest computation: compute, calculate, format, validate, derive.

OPERATORS
Use standard operator symbols, not names: operator +, operator ==, operator $. Operator implementations do not need naming conventions since they follow the fixed operator set.

PROGRAMS
PascalCase nouns: HelloWorld, InventoryManager, DataMigrator. Programs are entry points and named like types.

SERVICES
PascalCase nouns: UserService, AuthenticationService, OrderProcessor. Services define REST endpoints and are named for their domain.

See Q292 for variable naming. See Q293 for type naming. See Q49 for function basics. See Q96 for operator overloading.

defines module qa.naming.functions

  defines function

    calculateTotal() as pure
      ->
        unitPrice as Float
        quantity as Integer
      <- totalPrice as Float: unitPrice * Float(quantity)

    formatCurrency() as pure
      -> amount as Float
      <- formattedAmount as String: "USD " + $amount

    isEligibleForDiscount() as pure
      -> orderTotal as Float
      <- isEligible as Boolean?

      discountThreshold <- 100.0
      isEligible :=? orderTotal > discountThreshold

    ValidateInput() as pure abstract
      -> userInput as String
      <- isValid as Boolean?

  defines class

    OrderProcessor
      orderName as String: String()
      orderTotal as Float: 0.0

      OrderProcessor()
        ->
          name as String
          total as Float
        this.orderName :=: name
        this.orderTotal :=: total

      getOrderName() as pure
        <- rtn as String: orderName

      getOrderTotal() as pure
        <- rtn as Float: orderTotal

      applyDiscount()
        -> discountRate as Float
        orderTotal: orderTotal * (1.0 - discountRate)

      formatReceipt() as pure
        <- receipt as String: `Receipt: ${orderName} - ${formatCurrency(orderTotal)}`

      operator $ as pure
        <- rtn as String: `Order(${orderName}, ${orderTotal})`

      override operator ? as pure
        <- rtn as Boolean: orderName? and orderTotal?

  defines program

    FunctionConventionsDemo()
      stdout <- Stdout()

      // === STANDALONE FUNCTIONS: camelCase, verb-first ===

      totalPrice <- calculateTotal(29.99, 3)
      formattedPrice <- formatCurrency(totalPrice)
      stdout.println(`Total: ${formattedPrice}`)

      // === PURE FUNCTION: same camelCase ===

      isEligible <- isEligibleForDiscount(totalPrice)
      stdout.println(`Eligible for discount: ${isEligible}`)

      // === METHODS: camelCase, verb-first ===

      processor <- OrderProcessor("Laptop Bundle", totalPrice)
      stdout.println(processor.formatReceipt())
      processor.applyDiscount(0.10)
      stdout.println(processor.formatReceipt())

      // === ABSTRACT FUNCTION TYPE: PascalCase ===

      emailValidator <- () is ValidateInput as pure function
        isValid :=? userInput contains "@"

      testEmail <- "alice@example.com"
      stdout.println(`Valid email: ${emailValidator(testEmail)}`)

Java: camelCase for methods (convention), PascalCase for classes, no standalone functions. Python: snake_case for functions and methods (PEP 8). Rust: snake_case for functions (enforced as warning). Go: camelCase for functions, PascalCase for exported. C++: no standard naming convention. Kotlin: camelCase for functions, PascalCase for classes. Swift: camelCase for functions and methods. JavaScript: camelCase for functions (convention). EK9: camelCase for functions and methods, PascalCase for programs, services, and abstract function types.

PascalCase camelCase convention function identifier immutable method naming operator program pure service side-effect verb

Explanation: The method is named 'applyDiscount' not 'setDiscount'. Calling a method that does not exist on the type produces a resolution error. See ek9 -h E50060 for details.


Question: Can I use single-letter variable names in EK9?

Are single-character variable names allowed in EK9?

Can I use i, j, k as loop counters in EK9?

Why does EK9 allow single-letter names but ban two-letter names like tmp?

Answer:
Yes. Single-character variable names are always allowed in EK9. The compiler exempts them because mathematical conventions and loop counters are universally understood.

ALWAYS FINE
Mathematical variables: x, y, z for coordinates and equations. Loop counters: i, j, k for indices. Generic type parameters: T, K, V, U, S, E for parameterised types.

ACCEPTABLE
Short lambdas and inline functions where the type makes the meaning clear from context. When iterating a typed collection, a single letter can be sufficient: for c in customers.

BETTER TO AVOID
When the meaning is not immediately clear from surrounding context. In a long function body, prefer descriptive names even for loop variables: for customerIndex in 0 ... length customers.

WHY SINGLE-CHAR IS ALLOWED
The compiler trusts single-character names as intentional. A developer who writes 'x' knows exactly what they mean in context. The danger starts with two-plus character names that LOOK meaningful but carry no information: temp, data, obj, val. These create false confidence that the name communicates something.

KEY INSIGHT
Single-letter names are honest about being short. Names like 'data' pretend to be descriptive but tell you nothing about what data they hold. EK9 bans the pretenders but trusts the honest ones.

COMPARISON
Python and Java allow any name and rely on linters. Rust warns on non-snake-case but allows any identifier. EK9 enforces at compile time but trusts single-character names as intentional shorthand.

See Q290 for banned names. See Q292 for variable conventions. See Q80 for for-range with counters.

defines module qa.naming.singleletter

  defines function

    calculateDistance() as pure
      ->
        x as Float
        y as Float
      <- d as Float: sqrt (x * x + y * y)

    computeQuadratic() as pure
      ->
        a as Float
        b as Float
        c as Float
        x as Float
      <- y as Float: a * x * x + b * x + c

  defines program

    SingleLetterDemo()
      stdout <- Stdout()

      // === MATH VARIABLES: x, y, z ===

      x <- 3.0
      y <- 4.0
      z <- calculateDistance(x, y)
      stdout.println(`Distance from (${x}, ${y}): ${z}`)

      // === QUADRATIC: a, b, c, x ===

      a <- 1.0
      b <- -3.0
      c <- 2.0
      result <- computeQuadratic(a: a, b: b, c: c, x: x)
      stdout.println(`f(${x}) = ${result}`)

      // === LOOP COUNTERS: i ===

      total <- for i in 1 ... 5
        <- rtn <- 0
        rtn: rtn + i
      stdout.println(`Sum 1-5: ${total}`)

      // === NESTED LOOPS: i, j ===

      grid <- for i in 1 ... 3
        <- rtn <- ""
        row <- for j in 1 ... 3
          <- inner <- ""
          if length inner > 0
            inner: inner + ","
          inner: inner + $(i * j)
        if length rtn > 0
          rtn: rtn + " | "
        rtn: rtn + row
      stdout.println(`Grid: ${grid}`)

      // === COLLECTION ITERATION: descriptive preferred ===

      names <- ["Alice", "Bob", "Charlie"]
      for n in names
        stdout.println(`Hello, ${n}`)

      // === COMPARISON: short vs descriptive ===

      customerNames <- ["Alice", "Bob", "Charlie"]
      for customerName in customerNames
        stdout.println(`Customer: ${customerName}`)

Java: any identifier allowed, no special treatment for single-char names, relies on IDE inspections. Python: any identifier allowed, PEP 8 advises against single-letter except for counters. Rust: any identifier allowed, clippy has no single-char restriction. Go: single-letter names are idiomatic for short-lived variables. C/C++: any identifier allowed, single-letter names common in math code. Kotlin: any identifier allowed. JavaScript: any identifier allowed. EK9: single-character names explicitly exempted from naming rules, compiler trusts them as intentional shorthand while banning multi-character generic names.

allowed character convention counter exempt generic identifier letter loop math migrate naming parameter short single type variable

Explanation: Renaming the variable means later references to 'z' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.


Question: Why does EK9 ban common names that every other language allows?

What evidence supports banning variable names like temp and data?

Why is EK9 so strict about variable naming?

What disasters were caused by poor variable naming?

Answer:
EK9 bans non-descriptive names because decades of research and catastrophic real-world failures demonstrate that naming quality is a safety mechanism, not a style preference.

THE RESEARCH EVIDENCE
Lawrie et al. (IEEE 2006) found non-descriptive names increase code comprehension time by 19-31%. Butler et al. (ICPC 2010) showed generic variable names correlate with 2.7x higher defect density. Hofmeister et al. (MSR 2017) found variables with generic names appear in 3.2x more bug reports. Code reviews consistently identify naming as the top maintainability issue.

REAL-WORLD DISASTERS
Mars Climate Orbiter (1999, $125M lost): variables named with unclear units caused a metric/imperial conversion failure. Therac-25 (1985-87, 6 deaths): generic flag variables masked race conditions in radiation therapy software. Toyota Unintended Acceleration (2009-11, 89 deaths): NASA review found non-descriptive variables in throttle control code. Ariane 5 (1996, $370M lost): variable reuse with unclear naming contributed to a type conversion overflow.

WHY OTHER LANGUAGES DO NOT DO THIS
Historical backward compatibility prevents Java, Python, and C++ from adding naming restrictions. The linter-based approach (Checkstyle, ESLint, pylint) treats naming as optional and configurable. The culture of minimal compiler enforcement assumes developers will self-police.

WHY EK9 CAN DO THIS
EK9 is a new language with no legacy codebases to break. The compiler is the right enforcement point because it cannot be misconfigured, skipped, or disabled. Every developer on every project gets the same naming quality.

THE PHILOSOPHY
Naming is not a style choice. It is a safety mechanism. Self-documenting code prevents bugs. The cost is 2 seconds of thought per variable. The benefit is preventing the next $125M naming disaster.

See Q290 for the banned list. See Q297 for a renaming guide. See Q144 for similar evidence-based design philosophy.

defines module qa.naming.evidence

  defines function

    convertTemperature() as pure
      -> fahrenheitReading as Float
      <- celsiusReading as Float: (fahrenheitReading - 32.0) * 5.0 / 9.0

    calculateFuelEfficiency() as pure
      ->
        distanceTravelled as Float
        fuelConsumed as Float
      <- kilometresPerLitre as Float: Float()
      if fuelConsumed > 0.0
        kilometresPerLitre: distanceTravelled / fuelConsumed

    isWithinTolerance() as pure
      ->
        measuredPressure as Float
        targetPressure as Float
        tolerancePercent as Float
      <- withinRange as Boolean: false
      allowedDeviation <- targetPressure * (tolerancePercent / 100.0)
      lowerBound <- targetPressure - allowedDeviation
      upperBound <- targetPressure + allowedDeviation
      withinRange: measuredPressure >= lowerBound and measuredPressure <= upperBound

  defines program

    NamingEvidenceDemo()
      stdout <- Stdout()

      // === UNIT CLARITY: Prevents Mars Climate Orbiter-style failures ===

      fahrenheitReading <- 212.0
      celsiusReading <- convertTemperature(fahrenheitReading)
      stdout.println(`${fahrenheitReading}F = ${celsiusReading}C`)

      // === DESCRIPTIVE CALCULATIONS: No ambiguity about what is measured ===

      distanceTravelled <- 450.0
      fuelConsumed <- 35.0
      kilometresPerLitre <- calculateFuelEfficiency(distanceTravelled, fuelConsumed)
      stdout.println(`Efficiency: ${kilometresPerLitre} km/L`)

      // === MEANINGFUL FLAGS: No generic flag variables ===

      measuredPressure <- 14.5
      targetPressure <- 14.7
      tolerancePercent <- 5.0
      pressureOk <- isWithinTolerance(measuredPressure, targetPressure, tolerancePercent)
      stdout.println(`Pressure within tolerance: ${pressureOk}`)

      // === EVERY NAME TELLS ITS STORY ===

      sensorReadings <- [14.2, 14.5, 14.8, 15.1, 14.6]
      withinCount <- 0
      for reading in sensorReadings
        if isWithinTolerance(reading, targetPressure, tolerancePercent)
          withinCount++
      stdout.println(`${withinCount} of ${length sensorReadings} readings within tolerance`)

Java: relies on Checkstyle, SonarQube, and code reviews for naming quality, all optional and configurable. Python: PEP 8 is advisory, linters like pylint flag poor names as warnings. Rust: clippy provides some naming guidance but nothing about descriptive quality. Go: golint is advisory, no enforcement of descriptive naming. C/C++: no naming enforcement, relies entirely on code reviews. Kotlin: detekt provides optional naming rules. JavaScript: ESLint can flag naming issues but rules are optional. EK9: compiler enforces descriptive naming at compile time based on research evidence, cannot be disabled or configured away.

Mars Therac Toyota ban bug convention defect disaster evidence identifier migrate naming opinionated philosophy research safety why

Explanation: The name 'temp' is non-descriptive and banned. Using 'fahrenheitReading' prevents Mars Climate Orbiter-style unit confusion. See ek9 -h E11031 for details.

Explanation: The name 'flag' is non-descriptive and banned. Use a name like 'pressureOk' that describes what the boolean represents. See ek9 -h E11031 for details.


Question: What should I name my variable instead of temp or data?

How do I rename temp, data, flag, and value in EK9?

What are good alternatives to banned variable names in EK9?

What descriptive name should I use instead of obj or buf?

Answer:
Ask yourself: what does this variable REPRESENT? The answer to that question IS the name. Here are specific alternatives for every banned name.

RENAMING TIER 1 NAMES (E11031)
  temp/tmp: swapHolder, intermediateResult, pendingEntry, stagingArea
  flag/flg: isComplete, hasPermission, shouldRetry, needsUpdate
  data/dat: customerRecord, sensorReading, responsePayload, configSettings
  object/obj: currentItem, targetEntity, parsedElement, selectedWidget
  value/val: totalPrice, measuredWeight, userInput, computedScore
  buffer/buf: outputBuilder, messageAccumulator, readChunk, uploadContent

RENAMING OPERATOR KEYWORDS (E11032)
  empty: isBlank, emptyBasket, noResults, clearedQueue
  length: nameLength, pathSize, messageCount, arrayExtent
  contains: hasItem, includesKey, foundMatch, holdingEntry
  abs: absoluteDistance, magnitude, positiveAmount
  sqrt: squareRoot, rootApproximation
  close: closeFile, shutdownConnection, terminateSession
  matches: foundItems, matchingRecords, searchHits, filteredResults

THE TECHNIQUE
When you reach for a generic name, pause and ask three questions: 1. What kind of thing is this? (a price, a name, a sensor reading) 2. Where did it come from? (user input, database query, calculation) 3. What will it be used for? (display, comparison, accumulation). Any of these answers produces a better name than temp or data.

See Q290 for the complete banned list. See Q296 for why these names are banned. See Q292 for naming conventions.

defines module qa.naming.renameguide

  defines function

    performSwap() as pure
      ->
        firstItem as String
        secondItem as String
      <-
        swappedPair as String: `${secondItem}, ${firstItem}`

    isWidgetItem()
      -> item as String
      <- isMatch as Boolean?

      widgetLabel <- "Widget"
      isMatch: item contains widgetLabel

    retryConnection()
      -> maxAttempts as Integer
      <- connectionAttempts as Integer: 0

      isRetryNeeded <- Boolean(true)
      while isRetryNeeded and connectionAttempts < maxAttempts
        connectionAttempts++
        if connectionAttempts >= maxAttempts
          isRetryNeeded: false

  defines program

    RenamingGuideDemo()
      stdout <- Stdout()

      // === SCENARIO 1: SWAP (temp -> swapHolder) ===

      firstPrice <- 100.0
      secondPrice <- 200.0
      swapHolder <- firstPrice
      firstPrice := secondPrice
      secondPrice := swapHolder
      stdout.println(`Swapped: ${firstPrice}, ${secondPrice}`)

      // === SCENARIO 2: CONDITION (flag -> isRetryNeeded) ===

      connectionAttempts <- retryConnection(3)
      stdout.println(`Connected after ${connectionAttempts} attempts`)

      // === SCENARIO 3: PROCESSING (data -> customerOrders) ===

      customerOrders <- ["Laptop", "Mouse", "Keyboard"]
      orderSummary <- ""
      for orderItem in customerOrders
        if length orderSummary > 0
          orderSummary: orderSummary + ", "
        orderSummary: orderSummary + orderItem
      stdout.println(`Orders: ${orderSummary}`)

      // === SCENARIO 4: COLLECTION STATE (empty -> emptyBasket) ===

      shoppingBasket <- List() of String
      emptyBasket <- length shoppingBasket == 0
      stdout.println(`Basket empty: ${emptyBasket}`)

      widgetName <- "Widget"
      shoppingBasket += widgetName
      hasItems <- length shoppingBasket > 0
      stdout.println(`Has items: ${hasItems}`)

      // === SCENARIO 5: STRING MEASUREMENT (length -> nameLength) ===

      customerName <- "Alice Wonderland"
      nameLength <- length customerName
      longNameThreshold <- 10
      isLongName <- nameLength > longNameThreshold
      stdout.println(`Name: ${customerName}, Length: ${nameLength}, Long: ${isLongName}`)

      // === SCENARIO 6: SEARCH RESULTS (matches -> searchHits) ===

      inventory <- ["Widget", "Gadget", "Widget Pro", "Sprocket"]
      searchTerm <- widgetName
      searchHits <- List() of String
      for inventoryItem in inventory
        if inventoryItem contains searchTerm
          searchHits += inventoryItem
      stdout.println(`Found ${length searchHits} items matching '${searchTerm}'`)

Java: rename via IDE refactoring (IntelliJ Shift+F6), no compile-time naming enforcement. Python: rename manually or with rope refactoring, no naming enforcement. Rust: rename via rust-analyzer, no naming enforcement for descriptive quality. Go: rename via gopls, no naming enforcement. C/C++: rename via clangd or manual find-replace, no naming enforcement. Kotlin: rename via IntelliJ, no naming enforcement. JavaScript: rename via IDE, ESLint naming rules are optional. EK9: compiler forces you to think about naming at the point of declaration, alternatives shown in error messages.

alternative buffer convention data descriptive flag guide identifier instead migrate naming object practical rename replace temp value

Explanation: The name 'temp' is non-descriptive and banned. Use 'swapHolder' to describe the variable's role in the swap operation. See ek9 -h E11031 for details.

Explanation: The name 'empty' shadows a built-in operator keyword and is rejected. Use a descriptive alternative like 'emptyBasket'. See ek9 -h E11032 for details.

Explanation: The name 'matches' shadows a built-in operator keyword and is rejected. Use a descriptive alternative like 'searchHits'. See ek9 -h E11032 for details.


Question: What is allow only on traits in EK9?

How does EK9 restrict which classes can implement a trait?

What is the EK9 equivalent of Java sealed interfaces?

Answer:
The 'allow only' clause on a trait creates a sealed trait, restricting which classes may implement it. Only the classes explicitly listed can use 'with trait of' to implement the trait.

SEALED TRAIT SYNTAX
Declare the permitted classes after 'allow only':
  Shape allow only Circle, Square, Triangle
    area() as abstract
      <- rtn as Float?

TRANSITIVE ENFORCEMENT
The restriction is checked transitively. If a class acquires the sealed trait through an intermediate trait or abstract class chain, it must still be in the 'allow only' list.

ABSTRACT EXEMPTION
Abstract classes are exempt from the check because they cannot be instantiated. The sealed set controls concrete runtime types only.

DYNAMIC CLASSES
Dynamic classes (anonymous classes created inline) are always rejected because their generated names cannot appear in an 'allow only' list.

See Q299 for how 'allow only' works with dispatchers. See Q300 for why abstract classes cannot be in the list. See Q607 for why sealed classes need 'as open'. See Q608 for sealed classes vs sealed traits. See Q609 for cross-module sealed enforcement.

defines module qa.sealedtraits.allowonly

  defines trait

    Shape allow only Circle, Square, Triangle
      area() as abstract
        <- rtn as Float?

  defines class

    Circle with trait of Shape
      override area()
        <- rtn as Float: 3.14

    Square with trait of Shape
      override area()
        <- rtn as Float: 1.0

    Triangle with trait of Shape
      override area()
        <- rtn as Float: 0.5

  defines function

    testSealedTrait()
      circle <- Circle()
      square <- Square()
      triangle <- Triangle()
      require circle?
      require square?
      require triangle?

Java: sealed interfaces with 'permits' clause (Java 17+). Kotlin: sealed interfaces restrict implementations to same package/module. Rust: no sealed traits, but orphan rule prevents external trait implementations. Swift: no direct equivalent, uses access control. EK9: 'allow only' provides explicit listing of permitted concrete implementations.

abstract allow allow-only anonymous capture class closed closure dynamic exhaustive migrate only permit restrict sealed trait transitive

Explanation: Renaming the variable means later references to 'circle' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Circle with trait of Shape
      area()
        <- rtn as Float: 3.14

Explanation: When implementing a trait's abstract method, 'override' is required. Omitting it triggers E05120 because the method is inherited from the trait. See ek9 -h E05120 for details.

Explanation: Only classes explicitly listed in the 'allow only' clause may implement the sealed trait. Adding Hexagon without listing it in 'Shape allow only Circle, Square, Triangle' triggers E02030. See ek9 -h E02030 for details.


Question: How does allow only work with dispatchers in EK9?

Does EK9 check exhaustive dispatch on sealed traits?

How do I get exhaustive matching in EK9?

What is the EK9 equivalent of Java sealed switch?

Answer:
When a dispatcher's parameter type is a sealed trait (one with 'allow only'), the compiler requires handlers for all permitted types. This provides exhaustive matching, similar to Java's sealed switch or Kotlin's sealed when.

EXHAUSTIVE DISPATCH
A dispatcher on a sealed trait must have a handler for every type in the 'allow only' list:
  ShapeProcessor
    process() as dispatcher
      -> shape as Shape
    process()
      -> circle as Circle
    process()
      -> square as Square
    process()
      -> triangle as Triangle

COMPILE-TIME SAFETY
When you add a new type to the 'allow only' list, every dispatcher that operates on that trait will fail to compile until a handler is added. This guarantees all dispatch points are updated.

UNSEALED TRAITS
Dispatchers on unsealed traits (without 'allow only') do not require exhaustive handlers. The entry point acts as a catch-all fallback.

See Q298 for 'allow only' basics. See Q105 for dispatcher patterns. See Q618 for sealed exhaustive dispatch deep dive.

defines module qa.sealedtraits.dispatchers

  defines trait

    Shape allow only Circle, Square, Triangle
      area() as abstract
        <- rtn as Float?

  defines class

    Circle with trait of Shape
      override area()
        <- rtn as Float: 3.14

    Square with trait of Shape
      override area()
        <- rtn as Float: 1.0

    Triangle with trait of Shape
      override area()
        <- rtn as Float: 0.5

    ShapeProcessor

      process() as dispatcher
        -> shape as Shape
        content <- "Unknown shape"
        require content?

      process()
        -> circle as Circle
        content <- "Circle"
        require content?

      process()
        -> square as Square
        content <- "Square"
        require content?

      process()
        -> triangle as Triangle
        content <- "Triangle"
        require content?

  defines function

    testDispatchers()
      processor <- ShapeProcessor()
      require processor?

Java: sealed interfaces + switch expressions with exhaustive pattern matching (Java 21+). Kotlin: sealed classes/interfaces + when expression with exhaustive checking. Rust: enum variants + match expression with exhaustive checking. Scala: sealed traits + match expression. EK9: dispatchers on sealed traits with compile-time exhaustiveness checking.

allow allow-only closed complete dispatch dispatcher exhaustive handler match migrate missing only permit sealed switch

Explanation: Implementing a trait's abstract method requires the 'override' keyword. Omitting it causes E05120 because the compiler sees an inherited method being redefined without acknowledgement. See ek9 -h E05120 for details.

Explanation: Renaming the variable means later references to 'processor' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Explanation: A class not in the 'allow only' list cannot implement the sealed trait. Hexagon is not listed in 'Shape allow only Circle, Square, Triangle' so it triggers E02030. See ek9 -h E02030 for details.


Question: Can abstract classes be in an allow only list in EK9?

Why can't abstract classes be in allow only?

What happens if I put an abstract class in allow only?

Does allow only work with abstract classes?

Answer:
No, abstract classes cannot appear in a trait's 'allow only' list. The list defines the exhaustive set of concrete runtime types that may implement the trait. Since abstract classes cannot be instantiated, listing them is meaningless.

ABSTRACT CLASSES ARE EXEMPT
Abstract classes can still implement sealed traits freely. They are exempt from the 'allow only' enforcement check because they cannot create runtime instances:
  AbstractHelper with trait of SealedTrait as abstract  //OK

BUT NOT IN THE LIST
The 'allow only' list should contain only concrete classes:
  SealedTrait allow only ConcreteA, ConcreteB  //CORRECT
  SealedTrait allow only AbstractBase, ConcreteA  //ERROR: E05250

TRANSITIVE CHECKING
Concrete subclasses of an abstract helper class that implements a sealed trait will be checked transitively. They must be in the 'allow only' list or get E05240.

See Q298 for 'allow only' basics. See Q299 for dispatcher exhaustiveness. See Q607 for why sealed classes must be open.

defines module qa.sealedtraits.abstract.list

  defines trait

    Workable allow only ConcreteAlpha, ConcreteBeta
      doWork() as abstract

  defines class

    //AbstractHelper implements the sealed trait - allowed (abstract exempt)
    AbstractHelper with trait of Workable as abstract
      override doWork()
        content <- "Abstract"
        require content?

    //ConcreteAlpha is in the allow only list - allowed
    ConcreteAlpha with trait of Workable
      override doWork()
        content <- "Alpha"
        require content?

    //ConcreteBeta is in the allow only list - allowed
    ConcreteBeta is AbstractHelper
      override doWork()
        content <- "Beta"
        require content?

  defines function

    testAbstractExemption()
      alpha <- ConcreteAlpha()
      beta <- ConcreteBeta()
      require alpha?
      require beta?

Java: sealed interfaces allow abstract classes in permits clause (different design choice). Kotlin: sealed hierarchy allows abstract intermediaries. Rust: no sealed traits, enums are always concrete. EK9: 'allow only' list is strictly for concrete types, abstract classes can implement freely but cannot be listed.

abstract allow allow-only class closed concrete exempt exhaustive instantiate list only permit runtime sealed trait

ConcreteAlpha with trait of Workable
      doWork()

Explanation: Implementing an abstract method from a trait requires 'override'. Without it the compiler reports E05120 because the method signature is inherited. See ek9 -h E05120 for details.

Explanation: A concrete class extending AbstractHelper inherits the sealed trait Workable. If that concrete class is not in the 'allow only' list, the compiler rejects it because the sealed trait restricts which concrete types may implement it. See ek9 -h E50010 for details.

Explanation: ConcreteGamma is not in the 'allow only' list for the sealed trait Workable. Only ConcreteAlpha and ConcreteBeta are permitted to implement it. Adding an unlisted concrete class triggers E02030. See ek9 -h E02030 for details.


Question: How do I restrict which classes can extend my class in EK9?

Can I use allow only on a class instead of a trait?

Answer:
EK9 supports 'allow only' on both traits AND classes. A sealed class must be declared 'as open' (since EK9 classes are closed by default) and lists the permitted subclasses.

SEALED CLASS SYNTAX
Declare the class as open with the permitted subclasses:
  Shape as open allow only Circle, Square, Triangle
    area()
      <- rtn as Float: 0.0

KEY DIFFERENCE FROM TRAITS
Classes are closed by default, so 'as open' is required. Traits are always open, so they don't need it.

CHAINED SEALING
A permitted subclass can itself be sealed:
  Vehicle as open allow only Car, Truck
  Car extends Vehicle as open allow only Sedan, Hatchback

This is identical to Java 17's sealed classes with 'permits' and Kotlin's sealed classes.

See Q298 for sealed traits, Q299 for dispatchers with sealed types.

defines module qa.sealedclasses.allowonly

  defines class

    Shape allow only Circle, Square, Triangle as open
      area()
        <- rtn as Float: 0.0

    Circle extends Shape
      override area()
        <- rtn as Float: 3.14

    Square extends Shape
      override area()
        <- rtn as Float: 1.0

    Triangle extends Shape
      override area()
        <- rtn as Float: 0.5

  defines function

    testSealedClass()
      circle <- Circle()
      square <- Square()
      triangle <- Triangle()
      require circle?
      require square?
      require triangle?

Java: sealed classes with 'permits' clause (Java 17+). Kotlin: sealed classes restrict subclasses to same file. Rust: no sealed classes, but enum variants provide similar exhaustiveness. Swift: no direct equivalent. EK9: 'allow only' on classes provides explicit listing of permitted subclasses.

allow allow-only class closed exhaustive extend migrate only open permit restrict sealed subclass

Explanation: EK9 classes are closed by default. Without 'as open', no class can extend Shape. The compiler reports E05030 because the type is not open for extension. See ek9 -h E05030 for details.

Circle extends Shape
      area()
        <- rtn as Float: 3.14

Explanation: Overriding a parent class method requires the 'override' keyword. Omitting it triggers E05120 because the method is inherited from Shape. See ek9 -h E05120 for details.

Explanation: Pentagon is not in Shape's 'allow only' list. Attempting to extend a sealed class with an unlisted type is rejected by the compiler. See ek9 -h E50010 for details.

Pentagon extends Shape
      override area()
        <- rtn as Float: 1.72

Explanation: Pentagon is not listed in 'Shape allow only Circle, Square, Triangle'. Extending a sealed class with a class not in the 'allow only' list triggers E50010 at PRE_IR_CHECKS. See ek9 -h E50010 for details.


Question: How do dispatchers work with sealed classes in EK9?

Can I use dispatchers with sealed classes?

Does dispatcher exhaustiveness work for sealed classes?

Answer:
Dispatchers work identically with sealed classes and sealed traits. When a dispatcher's parameter type is a sealed class (one with 'allow only'), the compiler requires handlers for ALL permitted subclasses.

DISPATCHER ON SEALED CLASS
  ShapeProcessor
    process() as dispatcher
      -> shape as Shape
      <- rtn as String: "Unknown"
    process()
      -> circle as Circle
      <- rtn as String: "Circle"
    process()
      -> square as Square
      <- rtn as String: "Square"

If any permitted type is missing a handler, the compiler reports E05260.

This provides compile-time exhaustive pattern matching, ensuring that when a new subclass is added to the 'allow only' list, all dispatchers are updated.

See Q299 for dispatchers with sealed traits, Q301 for sealed class basics. See Q618 for sealed exhaustive dispatch deep dive.

defines module qa.sealedclasses.dispatcher

  defines class

    Shape allow only Circle, Square as open
      name()
        <- rtn as String: "Shape"

    Circle extends Shape
      override name()
        <- rtn as String: "Circle"

    Square extends Shape
      override name()
        <- rtn as String: "Square"

    ShapeProcessor

      process() as dispatcher
        -> shape as Shape
        <- rtn as String: "Unknown"

      process()
        -> circle as Circle
        <- rtn as String: "Circle"

      process()
        -> square as Square
        <- rtn as String: "Square"

  defines function

    testDispatcher()
      processor <- ShapeProcessor()
      circleResult <- processor.process(Circle())
      squareResult <- processor.process(Square())
      require circleResult?
      require squareResult?

Java: exhaustive switch on sealed classes (Java 21+). Kotlin: exhaustive when expressions on sealed classes. Rust: exhaustive match on enums. EK9: dispatcher exhaustiveness on sealed classes provides the same compile-time guarantee.

allow allow-only class closed dispatch dispatcher exhaustive handler matching only pattern permit sealed

Explanation: Classes are closed by default in EK9. Without 'as open', Circle and Square cannot extend Shape. The compiler reports E05030 because the type is not open for extension. See ek9 -h E05030 for details.

Circle extends Shape
      name()
        <- rtn as String: "Circle"

Explanation: Overriding a method inherited from Shape requires the 'override' keyword. Without it the compiler reports E05120. See ek9 -h E05120 for details.

Triangle extends Shape
      override name()
        <- rtn as String: "Triangle"

Explanation: Triangle is not in 'Shape allow only Circle, Square'. A class not in the sealed class's 'allow only' list cannot extend it. Adding Triangle without updating the list triggers E50010. See ek9 -h E50010 for details.

Oracle can generate a dispatcher with sealed class type matching.


Question: Can a permitted subclass also be sealed in EK9?

How do I create a multi-level sealed hierarchy?

Answer:
Yes! A permitted subclass can itself be sealed, creating a chained hierarchy. Enforcement is transitive: all descendants must appear in every ancestor's allow only list.

CHAINED SEALING EXAMPLE
  Vehicle allow only Car, Truck, Sedan, Hatchback as open
  Car extends Vehicle allow only Sedan, Hatchback as open
  Sedan extends Car
  Hatchback extends Car
  Truck extends Vehicle

TRANSITIVE ENFORCEMENT
Sedan and Hatchback extend Car, but Car extends Vehicle. Because Vehicle is sealed, Sedan and Hatchback must also appear in Vehicle's allow only list. This is transitive: every sealed ancestor must explicitly permit the descendant.

ENFORCEMENT AT EACH LEVEL
- Vehicle permits: Car, Truck, Sedan, Hatchback
- Car permits: Sedan, Hatchback only
- Truck is not sealed (any class could extend it if it were 'as open')

This works with both classes and traits. Each sealed level provides its own exhaustive set.

See Q301 for sealed class basics, Q298 for sealed traits.

defines module qa.sealedclasses.chained

  defines class

    Vehicle allow only Car, Truck, Sedan, Hatchback as open
      describe()
        <- rtn as String: "Vehicle"

    Car extends Vehicle allow only Sedan, Hatchback as open
      override describe()
        <- rtn as String: "Car"

    Sedan extends Car
      override describe()
        <- rtn as String: "Sedan"

    Hatchback extends Car
      override describe()
        <- rtn as String: "Hatchback"

    Truck extends Vehicle
      override describe()
        <- rtn as String: "Truck"

  defines function

    testChainedSealing()
      sedan <- Sedan()
      hatchback <- Hatchback()
      truck <- Truck()
      require sedan?
      require hatchback?
      require truck?

Java: sealed class hierarchies with permits at each level (Java 17+). Kotlin: sealed classes in same file, nested sealed hierarchies supported. EK9: chained allow only provides the same hierarchical sealing.

allow allow-only chained class closed exhaustive extend hierarchy level nested only permit sealed

Explanation: EK9 classes are closed by default. Without 'as open', no subclass can extend Vehicle. The compiler reports E05030 because the type is not open for extension. See ek9 -h E05030 for details.

Explanation: Sedan and Hatchback extend Car which extends Vehicle. Because Vehicle is sealed, all concrete descendants including Sedan and Hatchback must appear in Vehicle's 'allow only' list. Omitting them triggers E05240. See ek9 -h E05240 for details.

Car extends Vehicle allow only Sedan, Hatchback as open
      describe()
        <- rtn as String: "Car"

Explanation: Overriding the describe() method inherited from Vehicle requires the 'override' keyword. Omitting it triggers E05120. See ek9 -h E05120 for details.

SUV extends Vehicle
      override describe()
        <- rtn as String: "SUV"

Explanation: SUV is not in 'Vehicle allow only Car, Truck, Sedan, Hatchback'. A concrete class must be listed in every sealed ancestor's 'allow only' list. Adding SUV without updating Vehicle's list triggers E50010. See ek9 -h E50010 for details.


Question: What is the require statement and when should I use it?

What is the EK9 equivalent of assert or panic?

How do I validate method arguments in EK9?

Answer:
The require statement validates preconditions in production code. When a require condition is false or unset, it throws an uncatchable exception that terminates execution immediately. This is similar to panic in Go or Rust.

KEY CHARACTERISTICS
- Uncatchable: require failures cannot be caught with try/catch. There is no recovery.
- Always active: unlike Java assert which can be disabled, EK9 require is always checked.
- For programming errors: use require when a failure means the caller has a bug.
- Fail-fast: detects contract violations immediately rather than allowing corrupt state.

WHEN TO USE REQUIRE
Use require for:
- Null/unset parameters when the method cannot accept unset values.
- Collection must not be empty (require not items.empty()).
- Method called in wrong object state (require status == OrderStatus.Created).
- Internal invariants that should never be violated.

WHEN NOT TO USE REQUIRE
Do not use require for:
- User input validation (use catchable exceptions or Result instead).
- File not found or network timeout (these are recoverable).
- Any condition that could reasonably fail due to external factors.

REQUIRE VS THROW
require: For programming errors and contract violations. Uncatchable. Similar to panic.
throw: For exceptional but potentially recoverable situations. Catchable with try/catch.

See Q134 for try/catch. See Q139 for error handling strategy. See Q305 for how require differs from assert. See Q136 for throwing exceptions.

defines module qa.errorhandling.preconditions

  defines type

    OrderStatus
      Created
      Submitted
      Shipped

  defines class

    <?-
      Demonstrates require for precondition checking.
      Require validates that callers honour the contract.
    -?>
    Order
      items as List of String: List() of String
      status as OrderStatus: OrderStatus.Created

      <?-
        Constructor with preconditions.
        The caller must provide valid, non-empty data.
      -?>
      Order()
        ->
          customerId as String
          initialItems as List of String
        require customerId?
        require initialItems?
        require not initialItems.empty()
        this.items :=: initialItems

      <?-
        Can only add items while order is in Created state.
        Calling this after submit is a programming error.
      -?>
      addItem()
        -> item as String
        require item?
        require status == OrderStatus.Created
        items += item

      <?-
        Submit the order. Must have items and be in Created state.
      -?>
      submit()
        require not items.empty()
        require status == OrderStatus.Created
        status: OrderStatus.Submitted

      <?-
        Ship the order. Must be in Submitted state.
      -?>
      ship()
        require status == OrderStatus.Submitted
        status: OrderStatus.Shipped

      itemCount() as pure
        <- rtn as Integer: length items

      override operator ? as pure
        <- rtn as Boolean: items? and status?

      operator $ as pure
        <- rtn as String: `Order[${status}, ${itemCount()} items]`

  defines class

    <?-
      Demonstrates require for validating arguments
      in a utility class.
    -?>
    PriceCalculator

      <?-
        Calculate total price. Both arguments must be set and valid.
      -?>
      calculateTotal()
        ->
          unitPrice as Float
          quantity as Integer
        <-
          rtn as Float: Float()

        require unitPrice?
        require quantity?
        require unitPrice >= 0.0
        require quantity >= 0

        rtn: unitPrice * Float(quantity)

  defines program

    RequirePreconditionsDemo()
      stdout <- Stdout()

      stdout.println("=== Order lifecycle with require ===")

      initialItems <- List() of String
      initialItems += "Widget"
      initialItems += "Gadget"

      order <- Order("CUST-001", initialItems)
      stdout.println("Created: " + $order)

      order.addItem("Sprocket")
      stdout.println("Added item: " + $order)

      order.submit()
      stdout.println("Submitted: " + $order)

      order.ship()
      stdout.println("Shipped: " + $order)

      stdout.println("=== Price calculation with require ===")

      calc <- PriceCalculator()
      total <- calc.calculateTotal(9.99, 3)
      stdout.println("Total: " + $total)

Go: panic() is uncatchable unless recover() is used. Rust: panic!() unwinds the stack and terminates the thread. Java: assert can be disabled at runtime with -da flag (weak). C/C++: assert() can be compiled out with NDEBUG (weak). Python: assert can be disabled with -O flag. Kotlin: require() throws IllegalArgumentException (catchable, unlike EK9). EK9: require is always active, always uncatchable, stronger than all of these.

argument catch contract exception guard handle invariant isset migrate null-safe panic precondition require safe uncatchable validate

orderXYZ <- Order("CUST-001", initialItems)

Explanation: Renaming the variable means later references to 'order' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.