Question: How do I catch specific exception types in EK9?

Can I catch different exception types in EK9?

How does exception type matching work in EK9?

How do I handle multiple exception types?

Answer:
EK9 supports exception type hierarchies. Custom exceptions extend Exception, and catch blocks match by type. You can catch exact types or use polymorphic catching via a base type.

EXCEPTION HIERARCHY
Create sibling exception types:
  NetworkError extends Exception
    ...
  TimeoutError extends Exception
    ...
Both extend Exception but are distinct types.

EXACT TYPE MATCHING
Catch a specific exception type:
  try
    throw networkError
  catch
    -> ex as NetworkError
    handleNetworkError(ex)
Only NetworkError (and its subtypes) will be caught.

POLYMORPHIC CATCHING
Catch the base Exception to handle any exception type:
  try
    throw networkError
  catch
    -> ex as Exception
    handleAnyError(ex)
Since all exceptions extend Exception, catching Exception catches everything.

TYPE MISMATCH BEHAVIOUR
If the thrown type does not match the catch type, the exception propagates:
  try
    throw networkError
  catch
    -> ex as TimeoutError
    // NOT reached - NetworkError is not TimeoutError
The exception passes through to an enclosing try/catch or terminates the program.

NESTED TRY FOR MULTI-TYPE
Use nested try blocks to handle different exception types:
  try
    try
      riskyOperation()
    catch
      -> ex as NetworkError
      handleNetwork(ex)
  catch
    -> ex as Exception
    handleOther(ex)
The inner catch handles NetworkError specifically. Anything else propagates to the outer catch.

See Q136 for throwing and creating custom exceptions. See Q134 for basic try/catch. See Q102 for the 'as open' modifier used in inheritance.

See Q308 for the dispatcher pattern for exception type routing.

defines module qa.errorhandling.exceptionsubtypes

  defines class

    <?-
      Exception for network-related failures.
    -?>
    NetworkError extends Exception
      host <- String()

      NetworkError()
        ->
          reason as String
          host as String

        super(reason)
        this.host :=: host

      host() as pure
        <- rtn as String: host

      default operator ?

    <?-
      Exception for timeout failures.
    -?>
    TimeoutError extends Exception
      seconds <- Integer()

      TimeoutError()
        ->
          reason as String
          seconds as Integer

        super(reason)
        this.seconds :=: seconds

      seconds() as pure
        <- rtn as Integer: seconds

      default operator ?

  defines program

    ExceptionSubtypesDemo()
      stdout <- Stdout()

      // === EXACT TYPE MATCH ===

      stdout.println("=== Exact type match ===")
      try
        ex <- NetworkError("Connection refused", "api.example.com")
        throw ex
      catch
        -> ex as NetworkError
        stdout.println("Network error on host: " + ex.host())

      // === POLYMORPHIC CATCH (base Exception) ===

      stdout.println("=== Polymorphic catch ===")
      try
        ex <- NetworkError("DNS failure", "db.example.com")
        throw ex
      catch
        -> ex as Exception
        stdout.println("Caught via base Exception: " + ex.reason())

      // === TYPE MISMATCH (nested try) ===

      stdout.println("=== Type mismatch propagation ===")
      try
        try
          ex <- NetworkError("Connection reset", "web.example.com")
          throw ex
        catch
          -> ex as TimeoutError
          stdout.println("Should NOT reach here")
      catch
        -> ex as Exception
        stdout.println("Propagated to outer catch: " + ex.reason())

      // === NESTED TRY FOR MULTI-TYPE HANDLING ===

      stdout.println("=== Multi-type handling ===")
      try
        try
          ex <- TimeoutError("Request timed out", 30)
          throw ex
        catch
          -> ex as TimeoutError
          stdout.println(`Timeout after ${ex.seconds()} seconds: ${ex.reason()}`)
      catch
        -> ex as Exception
        stdout.println("Other error: " + ex.reason())

Java: multiple catch blocks (catch IOException | SQLException), ordered most specific first. Python: multiple except clauses, tuple of types. Rust: no exception types, uses enum variants in Result. Go: errors.Is/errors.As for type checking error chains. Kotlin: multiple catch blocks like Java. EK9: single catch per try block with type matching, use nested try blocks for multiple types, polymorphic matching via base Exception.

catch exception extends handle handler hierarchy match nested polymorphic sealed specific subtype type visitor

Explanation: The method is named host(), not getHost(). EK9 does not use Java-style getter naming conventions. Check the class definition to confirm method names. See ek9 -h E50060 for details.

-> ex as String
        stdout.println("Network error on host: " + ex)

Explanation: Catch blocks require a type that extends Exception. Catching a String triggers E04030 — type must be of Exception type. See ek9 -h E04030 for details.

stdout.println("Propagated to outer catch: " + ex.getMessage())

Explanation: EK9 Exception has no getMessage() method. Use reason() for the error message. See ek9 -h E50060 for details.

stdout.println("Network error on host: " + ex.getHost())

Explanation: The method is host(), not getHost(). EK9 does not use Java-style getter naming. See ek9 -h E50060 for details.

stdout.println("Caught via base Exception: " + ex.getMessage())

Explanation: EK9 Exception has no getMessage() method. Use reason() to get the exception message. See ek9 -h E50060 for details.


Question: When should I use try/catch vs Result for error handling?

Should I use exceptions or Result in EK9?

What is the EK9 error handling philosophy?

Answer:
EK9 provides two error handling approaches: try/catch for truly exceptional situations and Result for expected, recoverable failures. Choosing the right one depends on whether the error is expected or unexpected.

TWO ERROR HANDLING APPROACHES
Try/catch: For unexpected, exceptional conditions that disrupt normal flow.
Result: For expected failures that are part of normal operation.

WHEN TO USE TRY/CATCH
Use try/catch when:
- The error is truly unexpected (I/O failure, network down)
- The caller cannot meaningfully recover inline
- The error represents a broken invariant
- System-level failures that need stack unwinding
Example: File system errors, database connection failures, out of memory.

WHEN TO USE RESULT
Use Result when:
- Failure is an expected outcome (validation, lookup miss)
- The caller should handle both paths explicitly
- You want the compiler to enforce error checking
- The function signature should communicate that failure is possible
Example: User input validation, configuration lookup, parsing.

DESIGN GUIDELINES
1. If the caller must handle the error: use Result
2. If the error should propagate up: use try/catch
3. If failure is common (>5% of calls): use Result
4. If failure is rare and unexpected: use try/catch
5. For public APIs: prefer Result for clarity

EK9 PHILOSOPHY
EK9 encourages using Result for most application-level error handling because:
- The compiler enforces safe access (cannot access .ok() without guard)
- The function signature is explicit about failure possibility
- No hidden control flow jumps
Reserve try/catch for truly exceptional situations where Result would be awkward.

See Q48 for the Result type. See Q134 for basic try/catch. See Q86 for Result guard patterns. See Q87 for Result operations.

See Q304 for require preconditions. See Q305 for require vs assert vs throw.

defines module qa.errorhandling.trycatchvsresult

  defines function

    <?-
      Validates an age value.
      Returns Result: ok with description, or error code.
      This is a RESULT approach - failure is expected and common.
    -?>
    validateAge()
      -> age as Integer
      <- rtn <- Result() of (String, Integer)

      maxAmount <- 150
      if age < 0
        rtn: Result(String(), -1)
      else
        if age > maxAmount
          rtn: Result(String(), -2)
        else
          rtn: Result("Valid: " + $age, Integer())

    <?-
      Reads configuration that might fail unexpectedly.
      Uses TRY/CATCH approach - failure is exceptional.
    -?>
    loadConfig()
      <- rtn <- String()

      // Simulating a config load that succeeds
      rtn: "production"

  defines program

    TryCatchVsResultDemo()
      stdout <- Stdout()

      // === RESULT APPROACH: Expected failures ===

      stdout.println("=== Result approach (validation) ===")

      ages <- List() of Integer
      ages += 25
      ages += -5
      ages += 200

      for age in ages
        result <- validateAge(age)
        if result?
          stdout.println("Valid age: " + $result.ok())
        else
          if result.isError()
            stdout.println(`Invalid (error code ${result.error()}) for age: ${age}`)

      // === TRY/CATCH APPROACH: Exceptional failures ===

      stdout.println("=== Try/catch approach (system errors) ===")

      try
        config <- loadConfig()
        stdout.println("Config loaded: " + config)
      catch
        -> ex as Exception
        stdout.println("System error: " + $ex)

      // === COMBINING BOTH ===

      stdout.println("=== Combined approach ===")

      try
        ageResult <- validateAge(30)
        if ageResult?
          stdout.println("Processing age: " + $ageResult.ok())
        else
          if ageResult.isError()
            stdout.println("Bad input: error code " + $ageResult.error())
      catch
        -> ex as Exception
        stdout.println("Unexpected error: " + $ex)

Java: exceptions for everything (checked and unchecked), no built-in Result type. Python: exceptions for everything including flow control (StopIteration). Rust: Result for all recoverable errors, panic! for unrecoverable. Go: error return values for everything, no exceptions. Kotlin: exceptions plus kotlin.Result for functional style. EK9: both try/catch AND Result available, philosophy favours Result for expected failures and try/catch for truly exceptional conditions, compiler enforces safe access on both.

approach catch choose debug design error exception guard handle handling ok philosophy result strategy when

Explanation: The return variable must be initialized. Using '<-' with a constructor ensures the variable starts initialized. Using 'as' without an initializer leaves it uninitialized, triggering E08180 — variable not marked for injection nor initialised. See ek9 -h E08180 for details.

stdout.println("Valid age: " + result.unwrap())

Explanation: EK9 Result has no unwrap() method. Use ok() with a guard check (result?) or isOk(). See ek9 -h E50060 for details.

Explanation: EK9 has no toString() method. Use string concatenation or interpolation instead. See ek9 -h E50060 for details.

stdout.println("System error: " + ex.getMessage())

Explanation: EK9 Exception has no getMessage() method. Use reason() for the message or the $ operator for string conversion. See ek9 -h E50060 for details.

stdout.println("Unexpected error: " + ex.getMessage())

Explanation: EK9 Exception has no getMessage() method. Use the $ operator for string conversion. See ek9 -h E50060 for details.


Question: How do I define constants in EK9?

How do I declare constants in EK9?
What types can be constants in EK9?
What is the defines constant block?

Answer:
EK9 provides a dedicated 'defines constant' block at module level for declaring named constant values. Constants use the declaration operator <- with a literal value, and the type is inferred from the literal form.

DEFINES CONSTANT BLOCK
Constants are declared at module level inside a 'defines constant' block:
  defines constant
    maxRetries <- 10
    greeting <- "Hello"
    Pi <- 3.142
Each constant uses <- with a literal. No type annotation is needed because EK9 infers the type from the literal syntax.

ALL 18 CONSTANT-ELIGIBLE TYPES
Only built-in value types can be constants. EK9 supports exactly 18 types:
  Integer       42, 0xFF, 0b1010
  Float         3.14, 2.0
  Bits          0b1010 (bit manipulation)
  Boolean       true, false
  Character     'A', 'z'
  String        "Hello"
  Time          10:30, 23:59:59
  Date          2024-01-15
  DateTime      2024-01-15T10:30:00Z
  Duration      PT2H, PT30M
  Millisecond   500ms, 1000ms
  Dimension     2m, 100cm, 5.2kg
  Resolution    300dpi, 72dpi
  Colour        #FF0000, #AB6F2B
  Money         12.50#USD, 100#GBP
  RegEx         /[a-z]+/, /[S|s]te(?:ven?|phen)/
  Version       1.0.0-1, 2.3.4-0
  Path          $?.some.path
User-defined classes and records cannot be constants because copy-on-access requires compiler-known copy constructors that only built-in types guarantee.

NATIVE LITERAL SYNTAX
EK9 provides domain-specific literal syntax for many types. You write Money as 12.50#USD, Colour as #FF0000, Duration as PT2H, Dimension as 2m, and Date as 2024-01-15. These are not strings that get parsed at runtime; they are native literals that the compiler understands directly.

NAMING CONVENTIONS
EK9 does not enforce a naming convention for constants. Common styles include UPPER_CASE (like MAX_RETRIES) and camelCase (like maxRetries). Choose a convention and apply it consistently across your project.

See Q23 for the full list of built-in types. See Q31 for Date and Time details. See Q35 for Money. See Q36 for Dimension. See Q141 for how constants are protected via copy-on-access. See Q143 for how EK9 constants compare to const/final/static in other languages. See Q142 for constants across modules. See Q317 for magic literal detection and why constants should be named.

defines module qa.constants.definition

  defines constant
    maxRetries <- 10
    Pi <- 3.142
    greeting <- "Hello"
    isEnabled <- true
    initial <- 'A'

    noon <- 12:00
    releaseDate <- 2024-01-15
    launchTime <- 2024-01-15T10:30:00Z
    timeout <- PT30M
    halfSecond <- 500ms

    pageWidth <- 2m
    screenDpi <- 300dpi
    brandColour <- #AB6F2B
    maxPayment <- 300000#USD
    matchSteves <- /[S|s]te(?:ven?|phen)/
    appVersion <- 1.0.0-1
    configPath <- $?.app.config

  defines program

    DefineConstantsDemo()
      stdout <- Stdout()

      // Numeric constants
      stdout.println(`Max retries: ${maxRetries}`)
      stdout.println(`Pi: ${Pi}`)

      // Text and character
      stdout.println(`Greeting: ${greeting}`)
      stdout.println(`Initial: ${initial}`)
      stdout.println(`Enabled: ${isEnabled}`)

      // Temporal constants
      stdout.println(`Noon: ${noon}`)
      stdout.println(`Release: ${releaseDate}`)
      stdout.println(`Launch: ${launchTime}`)
      stdout.println(`Timeout: ${timeout}`)
      stdout.println(`Half second: ${halfSecond}`)

      // Domain constants
      stdout.println(`Page width: ${pageWidth}`)
      stdout.println(`Screen DPI: ${screenDpi}`)
      stdout.println(`Brand colour: ${brandColour}`)
      stdout.println(`Max payment: ${maxPayment}`)
      stdout.println(`Version: ${appVersion}`)
      stdout.println(`Config: ${configPath}`)

Java: static final fields, no dedicated constant block. Rust: const for compile-time values, limited to scalar-like types. Go: const block for basic types only (no structs/slices/maps). Kotlin: const val for primitives and String only. EK9: defines constant block supporting 18 types including Money, Colour, Dimension with native literal syntax and copy-on-access immutability.

constant define fixed immutable inference literal module type value

Explanation: Renaming 'maxRetries' to 'maxretries' breaks all references since EK9 is case-sensitive. The constant name must match exactly wherever it is used. See ek9 -h E50001 for details.

Explanation: Constants declared in a 'defines constant' block are immutable. You cannot reassign maxRetries after declaration. Read the constant or assign it to a mutable variable instead. See ek9 -h E07890 for details.

Explanation: EK9 is case-sensitive. The constant is 'maxRetries' not 'MAX_RETRIES'. EK9 uses camelCase for all identifiers including constants. See ek9 -h E50001 for details.

Explanation: EK9 does not have toString(). Use string interpolation or the $ operator. See ek9 -h E50060 for details.

Oracle can generate a constants block with typed constant declarations.


Question: How does EK9 protect constants from mutation?

How does copy-on-access work for constants?

Answer:
EK9 enforces deep immutability for constants through two mechanisms: copy-on-access and compile-time mutation blocking. Every reference to a constant produces an independent copy, and all mutation operators are blocked at compile time.

COPY-ON-ACCESS
Every time you reference a constant, the compiler generates code that loads the constant value and immediately creates a fresh copy via the type's copy constructor. This means each access gets an independent copy. If you write:
  msg <- greeting
The variable msg receives a copy of the greeting constant, not a reference to the same object.

DEEP IMMUTABILITY
All mutation operators are blocked on constants at compile time with error NOT_MUTABLE:
  ++  --  +=  :=  :=?  :=:  :~:  :^:
Attempting any of these on a constant produces a compile error. This is not a runtime check; the compiler catches it before any code is generated.

AUTO-CLONING ON PASS
When you pass a constant as a function argument, the compiler auto-clones it. The function receives a copy, so the original constant value is unaffected regardless of what the function does with its parameter.

AUTO-CLONING ON RETURN
When a constant is used in an expression or returned from a function, the result is always a fresh mutable copy. This means you can freely use constants in computations without risk.

VARIABLE COPIES ARE MUTABLE
Once you assign a constant to a variable, the variable holds an independent mutable copy:
  name <- greeting
  name += " World"
The variable name is mutable and can be modified. The greeting constant remains unchanged.

WHY ONLY BUILT-IN TYPES
User-defined classes and records cannot be constants because copy-on-access requires the compiler to know exactly how to copy the value. Built-in types have guaranteed copy constructors with well-defined semantics. For user-defined types, the compiler cannot guarantee that a copy is truly independent, so it restricts constants to the 18 built-in value types.

See Q140 for defining constants and the full list of constant-eligible types. See Q130 for mutable vs immutable collection operations. See Q29 for unset/set semantics and how constants are always set. See Q143 for how this compares to const/final/static in other languages. See Q268 for how immutability prevents OWASP data integrity vulnerabilities. See Q317 for magic literal detection that encourages named constants.

defines module qa.constants.immutability

  defines constant
    greeting <- "Hello"
    baseCount <- 10
    price <- 9.99#USD

  defines function

    addSuffix()
      -> text as String
      <- rtn as String: text + " World"

    doubleIt()
      -> n as Integer
      <- rtn as Integer: n + n

  defines program

    ConstantImmutabilityDemo()
      stdout <- Stdout()

      // === COPY-ON-ACCESS: each reference is a fresh copy ===

      first <- greeting
      second <- greeting
      stdout.println(`First: ${first}`)
      stdout.println(`Second: ${second}`)

      // === VARIABLE COPIES ARE MUTABLE ===

      mutableName <- greeting
      mutableName += " World"
      stdout.println(`Modified copy: ${mutableName}`)

      // Original constant unchanged on next access
      stdout.println(`Constant still: ${greeting}`)

      // === CONSTANTS IN EXPRESSIONS ===

      doubled <- baseCount + baseCount
      stdout.println(`Doubled: ${doubled}`)
      stdout.println(`Original: ${baseCount}`)

      // === CONSTANTS PASSED TO FUNCTIONS ===

      result <- addSuffix(greeting)
      stdout.println(`Function result: ${result}`)
      stdout.println(`Constant after call: ${greeting}`)

      computed <- doubleIt(baseCount)
      stdout.println(`Computed: ${computed}`)
      stdout.println(`Base still: ${baseCount}`)

      // === CONSTANTS IN STRING INTERPOLATION ===

      stdout.println(`The price is ${price}`)

Java: final prevents reassignment but not mutation (final List can still add()). JavaScript: const prevents reassignment but not mutation (const obj = {}; obj.x = 1 works). Rust: const is compile-time only, let bindings are immutable by default. Go: const limited to basic types. EK9: copy-on-access ensures every reference gets a fresh independent copy, plus all mutation operators are blocked at compile time. Truly immutable, no escape hatch.

access clone constant copy deep immutable mutation protection safety

Explanation: Constants are protected by copy-on-access immutability. You cannot use mutating operators like += on a constant. Use the constant in a non-mutating expression to produce a new value instead. See ek9 -h E07890 for details.

Explanation: Constants cannot be reassigned with :=. Unlike Java final or JavaScript const which only prevent reassignment, EK9 constants block ALL mutation including assignment. Create a new variable from the constant instead. See ek9 -h E07890 for details.

Explanation: The ++ operator mutates the target variable. Constants cannot be mutated by any operator. Use the constant in a non-mutating expression to compute a new value. See ek9 -h E07890 for details.


Question: How do I use constants across modules in EK9?

How do I access constants from another module?

Can I share constants between files in EK9?

Answer:
EK9 constants belong to the module, not to individual files. Within the same module, constants are visible across all files without any import. For cross-module access, EK9 provides two mechanisms: fully qualified names and the references block.

SAME-MODULE ACCESS
Constants defined in one file are visible in all other files that define the same module. If two files both declare 'defines module com.example.app', they share the same constant namespace. No import or reference is needed.

FULLY QUALIFIED ACCESS
You can access a constant from another module using its fully qualified name:
  value <- other.module::CONSTANT_NAME
The :: operator separates the module name from the constant name. This works anywhere without importing the module first.

REFERENCES IMPORT
The 'references' block imports symbols from other modules for short-name access:
  references
    net.customer.geometry::Pi
After this declaration, you can use Pi directly instead of net.customer.geometry::Pi. This is purely a convenience; it does not change the constant's behaviour.

MIXING STYLES
You can mix short names (from references) and fully qualified names in the same file and even in the same expression. Use whichever is clearer in context. For widely-used constants, import via references. For one-off access, use the fully qualified form.

MODULE-LEVEL SCOPE
Constants are module-level declarations, just like classes, functions, and programs. They participate in the same visibility and access rules. A constant is accessible from any code that can reference its module.

See Q6 for how modules work in EK9. See Q13 for the difference between packages and modules. See Q140 for defining constants. See Q141 for copy-on-access protection.

defines module qa.constants.crossmodule

  defines constant
    Pi <- 3.14159
    maxItems <- 100
    appName <- "ConstDemo"

  defines function

    circumference()
      -> radius as Float
      <- rtn as Float: 2.0 * Pi * radius

    describe()
      -> name as String
      <- rtn as String: `${appName}: ${name}`

  defines program

    CrossModuleConstantsDemo()
      stdout <- Stdout()

      // === SAME-MODULE ACCESS: constants visible without import ===

      stdout.println(`Pi: ${Pi}`)
      stdout.println(`Max items: ${maxItems}`)
      stdout.println(`App: ${appName}`)

      // === CONSTANTS USED IN SAME-MODULE FUNCTIONS ===

      circ <- circumference(5.0)
      stdout.println(`Circumference of radius 5: ${circ}`)

      desc <- describe("test")
      stdout.println(`Description: ${desc}`)

      // === CONSTANTS IN EXPRESSIONS ACROSS THE MODULE ===

      halfMax <- maxItems / 2
      stdout.println(`Half max: ${halfMax}`)

      fullGreeting <- appName + " v1.0"
      stdout.println(`Full: ${fullGreeting}`)

Java: public static final fields accessed via ClassName.CONSTANT, import statements for short access. Python: module-level variables, import or from-import. Go: exported constants (uppercase) accessible as package.Constant. Rust: pub const in module, use statement for import. EK9: module-level constants, visible across same-module files automatically, fully qualified access via :: or short names via references block.

access constant cross import module qualified reference scope share

Explanation: Module-level constants cannot be reassigned. Attempting to modify maxItems with := triggers E07890. Derive new values from constants using expressions instead. See ek9 -h E07890 for details.

Explanation: EK9 is case-sensitive. The constant is 'Pi' not 'pi'. Constant names must match exactly wherever referenced. See ek9 -h E50001 for details.


Question: How do EK9 constants compare to const, final, and static in other languages?

What is the difference between const and final?

How does EK9 const compare to Java final?

Is EK9 const like Rust const or JavaScript const?

Answer:
Developers frequently confuse const, final, and static because these keywords overlap differently across languages. Here is how each language handles constants and what EK9 does differently.

THE CONFUSION
The word 'constant' should mean 'this value never changes'. But most languages only guarantee 'this binding cannot be reassigned', which is fundamentally different. A final reference to a mutable object does not make the object constant.

JAVA
Java uses 'final' to prevent reassignment, but final does NOT prevent mutation:
  final List<String> names = new ArrayList<>();
  names.add("Alice"); // This works! The list is mutated.
The 'static final' idiom is Java's convention for constants, but even static final objects can be mutated if they are mutable types. Only primitive static final fields are truly constant (inlined by the compiler).

RUST
Rust's 'const' is truly compile-time: the value must be computable at compile time and is limited to scalar-like types. 'static' is runtime-initialized and lives for the program lifetime. 'let' bindings are immutable by default but interior mutability (RefCell, Mutex) can bypass this. Rust comes closest to real immutability but still has escape hatches.

GO
Go's 'const' is compile-time only and limited to basic types: numbers, strings, and booleans. No structs, slices, or maps. Everything else is a 'var' that you promise not to change, with no compiler enforcement.

KOTLIN
Kotlin's 'const val' is compile-time, limited to primitives and String. 'val' prevents reassignment but not mutation: val list = mutableListOf() still allows list.add(). Similar to Java's final.

JAVASCRIPT
JavaScript's 'const' is arguably the most misleading: it prevents reassignment but not mutation. const obj = {}; obj.x = 1 works perfectly. The name 'const' suggests immutability but delivers only binding fixity.

EK9
EK9's 'defines constant' block provides true value immutability through copy-on-access:
  1. Every access returns a fresh independent copy via copy constructor
  2. All mutation operators are blocked at compile time (NOT_MUTABLE error)
  3. Supports 18 types including domain types (Money, Colour, Duration, Dimension)
  4. No escape hatch: you cannot mutate a constant through any mechanism
The key insight is that assigning a constant to a variable gives you a mutable copy. The constant itself is forever protected, but the copy is yours to modify freely.

KEY INSIGHT
Most languages conflate 'immutable binding' with 'immutable value'. EK9 separates them: the constant value is immutable (copy-on-access), while variables that receive copies are fully mutable. This eliminates the confusion entirely.

See Q140 for defining constants in EK9. See Q141 for copy-on-access immutability details. See Q130 for mutable vs immutable collection operations.

defines module qa.constants.comparison

  defines constant
    greeting <- "Hello"
    count <- 42
    price <- 9.99#USD
    colour <- #FF8800

  defines program

    ConstFinalStaticDemo()
      stdout <- Stdout()

      // === EK9: CONSTANT ACCESS GIVES YOU A COPY ===

      stdout.println(`Constant: ${greeting}`)
      stdout.println(`Count: ${count}`)

      // === VARIABLE FROM CONSTANT IS MUTABLE ===

      name <- greeting
      name += " World"
      stdout.println(`Modified copy: ${name}`)

      // Constant unchanged
      stdout.println(`Constant still: ${greeting}`)

      // === EXPRESSIONS WITH CONSTANTS PRODUCE MUTABLE RESULTS ===

      doubled <- count + count
      stdout.println(`Doubled: ${doubled}`)

      doubled := doubled + 1
      stdout.println(`Incremented copy: ${doubled}`)

      // Original constant unchanged
      stdout.println(`Count still: ${count}`)

      // === DOMAIN TYPE CONSTANTS ===

      stdout.println(`Price: ${price}`)
      stdout.println(`Colour: ${colour}`)

      localPrice <- price
      stdout.println(`Local price copy: ${localPrice}`)

Java: final prevents reassignment not mutation, static final is convention for constants. Rust: const is compile-time only (scalar types), let is immutable binding with interior mutability escape. Go: const limited to basic types, no struct/slice/map. Kotlin: const val for primitives/String only, val prevents reassignment not mutation. JavaScript: const prevents reassignment not mutation (most misleading). EK9: copy-on-access true immutability for 18 types, no escape hatch, variable copies are mutable.

comparison const constant final go immutable java migrate mutation reassignment rust static

Explanation: Unlike Java's final which only prevents reassignment, EK9 constants block ALL mutation operators including +=. Assign the constant to a mutable variable first, then modify the copy. See ek9 -h E07890 for details.

Explanation: Constants cannot be reassigned. The := operator mutates the target variable, which is forbidden for constants. Copy the constant to a local variable first. See ek9 -h E07890 for details.

Explanation: The :=? guarded assignment still mutates the target when it is unset. EK9 constants cannot be mutated through any mechanism including guarded assignment. Constants are always set, so :=? would never trigger anyway. See ek9 -h E07890 for details.


Question: Why doesn't EK9 have break, continue, or return statements?

What happened to break and continue in EK9?

Why did EK9 remove the return statement?

Is EK9 missing break, continue, and return, or is that deliberate?

What is the evidence against break, continue, and return?

Answer:
EK9 deliberately excludes break, continue, and return. This is not a gap or oversight. It is a core design decision backed by decades of production evidence.

THE DESIGN DECISION
EK9 removes break, continue, return, and switch fallthrough from the language grammar entirely. These keywords do not exist. The compiler cannot parse them. This eliminates an entire category of bugs at the language level rather than relying on developer discipline.

THE EVIDENCE
Microsoft (2011): 15% of production bugs in C# involved loop control flow errors with break and continue. Apple SSL bug (2014): a duplicated goto statement bypassed SSL certificate validation, affecting millions of devices. Linux Kernel: over 200 CVE fixes traced to break-in-wrong-loop and fallthrough bugs. CERT: switch fallthrough ranked as the 7th most dangerous coding error.

THE PRINCIPLE
If you eliminate a feature from the language, you eliminate every bug that feature can cause. Not reduce. Eliminate. EK9 applies this principle to the four most error-prone control flow mechanisms.

WHAT EK9 PROVIDES INSTEAD
Stream pipelines replace break and continue. Use cat, filter, head, tail, and skip to express what you want rather than how to exit:
  results <- cat items | filter by isValid | head 5 | collect as List of String
Declared return variables replace return statements. The compiler verifies all paths initialise the return:
  describe() as pure
    -> number as Integer
    <- label as String: "other"
    if number > 100
      label: "high"
Multiple case values replace switch fallthrough:
  switch day
    case "Saturday", "Sunday"
      type: "weekend"
    default
      type: "weekday"
Guard expressions replace early returns:
  if data <- fetchData()
    process(data)

MODERN TRENDS
Swift removed fallthrough by default. Rust enforces exhaustive matching. Go removed fallthrough by default. Kotlin sealed classes enforce exhaustive when blocks. EK9 takes these individual improvements to their logical conclusion by removing all four mechanisms.

See Q50 for declared return variables. See Q69 for multiple case values. See Q89 for stream pipelines. See Q125 for head, tail, and skip. See Q145 for replacing break and continue. See Q146 for function decomposition. See Q147 for switch fallthrough details. See Q148 for migration control flow. See Q268 for how removing these prevents OWASP insecure design vulnerabilities. See Q274 and Q275 for AI hallucination patterns with return, break, and continue. See Q282 through Q289 for practical patterns without break, return, or continue. See Q296 for similar evidence-based naming restrictions. See Q310 for how these removals fit into the quality pyramid.

See Q338 for how no-return prevents partial transaction commits.

defines module qa.flow.philosophy.overview

  defines function

    isValid() as pure
      -> item as String
      <- valid as Boolean: length item > 0

  defines program

    NoBreakContinueReturnDemo()
      stdout <- Stdout()

      // === STREAM PIPELINE REPLACES BREAK AND CONTINUE ===

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

      // Filter out empty strings (replaces continue) and take first 3 (replaces break)
      selected <- cat items | filter by isValid | head 3 | collect as List of String
      stdout.println(`Selected: ${selected}`)

      // === DECLARED RETURN REPLACES RETURN STATEMENT ===

      label <- describeValue(42)
      stdout.println(`42 is: ${label}`)

      label2 <- describeValue(150)
      stdout.println(`150 is: ${label2}`)

      // === MULTIPLE CASE VALUES REPLACE FALLTHROUGH ===

      days <- ["Monday", "Saturday", "Wednesday", "Sunday"]
      for day in days
        dayType <- String()
        switch day
          case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
            dayType: "weekday"
          case "Saturday", "Sunday"
            dayType: "weekend"
          default
            dayType: "unknown"
        stdout.println(`${day} -> ${dayType}`)

  defines function

    describeValue() as pure
      -> number as Integer
      <- label as String: "normal"
      upperLimit <- 100
      if number > upperLimit
        label: "high"
      else if number < 0
        label: "negative"

Java: break and continue in loops, return in methods, switch fallthrough by default (arrow syntax since Java 14 avoids it). Python: break and continue in loops, return in functions, no switch until match/case in 3.10. Rust: break and continue in loops, implicit return via last expression, match is exhaustive with no fallthrough. Go: break and continue in loops, return in functions, switch has no fallthrough by default. C/C++: break and continue, return, switch fallthrough by default (major bug source). Kotlin: break and continue, return, when expression with no fallthrough. Swift: break (rarely needed), continue, return, switch with no fallthrough by default. EK9: none of these exist, replaced by stream pipelines, declared return variables, multiple case values, and guard expressions.

apple branch break bug cert condition continue control deliberate design eliminate evidence flow microsoft migrate philosophy removed return safety ssl

no break
no continue
no return
why no break

selected <- List() of String
      for item in items
        if isValid(item)
          selected += item
          if length selected >= 3
            break

Explanation: EK9 has no break statement. Stream pipelines with head replace the loop-with-break pattern. The pipeline expresses intent directly: filter and take 3. See ek9 -h E01070 for details.

<- label as String: "normal"
      return label

Explanation: EK9 has no return statement — it was designed out of existence. Declare a return variable with '<-' and the compiler ensures all code paths initialise it. See ek9 -h E01072 for details.

selected <- cat items | filter by isValid | head 3 | collect as List of String
      continue

Explanation: EK9 has no continue statement. Stream pipelines with 'filter by' replace skip-and-continue patterns. The pipeline expresses filtering intent directly. See ek9 -h E01071 for details.


Question: How do I exit a loop early or skip iterations in EK9?

What replaces break and continue in EK9?

How do I find the first match without break in EK9?

How do I skip items in a loop without continue in EK9?

How do I replace break and continue with stream operations in EK9?

Answer:
EK9 has no break or continue. Stream pipelines with head, filter, and skip are the designed replacements.

REPLACING BREAK WITH HEAD
Where other languages use a loop with break to find the first match, EK9 uses head 1:
  first <- cat items | filter by isLong | head 1 | collect as List of String
The head operation stops the pipeline after N items. No elements beyond that point are processed.

REPLACING CONTINUE WITH FILTER
Where other languages skip unwanted items with continue, EK9 uses filter:
  valid <- cat items | filter by isNonEmpty | collect as List of String
The filter operation passes only items that match the predicate. Items that do not match are discarded.

REPLACING COUNTED BREAK WITH HEAD N
Where other languages count items and break at a limit, EK9 uses head N:
  topFive <- cat items | sort | head 5 | collect as List of String
This takes exactly 5 items and stops. No counter variable, no break condition.

REPLACING SKIP-AHEAD WITH SKIP
Where other languages use continue with a counter to skip initial items, EK9 uses skip:
  afterHeader <- cat lines | skip 3 | collect as List of String
This discards the first 3 elements and passes the rest through.

WHEN LOOPS ARE STILL APPROPRIATE
For-in and for-range loops are still the right tool for side effects that process every item:
  for item in items
    stdout.println(`Processing: ${item}`)
Loops work when you need to process ALL items and perform actions. Streams work when you need to select, transform, or limit.

COMBINING OPERATIONS
Pipeline operations compose naturally:
  results <- cat items | filter by isValid | skip 2 | head 3 | collect as List of String
This filters, skips 2 valid items, then takes the next 3. Each operation has a single responsibility.

See Q64 for for-range loops. See Q65 for for-in loops. See Q89 for stream pipeline basics. See Q125 for head, tail, and skip details. See Q131 for Python comprehension migration. See Q133 for tee and uniq operations. See Q144 for why break and continue were removed. See Q282 for nested loop patterns. See Q288 for before and after migration examples.

defines module qa.flow.philosophy.replacingbreak

  defines function

    isLong() as pure
      -> item as String
      <- long <- false
      maxItems <- 5
      long: length item > maxItems

    isNonEmpty() as pure
      -> item as String
      <- nonEmpty as Boolean: length item > 0

    isValid() as pure
      -> item as String
      <- valid <- false
      divisor <- 2
      valid: length item > divisor

  defines program

    ReplacingBreakContinueDemo()
      stdout <- Stdout()

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

      // === REPLACING BREAK: FIND FIRST LONG ITEM ===

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

      // === REPLACING CONTINUE: SKIP EMPTY STRINGS ===

      nonEmpty <- cat items | filter by isNonEmpty | collect as List of String
      stdout.println(`Non-empty: ${nonEmpty}`)

      // === REPLACING COUNTED BREAK: TOP 3 ===

      topThree <- cat items | filter by isNonEmpty | sort | head 3 | collect as List of String
      stdout.println(`Top 3 sorted: ${topThree}`)

      // === REPLACING SKIP-AHEAD ===

      afterSkip <- cat items | filter by isValid | skip 2 | collect as List of String
      stdout.println(`After skipping 2 valid: ${afterSkip}`)

      // === COMBINING: FILTER + SKIP + HEAD ===

      window <- cat items | filter by isValid | skip 1 | head 3 | collect as List of String
      stdout.println(`Window (skip 1, head 3): ${window}`)

      // === LOOP FOR SIDE EFFECTS ===

      stdout.println("Processing all valid items:")
      for item in items
        if length item > 0
          stdout.println(`  -> ${item}`)

Java: break to exit loop, continue to skip iteration, Streams with limit() and skip() as alternative. Python: break to exit loop, continue to skip, list comprehensions with conditions as alternative. Rust: break to exit loop, continue to skip, iterators with take() and skip() as alternative. Go: break to exit loop, continue to skip, no built-in stream alternative. C/C++: break and continue in loops. Kotlin: break and continue, sequences with take() and drop(). JavaScript: break and continue, array methods filter() and slice(). EK9: no break or continue, stream pipelines with head (replaces break), filter (replaces continue), skip (replaces counted skip), and tail (last N).

alternative branch break condition continue control early exit filter first flow head iteration loop match migrate pipeline replace skip stream

firstLong <- String()
      for item in items
        if isLong(item)
          firstLong: item
          break

Explanation: EK9 has no break statement. Use stream pipelines with head 1 to find the first matching item. The pipeline stops processing after the first match. See ek9 -h E01070 for details.

nonEmpty <- List() of String
      for item in items
        if length item == 0
          continue
        nonEmpty += item

Explanation: EK9 has no continue statement. Use stream pipelines with filter to keep only items matching a predicate, replacing the skip-with-continue pattern. See ek9 -h E01071 for details.

Explanation: EK9 has no return statement. Declare the return variable with '<-' and assign it conditionally. The compiler ensures all paths initialise the return variable. See ek9 -h E01072 for details.


Question: How do I structure functions without early return in EK9?

How do I replace early returns with function decomposition in EK9?

How do I avoid deeply nested if/else in EK9 without return?

What is the decomposition pattern for complex functions in EK9?

How do I write focused single-purpose functions instead of using early returns?

Answer:
EK9 has no return statement. Instead of using early returns to handle different cases, decompose complex logic into small focused functions that each do one thing.

THE PROBLEM WITH EARLY RETURNS
In other languages, functions grow complex through accumulated early returns: validate input (return if bad), check permissions (return if denied), handle edge cases (return if empty), then finally process. Each return is a hidden exit. Paths multiply. Cleanup can be skipped. Testing requires tracing every possible exit.

THE EK9 APPROACH: DECOMPOSITION
Instead of one large function with early returns, write several small functions. Each function does one job. The calling function composes them.

SEPARATE VALIDATION FROM PROCESSING
Write a validation function that returns a Boolean or an unset signal:
  validateAge() as pure
    -> age as Integer
    <- valid as Boolean: age >= 0 and age <= 150
Then guard on the result:
  if validateAge(age)
    processAge(age)

SEPARATE DIFFERENT PROCESSING PATHS
Instead of if/else chains with early returns, use a categorisation function followed by a switch:
  categorise() as pure
    -> score as Integer
    <- category as String: "average"
    if score >= 90
      category: "excellent"
    else if score >= 70
      category: "good"
    else if score < 40
      category: "poor"

WHY DECOMPOSITION IS BETTER
Each function does one job. All paths through a function are visible at a glance. There are no hidden exits. Each function is independently testable. The calling code reads as a sequence of clear steps rather than a maze of guard clauses.

GUARDS FOR CONDITIONAL EXECUTION
Use guard variables to execute code only when a value is available:
  if record <- findRecord(id)
    process(record)
The body only executes if findRecord returns a set value. This replaces the early-return-if-null pattern.

UNSET RETURN AS SIGNAL
Functions can return an unset value to signal that no result was found:
  findByName()
    ->
      name as String
      items as List of String
    <- found as String: String()
    for item in items
      if item == name
        found: item
The caller uses a guard to check: if result <- findByName(name, items).

See Q50 for declared return variables. See Q51 for abstract function patterns. See Q56 for higher-order function composition. See Q74 for guard variables in if statements. See Q144 for why return was removed. See Q145 for replacing break and continue. See Q285 for multiple precondition patterns. See Q289 for before and after migration examples.

defines module qa.flow.philosophy.decomposition

  defines function

    // Validation function: one job
    validateAge() as pure
      -> age as Integer
      <- valid <- false
      maxAge <- 150
      valid: age >= 0 and age <= maxAge

    // Categorisation function: one job
    categorise() as pure
      -> score as Integer
      <- category as String: "average"
      excellentMin <- 90
      goodMin <- 70
      poorMax <- 40
      if score >= excellentMin
        category: "excellent"
      else if score >= goodMin
        category: "good"
      else if score < poorMax
        category: "poor"

    // Description function: one job
    describeCategory() as pure
      -> category as String
      <- description as String: category + " performance"

    // Search function: returns unset if not found
    findItem()
      ->
        name as String
        items as List of String
      <- found as String: String()
      for item in items
        if item == name
          found: item

  defines program

    DecompositionDemo()
      stdout <- Stdout()

      // === VALIDATION THEN PROCESSING ===

      ages <- [25, -5, 200, 42, 0, 150]
      for age in ages
        if validateAge(age)
          stdout.println(`Valid age: ${age}`)
        else
          stdout.println(`Invalid age: ${age}`)

      // === CATEGORISE THEN DESCRIBE ===

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

      // === GUARD ON SEARCH RESULT ===

      fruits <- ["apple", "banana", "cherry"]

      if result <- findItem("banana", fruits)
        stdout.println(`Found: ${result}`)

      if result <- findItem("mango", fruits)
        stdout.println(`Found: ${result}`)
      else
        stdout.println("Mango not found")

Java: early return for validation, guard clauses common, Extract Method refactoring in IDE. Python: early return common, guard clauses with if/return at top of function. Rust: early return with ?, pattern matching replaces some guard clauses. Go: early return for error checking (if err != nil return), very common pattern. Kotlin: early return, when expression reduces some nesting. Swift: guard let for early return on nil. EK9: no return statement, decompose into focused functions, guard expressions for conditional execution, unset return as signal, compiler verifies all paths initialise return variable.

branch compose condition control decomposition early exit flow focused function guard isset nested null-safe path process refactor return safe single validate

validateAge() as pure
      -> age as Integer
      if age < 0 or age > 150
        return false
      return true

Explanation: EK9 has no return statement. Functions use declared return variables. Decompose complex logic into small focused functions that each assign to their return variable. See ek9 -h E01072 for details.

Explanation: String has no toUpperCase() method. Use upperCase() instead. See ek9 -h E50060 for details.

Explanation: EK9 has no null. Use an unset value (String()) to signal 'not found'. The caller uses a guard expression: if result <- findItem(name). See ek9 -h E01073 for details.


Question: Why doesn't EK9 have switch fallthrough and how do I group cases?

How does EK9 prevent switch fallthrough bugs?

What is wrong with switch fallthrough in other languages?

How do I handle multiple values in one case without fallthrough in EK9?

How do comma-separated case values replace switch fallthrough in EK9?

Answer:
Switch fallthrough is one of the most dangerous features in programming. EK9 eliminates it entirely and provides a safer, clearer alternative.

THE FALLTHROUGH PROBLEM
In C, Java, and JavaScript, switch cases fall through to the next case by default. Forgetting a break statement causes silent execution of unintended code. CERT ranks this as the 7th most dangerous coding error. The bug is insidious because the code compiles and often appears to work until an edge case triggers the missing break.

A CLASSIC FALLTHROUGH BUG
In Java, this code silently assigns the wrong category:
  switch (code) {
    case 1: result = "admin";
    // missing break - falls through!
    case 2: result = "user";
    break;
  }
When code is 1, result becomes "user" instead of "admin". The compiler gives no warning. This pattern has caused countless production bugs.

EK9 SOLUTION: MULTIPLE VALUES PER CASE
EK9 uses comma-separated values in a single case clause:
  switch code
    case 1, 2, 3
      category: "low"
    case 4, 5
      category: "high"
    default
      category: "unknown"
Each case runs its body and stops. There is no fallthrough mechanism in the language.

SWITCH AS EXPRESSION
Switch can return a value directly:
  label <- switch priority
    <- rtn as String?
    case 1, 2
      rtn: "Low"
    case 3
      rtn: "Medium"
    case 4, 5
      rtn: "High"
    default
      rtn: "Unknown"

COMPARISON OPERATORS IN CASES
Cases can use comparison operators for range matching:
  switch score
    case >= 90
      grade: "A"
    case >= 80
      grade: "B"
    case >= 70
      grade: "C"
    default
      grade: "F"

ENUM EXHAUSTIVENESS
With enumerations, the compiler can verify all values are handled:
  switch colour
    case Colour.Red
      label: "stop"
    case Colour.Amber
      label: "caution"
    case Colour.Green
      label: "go"

See Q63 for basic switch syntax. See Q68 for switch as expression. See Q69 for multiple case values. See Q70 for comparison operators in cases. See Q73 for enum exhaustiveness. See Q144 for the full control flow philosophy.

defines module qa.flow.philosophy.nofallthrough

  defines type
    Colour
      Red
      Amber
      Green

  defines program

    NoFallthroughDemo()
      stdout <- Stdout()

      // === MULTIPLE VALUES PER CASE ===

      codes <- [1, 2, 3, 4, 5, 6]
      for code in codes
        category <- String()
        switch code
          case 1, 2, 3
            category: "low"
          case 4, 5
            category: "high"
          default
            category: "unknown"
        stdout.println(`Code ${code} -> ${category}`)

      // === SWITCH AS EXPRESSION ===

      priorities <- [1, 3, 5]
      for priority in priorities
        label <- switch priority
          <- rtn as String?
          case 1, 2
            rtn: "Low"
          case 3
            rtn: "Medium"
          case 4, 5
            rtn: "High"
          default
            rtn: "Unknown"
        stdout.println(`Priority ${priority}: ${label}`)

      // === COMPARISON OPERATORS IN CASES ===

      scores <- [95, 82, 71, 55]
      for score in scores
        grade <- String()
        switch score
          case >= 90
            grade: "A"
          case >= 80
            grade: "B"
          case >= 70
            grade: "C"
          default
            grade: "F"
        stdout.println(`Score ${score}: grade ${grade}`)

      // === ENUM SWITCH ===

      colours <- [Colour.Red, Colour.Amber, Colour.Green]
      for colour in colours
        meaning <- String()
        switch colour
          case Colour.Red
            meaning: "stop"
          case Colour.Amber
            meaning: "caution"
          case Colour.Green
            meaning: "go"
          default
            meaning: "unknown"
        stdout.println(`${colour} means ${meaning}`)

Java: switch fallthrough by default, break required to prevent it, switch expressions with arrow syntax since Java 14 eliminate fallthrough. C/C++: switch fallthrough by default, break required, Clang and GCC warn but cannot prevent. JavaScript: switch fallthrough by default, break required. Python: match/case since 3.10 with no fallthrough, pipe operator for multiple values. Rust: match with no fallthrough, pipe operator for multiple patterns, exhaustive. Go: switch with no fallthrough by default (opposite of C), explicit fallthrough keyword if needed. Kotlin: when with no fallthrough, comma-separated values. Swift: switch with no fallthrough, comma-separated values, exhaustive. C#: switch with no fallthrough between non-empty cases. EK9: no fallthrough mechanism exists, comma-separated case values, comparison operators in cases, switch as expression, enum exhaustiveness.

branch break bug case cert comma condition control danger exhaustive expression fallthrough flow migrate multiple safety silent switch values

case 1, 2, 3
            category: "low"

case 1, 2, 3
            category: "low"
            break

Explanation: EK9 has no break statement. Each case clause is self-contained with no fallthrough. The break keyword does not exist in the language. See ek9 -h E01070 for details.

Explanation: EK9 has no return statement. Switch expressions automatically return the declared variable's value. There is no need (and no way) to explicitly return from a program. See ek9 -h E01072 for details.

Explanation: Non-enum switch statements require a default clause to handle unexpected values. Without it, the compiler reports E07320. This ensures every possible value is handled. Enum switches with all values covered do not need default. See ek9 -h E07320 for details.


Question: How do I migrate complex control flow from Java or Python to EK9?

How do I rewrite a Java loop with break and continue in EK9?

How do I translate early return patterns to EK9?

What is the step-by-step process for converting control flow to EK9?

What are the migration rules for converting break, continue, and return to EK9?

Answer:
Five rules map familiar control flow patterns to EK9 equivalents.

RULE 1: BREAK BECOMES HEAD
  results <- cat items | head 3 | collect as List of String

RULE 2: CONTINUE BECOMES FILTER
  valid <- cat items | filter by isNonEmpty | collect as List of String

RULE 3: EARLY RETURN BECOMES IF GUARD
  if data <- fetch()
    process(data)

RULE 4: MULTIPLE RETURNS BECOME DECOMPOSITION
  category <- categorise(amount)
  label <- describe(category)

RULE 5: FALLTHROUGH BECOMES COMMA-SEPARATED CASES
  switch code
    case 1, 2, 3
      result: "low"

THE MENTAL MODEL SHIFT
Think 'what do I want?' not 'how do I exit?'. cat items | filter by isValid | head 3 | collect expresses intent directly.

See Q50 for return variables. See Q89 for stream pipelines. See Q125 for head/tail/skip. See Q131 for Python migration. See Q132 for Java migration. See Q144 for control flow philosophy. See Q145 for break/continue replacements. See Q146 for decomposition. See Q147 for fallthrough replacements. See Q288 for break loop migration. See Q289 for return migration. See Q748 for Swift migration.

defines module qa.flow.philosophy.migration

  defines function

    isNonEmpty() as pure
      -> item as String
      <- nonEmpty as Boolean: length item > 0

    isExpensive() as pure
      -> item as String
      <- expensive <- false
      minExpensiveLength <- 6
      expensive: length item > minExpensiveLength

    categorise() as pure
      -> amount as Integer
      <- category as String: "medium"
      highThreshold <- 100
      lowThreshold <- 10
      if amount >= highThreshold
        category: "high"
      else if amount < lowThreshold
        category: "low"

    describe() as pure
      -> category as String
      <- label as String: "standard item"
      switch category
        case "high"
          label: "premium item"
        case "low"
          label: "budget item"
        default
          label: "standard item"

    findByName()
      ->
        target as String
        items as List of String
      <- found as String: String()
      for item in items
        if item == target
          found: item

  defines program

    MigrationDemo()
      stdout <- Stdout()

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

      // === RULE 1: BREAK -> HEAD ===

      firstThree <- cat items | filter by isNonEmpty | head 3 | collect as List of String
      stdout.println(`First 3 non-empty: ${firstThree}`)

      // === RULE 2: CONTINUE -> FILTER ===

      nonEmpty <- cat items | filter by isNonEmpty | collect as List of String
      stdout.println(`Non-empty items: ${nonEmpty}`)

      // === RULE 3: EARLY RETURN -> GUARD ===

      names <- ["Alice", "Bob", "Charlie"]

      if found <- findByName("Bob", names)
        stdout.println(`Found: ${found}`)

      if found <- findByName("Dave", names)
        stdout.println(`Found: ${found}`)
      else
        stdout.println("Dave not found")

      // === RULE 4: MULTIPLE RETURNS -> DECOMPOSITION ===

      amounts <- [5, 50, 150]
      for amount in amounts
        category <- categorise(amount)
        label <- describe(category)
        stdout.println(`Amount ${amount}: ${label}`)

      // === RULE 5: FALLTHROUGH -> COMMA CASES ===

      codes <- [1, 2, 3, 4, 5]
      for code in codes
        level <- String()
        switch code
          case 1, 2, 3
            level: "basic"
          case 4, 5
            level: "advanced"
          default
            level: "unknown"
        stdout.println(`Code ${code}: ${level}`)

      // === COMBINED: THE MENTAL MODEL SHIFT ===
      // "I want the first 2 expensive items" (not "loop, check, count, break")

      expensive <- cat items | filter by isExpensive | head 2 | collect as List of String
      stdout.println(`First 2 expensive: ${expensive}`)

Java/Python/Rust/Go/JS: break, continue, return, switch fallthrough. EK9: five rules map to stream pipelines, guard expressions, function decomposition, comma-separated cases.

branch break condition continue control convert flow java mental migrate migration model pattern python return rewrite rule step swift translate

firstThree <- List() of String
      for item in items
        if isNonEmpty(item)
          firstThree += item
          if length firstThree >= 3
            break

Explanation: EK9 has no break statement. Migration Rule 1: break becomes head. Use stream pipelines with head N instead of loop-with-break-at-count. See ek9 -h E01070 for details.

nonEmpty <- List() of String
      for item in items
        if not isNonEmpty(item)
          continue
        nonEmpty += item

Explanation: EK9 has no continue statement. Migration Rule 2: continue becomes filter. Use stream pipelines with filter to keep only matching items. See ek9 -h E01071 for details.

<- found as String: String()
      return found

Explanation: EK9 has no return statement. Declare the return variable with a default value and conditionally assign. The compiler ensures all paths initialize the return variable. See ek9 -h E01072 for details.

items <- new List("", "apple", "banana", "", "cherry", "date", "elderberry", "fig", "grape")

Explanation: EK9 has no 'new' keyword. Types are instantiated by calling the constructor directly. Java's 'new ArrayList<>()' becomes List() of Type in EK9. See ek9 -h E01075 for details.


Question: Why does EK9 have a built-in Money type and how does it handle currency safety?

Why not use BigDecimal or a money library?

What happens when I add GBP to USD in EK9?

How does EK9 prevent money bugs that plague other languages?

Why is Money built into EK9 instead of being a library?

Answer:
EK9 makes Money a built-in type because money is almost always implemented incorrectly in other languages. This is not a theoretical concern — it is the norm across the industry.

COMMON MONEY BUGS IN OTHER LANGUAGES
  - Using float or double: 0.1 + 0.2 != 0.3 in IEEE 754
  - Using BigDecimal but forgetting the rounding mode on one operation out of hundreds
  - Using BigDecimal with the double constructor: new BigDecimal(0.1) produces 0.100000000000000005551... not 0.1
  - Hardcoding 2 decimal places everywhere, then failing for JPY (0 decimals), BHD (3 decimals), or CLF (4 decimals)
  - Storing currency code separately from amount, then mixing them up during refactoring
  - Silently adding GBP to USD and getting a plausible but completely wrong number
These bugs are among the most expensive in software because they go undetected for months — the numbers look close enough until an audit finds millions in discrepancies.

MIXED CURRENCY RETURNS UNSET
Adding GBP to USD does not throw an exception. It returns an unset Money value:
  mixed <- 10#GBP + 30#USD    mixed is unset (mixed? is false)
  compare <- 10#GBP == 30#USD  compare is unset (not true, not false)
This follows the EK9 tri-state pattern. The compiler cannot catch this at compile time because variables could hold any currency at runtime. Instead, mixed currency operations return unset — forcing you to check before using the result.

DIVISION BY ZERO RETURNS UNSET
  bad <- 100#GBP / 0    bad is unset (bad? is false)
Operations that cannot produce a meaningful result return unset rather than crashing.

WHAT EK9 ELIMINATES
The literal 10#GBP encodes amount, currency, and precision in a single expression. The compiler knows all ISO 4217 currencies and their correct decimal places. Rounding is automatic and consistent (HALF_UP). Mixed currency operations return unset instead of silently producing wrong answers. There is nothing to forget, nothing to configure, and no way to accidentally use the wrong precision.

See Q35 for Money arithmetic and literals. See Q150 for currency conversion and locale formatting. See Q29 for the tri-state (absent/unset/set) model.

defines module qa.money.safety

  defines program
    MoneySafetyDemo()
      stdout <- Stdout()

      tenPounds <- 10#GBP
      thirtyDollars <- 30.20#USD

      // === MIXED CURRENCY RETURNS UNSET ===

      // Cannot accidentally add GBP to USD — returns unset, not an exception
      mixedResult <- tenPounds + thirtyDollars
      require ~mixedResult?
      stdout.println(`Mixed currency isSet: ${mixedResult?}`)

      // Comparison across currencies also returns unset
      mixedCompare <- tenPounds == thirtyDollars
      require ~mixedCompare?
      stdout.println(`Mixed compare isSet: ${mixedCompare?}`)

      // === DIVISION BY ZERO RETURNS UNSET ===

      badResult <- tenPounds / 0
      require ~badResult?
      stdout.println(`Div by zero isSet: ${badResult?}`)

      // === SAME CURRENCY WORKS NORMALLY ===

      total <- tenPounds + 89.51#GBP
      require total?
      require total == 99.51#GBP
      stdout.println(`Same currency total: ${total}`)

      // === GUARD PATTERN FOR SAFE MONEY OPERATIONS ===

      // Use guard to safely handle potentially unset results
      if safeTotal <- tenPounds + 5#GBP
        stdout.println(`Safe total: ${safeTotal}`)

      // Mixed currency — guard body does NOT execute
      if unsafeTotal <- tenPounds + thirtyDollars
        stdout.println("This never prints")
      else
        stdout.println("Mixed currency detected via guard")

Java: BigDecimal with explicit RoundingMode at every operation, Currency class separate from amount, mixed currency not detected. Python: decimal.Decimal requires context for rounding, no currency awareness, money libraries (py-moneyed) needed. JavaScript: IEEE 754 floating-point causes 0.1+0.2!=0.3 bugs, Dinero.js or similar needed. Ruby: no built-in, money gem needed. Go: no built-in, shopspring/decimal or similar. Rust: no built-in, rust_decimal crate. C#: decimal type has precision but no currency. EK9: built-in Money type eliminates every one of these mistakes at the language level.

beginner bigdecimal bug built-in currency financial first float intro iso4217 migrate mixed money precision rounding safety start unset

Explanation: EK9 has no getCurrency() method on Money. Use the #> (extract-right) prefix operator to get the currency code as a String. AI models from Java and Python generate getter methods that do not exist in EK9. See ek9 -h E50060 for details.

Explanation: EK9 Money uses operators for arithmetic, not method calls. There is no add() method. Use + for addition, - for subtraction. AI models transfer Java BigDecimal patterns that do not exist in EK9. See ek9 -h E50060 for details.

Explanation: EK9 has no getAmount() method on Money. Use the #< (extract-left) prefix operator to extract the numeric amount as a Float. AI models transfer Java BigDecimal getter patterns that do not exist in EK9. See ek9 -h E50060 for details.

Explanation: EK9 Money handles rounding automatically using HALF_UP and the currency's ISO 4217 decimal places. There is no setScale() or round() method. GBP uses 2 decimals, JPY uses 0, CLF uses 4 — all handled automatically. See ek9 -h E50060 for details.


Question: How do I convert currencies and format money for display in EK9?

How do I convert between currencies in EK9?

How do I format money with locale in EK9?

How do I sum a list of money values in EK9?

How does locale affect money display in EK9?

Answer:
EK9 provides currency conversion, locale-aware formatting, and stream collection for Money values.

CURRENCY CONVERSION
Two overloads for converting between currencies:
  inUSD <- amount.convert(1.32845, "USD")    Rate + target currency string
  inUSD <- amount.convert(1.32845#USD)        Rate embedded in money literal
Both multiply the amount by the rate and return a new Money value in the target currency, rounded to that currency's precision.

LOCALE FORMATTING
Four levels of locale-aware formatting using the Locale type:
  locale.format(money)         Full: currency symbol + amount + fractions
  locale.longFormat(money)     No symbol: amount + fractions
  locale.mediumFormat(money)   No fractions: currency symbol + amount
  locale.shortFormat(money)    Neither: amount only
The same money value formats differently in different locales:
  en_GB: GBP 10.00    de_DE: 10,00 GBP

STREAM COLLECTION
Collect a stream of money values into a total:
  amounts as List of Money := [10#GBP, 89.51#GBP, 49.76#GBP]
  total <- cat amounts | collect as Money
The collect operation adds all values in the stream. All values must be the same currency — mixed currencies produce an unset result.

See Q35 for Money arithmetic and literals. See Q149 for currency safety and why Money is built-in. See Q44 for the complete Locale type API. See Q89 for stream pipeline basics.

defines module qa.money.formatting

  defines program
    MoneyFormattingDemo()
      stdout <- Stdout()

      tenPounds <- 10#GBP
      amounts as List of Money := [tenPounds, 89.51#GBP, 49.76#GBP]

      // === STREAM COLLECTION ===

      streamTotal <- cat amounts | collect as Money
      require streamTotal == 159.27#GBP
      stdout.println(`Stream total: ${streamTotal}`)

      // === CURRENCY CONVERSION ===

      // Convert with exchange rate and target currency
      totalInUSD <- streamTotal.convert(1.32845, "USD")
      require totalInUSD == 211.58#USD
      stdout.println(`Converted: ${totalInUSD}`)

      // Alternative syntax: convert with Money literal
      alsoUSD <- streamTotal.convert(1.32845#USD)
      require alsoUSD == 211.58#USD

      // === LOCALE FORMATTING ===

      enGB <- Locale("en_GB")
      deutsch <- Locale("de_DE")

      // Four format levels
      stdout.println(`Full: ${enGB.format(tenPounds)}`)
      stdout.println(`No symbol: ${enGB.longFormat(tenPounds)}`)
      stdout.println(`No fractions: ${enGB.mediumFormat(tenPounds)}`)
      stdout.println(`Neither: ${enGB.shortFormat(tenPounds)}`)

      // Same currency, different locale
      stdout.println(`German: ${deutsch.format(tenPounds)}`)

Java: BigDecimal with explicit RoundingMode, NumberFormat for locale, Currency class separate from amount. Python: decimal.Decimal + locale module, babel for formatting. JavaScript: Intl.NumberFormat for locale, no built-in currency conversion. Rust: no built-in, external crates for both. Go: no built-in, golang.org/x/text for formatting. EK9: built-in convert() method with two overloads, four locale formatting levels, stream collection with cat|collect as Money.

beginner collect conversion convert currency display exchange first format formatting intro locale money rate start stream

Explanation: Formatting is done via the Locale type, not a method on Money. Create a Locale then call locale.format(money). Four levels available: format, longFormat, mediumFormat, shortFormat. See ek9 -h E50060 for details.

Explanation: EK9 has no toString() method. Use string interpolation or the $ prefix operator for string conversion. See ek9 -h E50060 for details.

Explanation: Currency conversion requires an exchange rate. The convert method takes either (Float, String) or (Money) — the rate is always required. There is no automatic rate lookup. See ek9 -h E50060 for details.


Question: How do I read a text file in EK9?

How do I open and read a file in EK9?
What is TextFile in EK9?
How do I load file contents in EK9?

Answer:
EK9 provides the TextFile type for reading and writing text files. Create a TextFile with a path string, then use try-with-resources to open an input stream and read the contents safely.

CREATING A TEXTFILE
Create a TextFile by passing a file path as a String:
  file <- TextFile("data.txt")
The file is not opened until you call input() or output(). The TextFile just records the path.

READING WITH INPUT STREAM
Use file.input() to get a CloseableStringInput stream:
  try
    -> input <- file.input()
    cat input > stdout
The input stream provides lines from the file one at a time. You can pipe them through stream operations or consume them directly.

TRY WITH RESOURCES
Always open file streams inside a try block. EK9's try-with-resources automatically closes the stream when the block exits, whether normally or via exception. This prevents resource leaks.

HANDLING FILE NOT FOUND
If the file does not exist, file.input() returns an unset value. The try guard (try -> input <- file.input()) only enters the body if input is set, so you naturally handle missing files without explicit null checks.

See Q3 for printing output. See Q4 for reading stdin. See Q134 for try/catch. See Q152 for writing files. See Q153 for file path exists. See Q154 for read file lines.

defines module qa.fileio.read

  defines function

    processLine() as pure
      -> line as String
      <- rtn as String: "[read] " + line

  defines program

    ReadTextFileDemo()
      stdout <- Stdout()

      // === CREATING A TEXTFILE ===

      file <- TextFile("/tmp/qa-example-input.txt")
      stdout.println("File: " + $file)

      // === READING WITH TRY-WITH-RESOURCES ===

      // If the file exists, input() returns a set stream
      // The try guard only enters the body if input is set
      stdout.println("Attempting to read file")

      // === TEXTFILE PROPERTIES ===

      // TextFile has useful query methods
      stdout.println("Is readable: " + $file.isReadable())

      // === STREAM PROCESSING ===

      // You can also pipe file input through stream operations
      // cat input | map with processLine | head 5 > stdout

Java: Files.readString(Path) or BufferedReader with try-with-resources. Python: with open('file') as f: content = f.read(). Rust: fs::read_to_string('file'). Go: os.ReadFile('file'). EK9: TextFile('file') with try -> input <- file.input() for automatic resource management and guard-based missing file handling.

content file input load open read stream text textfile write

Explanation: TextFile has no toString() method. Use the $ prefix operator for string conversion. See ek9 -h E50060 for details.

stdout.println("Is readable: " + $file.canRead())

Explanation: TextFile has no canRead() method. Use isReadable() to check if the file can be read. EK9 uses descriptive method names. See ek9 -h E50060 for details.


Question: How do I write text to a file in EK9?

How do I create and write a text file in EK9?

Answer:
EK9 uses TextFile with output() to write text to files. Create a TextFile with the target path, open an output stream inside try-with-resources, and write lines using the stream.

CREATING OUTPUT
Use file.output() to get a CloseableStringOutput stream:
  file <- TextFile("output.txt")
  try
    -> output <- file.output()
    output.println("Hello, file!")
The output stream is automatically closed when the try block exits.

WRITING LINES
The output stream supports println() for writing lines of text. You can write multiple lines in sequence:
  output.println("Line one")
  output.println("Line two")

CLOSING RESOURCES
The try-with-resources pattern ensures the file is properly flushed and closed, even if an exception occurs during writing. You never need to explicitly close the stream.

TRY WITH RESOURCES FOR OUTPUT
Just like reading, writing uses the try guard pattern:
  try
    -> output <- file.output()
    // write data here
  catch
    -> ex as Exception
    stderr.println("Write failed: " + $ex)
If the file cannot be opened for writing, output() returns an unset value and the try body is skipped.

See Q151 for reading files. See Q134 for try/catch. See Q3 for stdout.

defines module qa.fileio.write

  defines program

    WriteTextFileDemo()
      stdout <- Stdout()

      // === CREATING A TEXTFILE FOR WRITING ===

      file <- TextFile("/tmp/qa-example-output.txt")
      stdout.println("Output file: " + $file)

      // === FILE PROPERTIES ===

      stdout.println("Is writable: " + $file.isWritable())

      // === WRITING PATTERN ===

      // The standard pattern for writing:
      //   try
      //     -> output <- file.output()
      //     output.println("Hello, file!")
      //   catch
      //     -> ex as Exception
      //     stderr.println("Write failed")

      // TextFile also supports checking file state
      stdout.println("File length: " + $file.length())

Java: Files.writeString(Path, content) or BufferedWriter with try-with-resources. Python: with open('file', 'w') as f: f.write(content). Rust: fs::write('file', content). Go: os.WriteFile('file', data, perm). EK9: TextFile('file') with try -> output <- file.output() for safe, auto-closing file writing.

create file output read save stream text textfile write

stdout.println("Is writable: " + $file.canWrite())

Explanation: TextFile has no canWrite() method. Use isWritable() to check if the file can be written. EK9 uses descriptive method names. See ek9 -h E50060 for details.


Question: How do I check if a file or directory exists in EK9?

How do I use FileSystemPath in EK9?
How do I check if a path exists in EK9?
How do I work with file paths in EK9?

Answer:
EK9 provides the FileSystemPath type for working with file system paths. It supports checking existence, distinguishing files from directories, and performing path operations.

FILESYSTEMPATH TYPE
Create a FileSystemPath from a string:
  path <- FileSystemPath("/home/user/data.txt")
FileSystemPath represents a location on the file system without opening or reading the file.

CHECKING EXISTENCE
Use exists() to check if the path points to something on disk:
  if path.exists()
    stdout.println("Path exists")
The exists() method returns a Boolean that is set to true or false.

FILE VS DIRECTORY
Distinguish between files and directories:
  if path.isFile()
    stdout.println("It is a file")
  else if path.isDirectory()
    stdout.println("It is a directory")
You can also check isReadable(), isWritable(), and isExecutable().

PATH OPERATIONS
FileSystemPath supports several path manipulation methods:
  abs <- path.absolutePath()
  name <- path.fileName()
  parent <- path.parent()
  joined <- path + "subdir"
Use startsWith() and endsWith() for prefix and suffix checks.

See Q42 for the Path type. See Q151 for reading files. See Q152 for writing files.

defines module qa.fileio.path

  defines program

    FilePathExistsDemo()
      stdout <- Stdout()

      // === CREATING FILESYSTEMPATH ===

      path <- FileSystemPath("/tmp")
      stdout.println("Path: " + $path)

      // === CHECKING EXISTENCE ===

      stdout.println("Exists: " + $path.exists())

      // === FILE VS DIRECTORY ===

      stdout.println("Is file: " + $path.isFile())
      stdout.println("Is directory: " + $path.isDirectory())

      // === PATH PROPERTIES ===

      stdout.println("Is readable: " + $path.isReadable())
      stdout.println("Is writable: " + $path.isWritable())
      stdout.println("Is absolute: " + $path.isAbsolute())

      // === PATH OPERATIONS ===

      absolutePath <- path.absolutePath()
      stdout.println("Absolute: " + $absolutePath)

      // Path concatenation with + operator
      subPath <- path + "subdir"
      stdout.println("Sub path: " + $subPath)

Java: Files.exists(Path) and Path for path operations. Python: os.path.exists() or pathlib.Path. Rust: Path::exists() and std::path::Path. Go: os.Stat() for existence checks. EK9: FileSystemPath('/path') with exists(), isFile(), isDirectory() methods and + operator for path joining.

check directory exists file filesystem folder isDirectory isFile path read write

Explanation: FileSystemPath constructor expects a String argument. Passing an Integer triggers E50060 — constructor not resolved because no FileSystemPath(Integer) exists. File paths are always represented as strings in EK9. See ek9 -h E50060 for details.

stdout.println("Exists: " + $path.fileExists())

Explanation: FileSystemPath has no fileExists() method. Use exists() to check if the path points to something on disk. See ek9 -h E50060 for details.

Explanation: FileSystemPath has no getAbsolutePath() method. Use absolutePath() to get the absolute path. EK9 uses descriptive method names rather than Java-style getters. See ek9 -h E50060 for details.


Question: How do I read a file line by line in EK9?

How do I process a file line by line in EK9?

How do I use cat with file input in EK9?

Answer:
EK9 treats file input as a stream source. Open a file with TextFile.input() inside a try block, then use cat to pipe lines through stream operations like filter, map, and collect.

STREAMING FILE INPUT
Open the file and stream its contents:
  file <- TextFile("data.txt")
  try
    -> input <- file.input()
    cat input > stdout
Each line from the file flows through the pipeline as a String.

CAT WITH FILE INPUT
The cat operation treats the input stream as a source of lines:
  cat input | map with processLine > stdout
This sends each line through the processLine function before printing.

FILTER AND TRANSFORM LINES
Combine file streaming with pipeline operations:
  cat input | filter by notEmpty | map with trim > stdout
This skips empty lines and trims whitespace from each remaining line.

COLLECT LINES INTO LIST
Gather all lines into a collection:
  cat input | collect as List of String
This reads every line and stores them in a List for further processing.

See Q89 for stream pipelines. See Q151 for reading files. See Q125 for head/tail/skip.

defines module qa.fileio.lines

  defines function

    notEmpty() as pure
      -> line as String
      <- rtn as Boolean: line?

    addLineNumber() as pure
      -> line as String
      <- rtn as String: ">> " + line

  defines program

    ReadFileLinesDemo()
      stdout <- Stdout()

      // === TEXTFILE FOR LINE READING ===

      file <- TextFile("/tmp/qa-example-lines.txt")
      stdout.println("File for line reading: " + $file)

      // === STREAMING PATTERN ===

      // The standard pattern for line-by-line processing:
      //   try
      //     -> input <- file.input()
      //     cat input | filter by notEmpty | map with addLineNumber > stdout

      // === FILTER AND TRANSFORM ===

      // Stream operations work naturally with file input:
      //   cat input | filter by notEmpty | head 10 > stdout

      // === COLLECT INTO LIST ===

      // Gather lines into a collection:
      //   cat input | collect as List of String

      stdout.println("Line-by-line processing uses cat with file input streams")

Java: Files.lines(Path) returns a Stream of String for lazy line-by-line processing. Python: for line in open('file') iterates lazily. Rust: BufRead::lines() returns an iterator of lines. Go: bufio.Scanner for line-by-line reading. EK9: cat input with TextFile.input() in a stream pipeline, supporting filter, map, collect and all stream operations.

cat file filter iterate line lines pipeline read stream write

Explanation: TextFile has no readLines() method. Use file.input() inside a try block to get a stream for line-by-line processing. See ek9 -h E50060 for details.


Question: How do I write a unit test in EK9?

Does EK9 have a built-in test framework?

Answer:
EK9 has built-in testing support with no external framework needed. Mark any program with @Test to make it a test. Group related tests with @Test: "groupName".

THE @TEST DIRECTIVE
Place @Test on its own line before a program to mark it as a test:
  defines program
    @Test
    SimpleTest()
      stdout <- Stdout()
      stdout.println("Test passed")
The @Test annotation must be on its own line, not inline.

TEST PROGRAMS
Tests are just programs. They run in the same way as normal programs but are collected and executed by the test runner. Any program marked @Test is included when you run ek9 -t file.ek9.

TEST GROUPS
Group related tests with @Test: "groupName":
  @Test: "database"
  DbConnectionTest()
    // test database connection
  @Test: "database"
  DbQueryTest()
    // test queries
Run a specific group with ek9 -tg database file.ek9. Tests in the same group run sequentially.

BLACK BOX TESTS
For output-based testing, print expected values to stdout and compare against an expected_output.txt file. This avoids assert bytecode contamination and provides clean test verification.

NO EXTERNAL FRAMEWORK NEEDED
Unlike Java (JUnit), Python (pytest), or Rust (#[test] with cargo), EK9 testing is built into the language. No dependencies to add, no test runner to install.

See Q156 for assertions. See Q157 for running tests. See Q2 for compile and run. See Q204 for black-box testing. See Q209 for exception testing.

defines module qa.testing.basics

  defines program

    // === BASIC TEST ===

    @Test
    SimpleArithmeticTest()
      stdout <- Stdout()
      result <- 2 + 3
      assert result == 5
      stdout.println("2 + 3 = " + $result)

    // === GROUPED TESTS ===

    @Test: "strings"
    StringConcatenationTest()
      stdout <- Stdout()
      full <- `Hello World`
      assert full == "Hello World"
      stdout.println("Concatenation: " + full)

    @Test: "strings"
    StringLengthTest()
      stdout <- Stdout()
      text <- "EK9"
      assert text.length() == 3
      stdout.println("Length: " + $text.length())

    // === TEST WITH SETUP ===

    @Test
    ListOperationsTest()
      stdout <- Stdout()
      items <- List() of Integer
      items += 10
      items += 20
      items += 30
      assert items.length() == 3
      stdout.println("List size: " + $items.length())

Java: JUnit @Test annotation with separate framework dependency. Python: pytest or unittest with class-based test structure. Rust: #[test] attribute with cargo test runner. Go: func TestXxx(t *testing.T) naming convention. EK9: built-in @Test directive on programs, no framework dependency, optional grouping with @Test: "group".

assert built-in directive framework group migrate program test testing unit verify

Explanation: Integer has no toString() method. Use the $ prefix operator for string conversion or string interpolation. See ek9 -h E50060 for details.

stdout.println("Length: " + $text.size())

Explanation: String has no size() method. Use length() to get the number of characters in a string. See ek9 -h E50060 for details.


Question: How do I assert conditions in a test in EK9?

What is the difference between assert and require in EK9?

Answer:
EK9 provides assert for test assertions and require for production preconditions. Both are built-in keywords, not library functions.

ASSERT KEYWORD
Use assert in test programs to verify conditions:
  assert result == 5
  assert name?
  assert items.length() > 0
Assert is only allowed in test context (programs marked with @Test). It throws an exception if the condition is false.

REQUIRE FOR PRECONDITIONS
Use require in production code for preconditions:
  require name?
  require age > 0
Require works in any context and enforces that callers provide valid inputs.

ASSERT VS REQUIRE
assert is for test verification: 'did the code produce the right result?'
require is for defensive programming: 'are the inputs valid before proceeding?'
Assert is restricted to test programs. Require works everywhere.

OUTPUT-BASED TESTING AS ALTERNATIVE
For cleaner tests, print values to stdout and compare against expected_output.txt files. This avoids assert bytecode contamination in @BYTECODE directives and provides two independent validations: output verifies runtime behavior, directives verify code generation.

See Q155 for writing tests. See Q134 for try/catch. See Q135 for throwing exceptions. See Q204 for black-box testing. See Q209 for exception testing.

See Q305 for require vs assert vs throw. See Q306 for assertThrows. See Q307 for assertDoesNotThrow.

defines module qa.testing.assertions

  defines function

    add() as pure
      ->
        a as Integer
        b as Integer
      <- rtn as Integer: a + b

    isPositive() as pure
      -> number as Integer
      <- rtn as Boolean: number > 0

  defines program

    // === ASSERT IN TESTS ===

    @Test
    BasicAssertTest()
      stdout <- Stdout()

      result <- add(2, 3)
      assert result == 5

      assert isPositive(42)

      name <- "EK9"
      assert name?

      stdout.println("All assertions passed")

    // === REQUIRE FOR PRECONDITIONS ===

    @Test
    RequireTest()
      stdout <- Stdout()

      count <- 10
      require count > 0

      text <- "Hello"
      require text?

      assert count == 10
      assert text == "Hello"
      stdout.println("All preconditions met")

    // === OUTPUT-BASED ALTERNATIVE ===

    @Test
    OutputBasedTest()
      stdout <- Stdout()

      // Print values and compare against expected_output.txt
      result <- add(10, 20)
      assert result == 30
      stdout.println("Result: " + $result)

Java: assertEquals/assertTrue from JUnit, assertThrows for exceptions. Python: assert statement or pytest.raises for exceptions. Rust: assert!, assert_eq!, assert_ne! macros. Go: t.Error() and t.Fatal() on testing.T. EK9: built-in assert keyword for tests, require keyword for preconditions, plus output-based testing as a cleaner alternative.

assert assertion check condition precondition require test throws verify

Explanation: The assert keyword requires a Boolean expression. Passing an Integer or String directly triggers E07530 — only compatible with Boolean type. Use a comparison operator to produce a Boolean. See ek9 -h E07530 for details.

Explanation: Integer has no valueOf() method. Use the $ prefix operator or string concatenation with + for string conversion. See ek9 -h E50060 for details.


Question: How do I run tests and see results in EK9?

How do I run EK9 tests from the command line?

What test output formats does EK9 support?

Answer:
EK9 provides a built-in test runner with multiple output formats and coverage reporting. Use the -t flag to run tests from the command line.

THE -t FLAG
Run all tests in a file:
  ek9 -t file.ek9
This runs every @Test program and reports results in human-readable format.

OUTPUT FORMATS
EK9 supports multiple output formats:
  ek9 -t file.ek9    # Human-readable (default)
  ek9 -t0 file.ek9   # Terse (pass/fail only)
  ek9 -t2 file.ek9   # JSON (for AI/tool parsing)
  ek9 -t3 file.ek9   # JUnit XML (for CI integration)

RUNNING TEST GROUPS
Run a specific test group:
  ek9 -tg database file.ek9
This runs only tests marked with @Test: "database".

LISTING TESTS
List all tests without running them:
  ek9 -tL file.ek9

COVERAGE
Run tests with coverage reporting:
  ek9 -tC file.ek9   # Coverage summary after tests
  ek9 -t4 file.ek9   # Detail JSON coverage
  ek9 -t5 file.ek9   # Verbose coverage
  ek9 -t6 file.ek9   # HTML coverage report
Coverage must be at least 80% for packaging with -P.

EXIT CODES
The test runner uses specific exit codes:
  0  = all tests pass, coverage OK
  11 = one or more tests failed
  12 = tests pass but coverage below 80%

See Q155 for writing tests. See Q156 for assertions. See Q2 for compile and run. See Q204 for black-box testing. See Q206 for test coverage. See Q322 for profiling data. See Q628 for profiling tests. See Q749 for fuzzing overview. See Q754 for automatic test generation.

defines module qa.testing.running

  defines function

    multiply() as pure
      ->
        first as Integer
        second as Integer
      <- rtn as Integer: first * second

  defines program

    // === BASIC TEST ===

    @Test
    MultiplyTest()
      stdout <- Stdout()
      result <- multiply(6, 7)
      assert result == 42
      stdout.println("6 * 7 = " + $result)

    // === GROUPED TEST ===

    // Run this group only with: ek9 -tg math file.ek9
    @Test: "math"
    AdditionTest()
      stdout <- Stdout()
      assert 2 + 2 == 4
      stdout.println("Addition test passed")

    @Test: "math"
    SubtractionTest()
      stdout <- Stdout()
      assert 10 - 3 == 7
      stdout.println("Subtraction test passed")

    // === DEMONSTRATING TEST STRUCTURE ===
    // Run all tests:       ek9 -t file.ek9
    // Run math group only: ek9 -tg math file.ek9
    // JSON output:         ek9 -t2 file.ek9
    // JUnit XML output:    ek9 -t3 file.ek9
    // With coverage:       ek9 -tC file.ek9

Java: mvn test or gradle test with JUnit, JaCoCo for coverage. Python: pytest with pytest-cov for coverage. Rust: cargo test with tarpaulin for coverage. Go: go test -v with go test -cover. EK9: built-in ek9 -t runner with JSON/JUnit XML output formats and integrated coverage via -tC flags, no external tools needed.

assert compile coverage format group json junit output result results run runner test verify

Explanation: Integer has no intValue() method. multiply() already returns an Integer. See ek9 -h E50060 for details.

Explanation: Integer has no toString() method. Use the $ prefix operator for string conversion or string concatenation. See ek9 -h E50060 for details.


Question: How do I protect shared data from concurrent access in EK9?

How does MutexLock protect shared state in EK9?

Answer:
EK9 uses MutexLock of T to protect shared data from concurrent access. The canonical pattern is to hold the MutexLock as a FIELD on a wrapping class, and expose access through methods that internally use MutexKey callbacks. The lock itself never escapes the wrapping class — it has a stable field-rooted home that the compiler tracks for data-race and deadlock detection.

FIELD-ON-CLASS PATTERN
Declare the MutexLock as a field with an inline initialiser:
  Counter
    lockedCount as MutexLock of Integer: MutexLock(0)

The field initialiser is the only legal MutexLock construction site (E08254/E08256 reject locals and collections of locks).

MUTEXKEY CALLBACK PATTERN
Inside each method that needs to read or update the protected value, declare a dynamic MutexKey function. The function body is the critical section; lockedItem is the protected value while the lock is held:
  read()
    <- rtn as Integer: Integer()
    accessKey <- (rtn) is MutexKey of Integer as function
      rtn :=: lockedItem
    require lockedCount.enter(accessKey)

When the callback returns, the lock is released automatically. This prevents forgetting to unlock.

ENTER VS TRYENTER
enter() blocks until the lock is available. tryEnter() attempts to acquire the lock and returns immediately with a Boolean indicating success:
  blocking <- lockedCount.enter(keyFunction)
  attempt <- lockedCount.tryEnter(keyFunction)

COPY NOT REASSIGN
Inside a MutexKey callback, use the copy operator :=: to update lockedItem, not regular assignment :=. Reassignment would leave an outside reference to the original value, bypassing the lock.

WHY FIELD-ON-CLASS
EK9's compile-time data-race detection needs every lock to have a stable field-rooted home. Locks declared as locals, returned from functions, or held inside collections defeat the static analysis. Wrapping the lock in a class also keeps the API surface small — callers see only the protected operations.

See Q117 for singleton and DI for shared components. See Q159 for parallel processing. See Q160 for the concurrency model. See Q213 for MutexLock design pattern.

defines module qa.concurrency.mutex

  defines class

    //Canonical pattern: MutexLock as a field on a wrapping class.
    //The class exposes protected operations as methods; the lock
    //never escapes.
    Counter
      lockedCount as MutexLock of Integer: MutexLock(0)

      read()
        <- rtn as Integer: Integer()

        accessKey <- (rtn) is MutexKey of Integer as function
          rtn :=: lockedItem

        require lockedCount.enter(accessKey)

      increment()
        -> amount as Integer

        accessKey <- (amount) is MutexKey of Integer as function
          lockedItem :=: lockedItem + amount

        require lockedCount.enter(accessKey)

      default operator ?

  defines program

    MutexLockDemo()
      stdout <- Stdout()

      //Construct the wrapper — the MutexLock is created by the
      //class's field initialiser, not here in user code.
      counter <- Counter()
      stdout.println(`Initial count: ${counter.read()}`)

      counter.increment(5)
      stdout.println(`After +5: ${counter.read()}`)

      counter.increment(10)
      stdout.println(`After +10: ${counter.read()}`)

Java: synchronized blocks or ReentrantLock with try/finally. Python: threading.Lock with context manager. Rust: Mutex<T> with lock() returning a guard. Go: sync.Mutex with Lock()/Unlock(). EK9: MutexLock of T held as a class field, accessed via MutexKey callback inside class methods. The field-rooted home enables compile-time data-race and deadlock detection that other languages lack.

class concurrent critical field lock mutex mutexlock parallel protect safety section shared thread

mutex
lock
thread safety
synchronization

counter <- Counter()
      strayLock <- MutexLock(0)
      assert strayLock?

Explanation: MutexLock must be declared as a field on a class with an inline initialiser. Constructing one as a local (here `strayLock <- MutexLock(0)`) is rejected at SYMBOL_DEFINITION (E08256). The lock must have a stable field-rooted home so the compiler can track its identity for data-race and deadlock analysis.

counter <- Counter()
      badLocks <- List() of MutexLock of Integer
      assert badLocks?

Explanation: MutexLock cannot appear as a type argument to another generic (here `List of MutexLock`). Use one MutexLock per shared payload, held as a field, rather than a collection of locks. See ek9 -h E08254 for details.