Question: How do I use a guard expression as a standalone assignment that returns Boolean?

Can I use ?= outside of an if or while statement?

How does the guard expression ?= return a Boolean result?

What does guardResult <- name ?= getValue() do in EK9?

Answer:
The guard expression operator (?=) can be used as a standalone assignment expression that returns a Boolean. The expression evaluates the right-hand side (RHS), checks if it is SET, and if so assigns it to the target variable. The Boolean result indicates whether the assignment occurred.

STANDALONE GUARD EXPRESSION
Unlike using ?= in control flow guards (if, while), this pattern captures the Boolean result:
  name as String?
  guardResult <- name ?= getValue()
Here guardResult is a Boolean: true if getValue() returned a set value (and name was assigned), false otherwise.

WHEN THE RHS IS SET
If getValue() returns a set String, the assignment happens and the result is true:
  name as String?
  result <- name ?= getValue()
  // result is true, name is now the returned value

WHEN THE RHS IS UNSET
If the RHS returns an unset value, no assignment occurs and the result is false:
  existing <- "already here"
  result <- existing ?= getEmpty()
  // result is false, existing is unchanged

KEY DIFFERENCE FROM :=? AND ?= IN GUARDS
The three related patterns:
  name :=?  checks TARGET isSet, assigns if unset
  if name ?=  checks SOURCE isSet, controls flow
  result <- name ?=  checks SOURCE isSet, returns Boolean
The standalone form returns Boolean rather than controlling a block.

See Q74 for guard variables in if. See Q77 for guard variables in while. See Q79 for guarded assignment :=? operator.

defines module qa.flow.guard.expression.boolean

  defines function

    getValue()
      <- rtn <- String()
      rtn: "hello"

    getEmpty()
      <- rtn <- String()

  defines program

    GuardExpressionBooleanDemo()
      stdout <- Stdout()

      // === GUARD EXPRESSION WITH SET RHS ===

      name as String?
      guardResult <- name ?= getValue()
      stdout.println(guardResult)
      stdout.println(name)

      // === GUARD EXPRESSION WITH UNSET RHS ===

      existing <- "already here"
      guardResult2 <- existing ?= getEmpty()
      stdout.println(guardResult2)
      stdout.println(existing)

      stdout.println("Done")

Java: No direct equivalent. Must write: 'Optional<String> opt = getValue(); boolean assigned = opt.isPresent(); if (assigned) name = opt.get();'. Three separate statements. Kotlin: 'val result = getValue()?.also { name = it } != null'. Requires nullable types and scope functions. Python: 'result = getValue(); assigned = result is not None; name = result if assigned else name'. Manual and error-prone. EK9: 'result <- name ?= getValue()' is a single expression combining assignment and Boolean result.

assignment boolean check conditional expression guard isset null-safe operator result return safe source standalone

guardResult as String: name ?= getValue()

Explanation: The guard expression ?= returns a Boolean, not the type of the assigned value. Declaring the result as String triggers E50030 because Boolean and String are incompatible. Use type inference with '<-' to get the correct Boolean type. See ek9 -h E50030 for details.


Question: How do I use guarded assignment :=? on an external object field?

Can I use :=? on a record field via dot access?

How does guarded assignment work on config.host?

What does object.field :=? value do in EK9?

Answer:
The guarded assignment operator (:=?) works on external object fields accessed via dot notation. It checks whether the field is currently SET, and only assigns if the field is UNSET. This is useful for providing default values to record or class fields without overwriting existing values.

BASIC EXTERNAL FIELD GUARD
Assign default values to record fields:
  config <- Config()
  config.host :=? "localhost"
  config.port :=? 8080
If host and port are unset (default-constructed), the defaults are applied.

PRESERVING EXISTING VALUES
If a field already has a value, the guarded assignment is skipped:
  config <- Config()
  config.host :=? "localhost"
  config.host :=? "should-not-overwrite"
After both lines, config.host is still "localhost" because the second :=? finds the field already set.

CONFIGURATION PATTERN
A common use is applying layered defaults to a configuration object:
  config <- loadFromFile()
  config.host :=? loadFromEnv("HOST")
  config.port :=? loadFromEnv("PORT")
  config.host :=? "localhost"
  config.port :=? 8080
File values win, then environment, then hardcoded defaults.

DIFFERENCE FROM LOCAL VARIABLE :=?
The semantics are identical — assign only when unset. The difference is that the compiler generates field access (getfield/putfield) rather than local variable access. Both check the isSet state of the target.

See Q79 for guarded assignment on local variables. See Q22 for variable declaration. See Q104 for uninitialised properties in classes.

defines module qa.flow.guard.external.field

  defines record

    Config
      host <- String()
      port <- Integer()

      default operator ?

  defines program

    GuardedExternalFieldDemo()
      stdout <- Stdout()

      // === BASIC EXTERNAL FIELD GUARD ===

      config <- Config()
      config.host :=? "localhost"
      config.port :=? 8080
      stdout.println(config.host)
      stdout.println(config.port)

      // === PRESERVING EXISTING VALUES ===

      config.host :=? "should-not-overwrite"
      config.port :=? 9090
      stdout.println(config.host)
      stdout.println(config.port)

      stdout.println("Done")

Java: No direct equivalent. Must write: 'if (config.getHost() == null) { config.setHost("localhost"); }'. Requires getter/setter boilerplate and manual null check. Kotlin: 'config.host = config.host ?: "localhost"'. Elvis operator handles null but requires reading the field twice. Go: 'if config.Host == "" { config.Host = "localhost" }'. Must know the zero value for each type. EK9: 'config.host :=? "localhost"' is a single operator that checks the field's isSet state and conditionally assigns. Works uniformly across all types.

access assignment conditional config default dot external field guarded isset object operator property record safe unset

config.host :=? 42
      config.port :=? "not a number"

Explanation: The guarded assignment operator :=? checks the target field's isSet state and conditionally assigns. The assigned value must be type-compatible with the field. Assigning an Integer to a String field (or vice versa) triggers E50030. See ek9 -h E50030 for details.


Question: What is the LCOM4 cohesion limit and how do I stay within it?

What is the LCOM4 threshold for classes?

Why does EK9 limit disconnected method groups?

Answer:
EK9 measures class cohesion using LCOM4 (Lack of Cohesion of Methods version 4). It builds a graph of method-to-field relationships — methods sharing at least one field are connected. The number of disconnected subgraphs is the LCOM4 score.

THRESHOLDS
- Class: max LCOM4 of 8 (E11014)
- Service: max LCOM4 of 10
- Component: max LCOM4 of 10

WHAT LCOM4 MEASURES
LCOM4 = 1 means perfect cohesion — all methods work together through shared fields. LCOM4 = 8 means 8 groups of methods that don't share any fields. Above the threshold, the class is doing too many unrelated things.

THIS EXAMPLE
The InventoryTracker class below has exactly 8 disconnected method groups (LCOM4 = 8), which is the maximum allowed for a class. Each method group accesses only its own field. Adding a 9th unrelated method would trigger E11014.

HOW TO FIX HIGH LCOM4
1. Extract each disconnected group into its own focused class
2. Add methods that bridge groups by accessing multiple fields
3. Consider if the class has a single clear purpose

See Q310 for code quality overview. See Q314 for cohesion and coupling details. See Q696 for complexity limits.

defines module qa.codequality.lcom4boundary

  defines class

    <?-
      This class has exactly 8 disconnected method groups (LCOM4 = 8).
      Each method accesses only one unique field.
      No method shares a field with another method.
      This is exactly at the class threshold — it compiles.
      Adding a 9th unrelated method/field pair would trigger E11014.
    -?>
    InventoryTracker
      field1 Integer: 1
      field2 Integer: 2
      field3 Integer: 3
      field4 Integer: 4
      field5 Integer: 5
      field6 Integer: 6
      field7 Integer: 7
      field8 Integer: 8

      method1()
        <- rtn Integer := field1

      method2()
        <- rtn Integer := field2

      method3()
        <- rtn Integer := field3

      method4()
        <- rtn Integer := field4

      method5()
        <- rtn Integer := field5

      method6()
        <- rtn Integer := field6

      method7()
        <- rtn Integer := field7

      method8()
        <- rtn Integer := field8

      override operator ? as pure
        <- rtn Boolean := field1?

  defines program

    Lcom4BoundaryDemo()
      stdout <- Stdout()
      tracker <- InventoryTracker()
      stdout.println($tracker.method1())
      stdout.println($tracker.method8())

Java: SonarQube measures LCOM but only as advisory metric. C#: NDepend reports LCOM informational only. Python: no cohesion analysis. Go: no cohesion measurement. Kotlin: Detekt does not measure LCOM. EK9: LCOM4 > 8 is a compiler error — class won't compile.

E11014 LCOM4 boundary class cohesion disconnect field method metric quality split threshold

LCOM4
cohesion
class design
code quality

      field8 Integer: 8

      method1()

      field8 Integer: 8
      field9 Integer: 9

      method9()
        <- rtn Integer := field9

      method1()

Explanation: Adding field9 and method9 creates a 9th disconnected method group (method9 accesses only field9). LCOM4 goes from 8 to 9, exceeding the class threshold of 8. Extract unrelated methods into separate classes. See ek9 -h E11014 for details.


Question: What is the efferent coupling limit and how do I stay within it?

What triggers E11015 excessive coupling?

Answer:
EK9 measures efferent coupling (Ce) — the number of distinct external types a construct depends on. When Ce exceeds the threshold, E11015 triggers.

THRESHOLDS
- Class: max Ce of 12
- Record: max Ce of 8
- Trait: max Ce of 8
- Service: max Ce of 15
- Component: max Ce of 15
- Function: max Ce of 8

THIS EXAMPLE
The HighCouplingClass below depends on exactly 12 distinct record types via fields. This is at the class threshold. Adding a 13th dependency triggers E11015.

See Q310 for code quality overview. See Q314 for cohesion and coupling details.

defines module qa.codequality.couplingboundary

  defines record

    Dep1
      mainValue Integer: 0
      default operator ?
    Dep2
      mainValue Integer: 0
      default operator ?
    Dep3
      mainValue Integer: 0
      default operator ?
    Dep4
      mainValue Integer: 0
      default operator ?
    Dep5
      mainValue Integer: 0
      default operator ?
    Dep6
      mainValue Integer: 0
      default operator ?
    Dep7
      mainValue Integer: 0
      default operator ?
    Dep8
      mainValue Integer: 0
      default operator ?
    Dep9
      mainValue Integer: 0
      default operator ?
    Dep10
      mainValue Integer: 0
      default operator ?
    Dep11
      mainValue Integer: 0
      default operator ?
    Dep12
      mainValue Integer: 0
      default operator ?
    Dep13
      mainValue Integer: 0
      default operator ?

  defines class

    HighCouplingClass
      d1 as Dep1: Dep1()
      d2 as Dep2: Dep2()
      d3 as Dep3: Dep3()
      d4 as Dep4: Dep4()
      d5 as Dep5: Dep5()
      d6 as Dep6: Dep6()
      d7 as Dep7: Dep7()
      d8 as Dep8: Dep8()
      d9 as Dep9: Dep9()
      d10 as Dep10: Dep10()
      d11 as Dep11: Dep11()
      d12 as Dep12: Dep12()

      allSet()
        <- rtn as Boolean: d1? and d2? and d3? and d4? and d5? and d6? and d7? and d8? and d9? and d10? and d11? and d12?

      default operator ?

  defines program

    CouplingBoundaryDemo()
      stdout <- Stdout()
      tracker <- HighCouplingClass()
      stdout.println($tracker.allSet())

Java: no coupling limit in javac; SonarQube reports Ce as informational. C#: NDepend measures Ce but does not enforce. Python: no coupling metrics. Go: no coupling enforcement. EK9: Ce exceeding threshold is a compiler error.

Ce E11015 boundary class coupling dependency efferent quality threshold type

coupling
efferent coupling
type dependencies
code quality

      d12 as Dep12: Dep12()

      allSet()

      d12 as Dep12: Dep12()
      d13 as Dep13: Dep13()

      allSet()

Explanation: Adding field d13 of type Dep13 pushes Ce from 12 to 13, exceeding the class threshold of 12. Group related dependencies into a record or split the class. See ek9 -h E11015 for details.


Question: What is the maximum inheritance depth allowed in EK9?

What triggers E11019 excessive inheritance?

Answer:
EK9 enforces Depth of Inheritance Tree (DIT) limits per construct type. When a hierarchy exceeds the limit, E11019 triggers.

THRESHOLDS
- Class: max DIT of 4
- Record: max DIT of 2
- Trait: max DIT of 4
- Function: max DIT of 3
- Component: max DIT of 4

HOW DIT IS COUNTED
DIT counts the number of extends relationships from the root to the leaf. The implicit Any base is not counted. So Level1 -> Level2 -> Level3 -> Level4 -> Level5 has DIT = 4.

THIS EXAMPLE
The class hierarchy below has exactly DIT = 4 (5 levels including root). This is the maximum allowed. Adding Level6 extending Level5 would trigger E11019.

WHY LIMIT DEPTH
- Research by Chidamber and Kemerer: classes with DIT > 5 have disproportionately more defects
- Deep hierarchies create fragile base class problems
- Each level adds coupling — changes ripple through all descendants
- Prefer composition over deep inheritance

HOW TO REDUCE DEPTH
1. Use composition/delegation instead of inheritance
2. Use traits for shared behaviour without hierarchy depth
3. Flatten by extracting common fields into records

See Q310 for code quality overview. See Q315 for inheritance limits. See Q729 for boundary testing.

defines module qa.codequality.inheritanceboundary

  defines class

    <?-
      5-level class hierarchy with DIT = 4 (exactly at class threshold).
      Level1 (root) -> Level2 -> Level3 -> Level4 -> Level5 (leaf)
      Each level must be 'as open' to allow extension, except the leaf.
    -?>
    Level1 as open
      value1 as Integer: 1

      getSum()
        <- rtn as Integer: value1

      default operator ?

    Level2 extends Level1 as open
      value2 as Integer: 2

      override getSum()
        <- rtn as Integer: value2

      default operator ?

    Level3 extends Level2 as open
      value3 as Integer: 3

      override getSum()
        <- rtn as Integer: value3

      default operator ?

    Level4 extends Level3 as open
      value4 as Integer: 4

      override getSum()
        <- rtn as Integer: value4

      default operator ?

    //DIT = 4, exactly at threshold — this compiles
    Level5 extends Level4
      value5 as Integer: 5

      override getSum()
        <- rtn as Integer: value5

      default operator ?

  defines program

    InheritanceDepthDitDemo()
      stdout <- Stdout()
      leaf <- Level5()
      stdout.println(`Sum: ${leaf.getSum()}`)

Java: no DIT limit (Spring hierarchies often 6-8 deep). C#: no DIT limit (NDepend advisory only). C++: no limit (MFC hierarchies notoriously deep). Kotlin: no DIT limit. Python: no limit. EK9: DIT exceeding threshold is a compiler error.

DIT E11019 boundary class composition depth extends hierarchy inheritance open quality

inheritance depth
DIT
class hierarchy
code quality

    Level5 extends Level4 as open
      value5 as Integer: 5

      override getSum()
        <- rtn as Integer: value5

      default operator ?

    Level6 extends Level5
      value6 as Integer: 6

      override getSum()
        <- rtn as Integer: value6

      default operator ?

  defines program

Explanation: Adding Level6 extending Level5 pushes the hierarchy depth from 4 to 5, exceeding the class threshold of 4. Use composition instead of deeper inheritance. See ek9 -h E11019 for details.


Question: How does EK9 detect confusingly similar variable names?

Why does EK9 flag variables with similar names?

What is the Levenshtein distance check for names?

Answer:
EK9 checks for variables in the same scope whose names differ by only 1-2 characters (Levenshtein distance). When detected, E11030 triggers.

WHAT IT DETECTS
Two variables like 'count' and 'coumt' differ by one character — a typo waiting to happen. Similarly 'index' and 'indx' or 'total' and 'totl'. When a developer (or AI) references the wrong one, the bug is subtle and hard to spot.

WHAT IS ALLOWED
- Names differing by 3+ characters are fine
- Prefixed/suffixed variants are fine: 'name' and 'firstName'
- Single-letter names in loop context: 'i', 'x', 'y'
- Different scopes: same-name variables in different functions

THIS EXAMPLE
The function below uses 'totalCount' and 'itemCount' — these differ by enough characters (Levenshtein distance > 2) to be clearly distinguishable. Using 'totalCount' and 'totalCoumt' would trigger E11030.

WHY THIS MATTERS
- AI assistants generate code at speed — similar names cause subtle bugs
- Code review misses single-character differences
- Autocomplete in editors may pick the wrong variable
- Debugging similar-name bugs wastes significant time

See Q290 for banned names. See Q292 for naming conventions. See Q310 for quality overview.

defines module qa.codequality.similarnames

  defines function

    <?-
      This function uses clearly distinct variable names.
      'totalCount' and 'itemCount' differ by enough characters
      to be clearly distinguishable (Levenshtein distance > 2).
      Using 'totalCount' and 'totalCoumt' would trigger E11030.
    -?>
    calculateTotals()
      ->
        items as List of Integer
      <-
        result as String: "empty"

      totalCount <- 0
      itemCount <- 0
      grandTotal <- 0

      for item in items
        itemCount++
        totalCount++
        grandTotal += item

      result: `Items: ${itemCount}, Count: ${totalCount}, Total: ${grandTotal}`

  defines program

    SimilarNamesBoundaryDemo()
      stdout <- Stdout()
      numbers <- [10, 20, 30, 40, 50]
      stdout.println(calculateTotals(numbers))

Java: no similar-name detection. C#: no similar-name detection. Python: no similar-name detection. Go: no similar-name detection. Rust: no similar-name detection. All languages rely on code review to catch confusingly similar names. EK9: similar names (Levenshtein ≤ 2) are a compiler error.

E11030 Levenshtein confusing distance names naming quality similar typo variable

naming
similar names
Levenshtein distance
code quality

      totalCount <- 0
      totalCont <- 0
      grandTotal <- 0

      for item in items
        totalCont++
        totalCount++
        grandTotal += item

      result: `Items: ${$totalCont}, Count: ${$totalCount}, Total: ${$grandTotal}`

Explanation: Renaming 'itemCount' to 'totalCont' makes it confusingly similar to 'totalCount' (Levenshtein distance 2). Both are used in the same scope, creating risk of referencing the wrong one. Use clearly distinct names. See ek9 -h E11030 for details.


Question: What is the cognitive complexity limit in EK9?

What triggers E11021 excessive cognitive complexity?

How does EK9 measure cognitive complexity?

What is the difference between cyclomatic and cognitive complexity?

Why does EK9 limit how hard code is to understand?

Answer:
EK9 measures cognitive complexity — how difficult code is to understand, not just how many paths exist. When it exceeds 35, E11021 triggers.

HOW IT DIFFERS FROM CYCLOMATIC COMPLEXITY
- Cyclomatic: counts decision points (each if/while/for = +1)
- Cognitive: adds penalties for NESTING depth (nested if = +depth, not +1)

NESTING MULTIPLIER
Each nesting level multiplies the cost:
- Level 0: if = +1
- Level 1: nested if = +2
- Level 2: doubly nested if = +3

HOW TO REDUCE COGNITIVE COMPLEXITY
1. Extract nested blocks into helper functions (resets nesting)
2. Use guard expressions to flatten conditional chains
3. Replace complex boolean expressions with named predicates

See Q312 for complexity metrics overview. See Q728 for nesting depth limits.

defines module qa.codequality.cognitiveboundary

  defines function

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

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

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

    <?-
      This function has cognitive complexity near the threshold of 35.
      Each nested level costs more than the last due to the nesting multiplier.
    -?>
    evaluateItems()
      ->
        items as List of Integer
        count as Integer
        enabled as Boolean
      <-
        message as String: "none"

      //Level 1: +1 each
      if checkA(count)
        //Level 2: +2 each
        if checkB(count)
          //Level 3: +3
          if enabled
            //Level 4: for = +4
            for item in items
              //Level 5: +5
              if checkC(item)
                message: "deep-a"
              else
                message: "deep-b"
          else
            message: "no-flag"
        else
          if enabled
            for item in items
              if checkA(item)
                message: "deep-c"
              else
                message: "deep-d"
          else
            message: "low"
      else
        //Level 2: +2 each
        if checkB(count)
          message: "alt-a"
        else
          message: "default"

  defines program

    CognitiveComplexityDemo()
      stdout <- Stdout()
      numbers <- [1, 2, 3]
      stdout.println(evaluateItems(numbers, 5, true))
      stdout.println(evaluateItems(numbers, 0, false))

Java: SonarQube measures cognitive complexity (advisory, default threshold 15). C#: NDepend measures cognitive complexity (informational). Python: no cognitive complexity in standard tools. EK9: cognitive complexity > 35 is a compiler error.

E11021 cognitive complexity metric nesting quality readability threshold understanding

cognitive complexity
readability
nesting cost
code quality

        if checkB(count)
          if enabled
            for item in items
              if checkC(item)
                message: "alt-a"
              else
                message: "alt-b"
          else
            message: "alt-low"
        else
          message: "default"

Explanation: Adding nested if at depth 6 adds +6 cognitive complexity, pushing the function past the threshold of 35. Each nested level costs +depth. Extract deep logic into helper functions. See ek9 -h E11021 for details.


Question: What is the module coupling limit in EK9?

What triggers E11016 excessive module coupling?

How many external modules can I reference?

What is the module-level coupling threshold?

Why does EK9 limit cross-module dependencies?

Answer:
EK9 limits the number of external modules a single module can depend on. When a module references more than 10 external modules, E11016 triggers.

WHAT IT MEASURES
Module coupling counts the distinct external module namespaces referenced via 'references' declarations. This is different from type-level coupling (Ce) — it measures architectural-level dependencies.

THRESHOLD
- Maximum: 10 external module references per module

WHY LIMIT MODULE COUPLING
- High module coupling creates brittle architectures
- Changes in one module ripple through many dependents
- Makes the module hard to understand in isolation
- Indicates the module is doing too many things

HOW TO REDUCE MODULE COUPLING
1. Split the module into focused sub-modules
2. Create a facade module that re-exports needed types
3. Use dependency injection to decouple
4. Apply the Interface Segregation Principle

THIS EXAMPLE
The module below demonstrates a self-contained module without external references. In a real project, you would add 'references' to declare dependencies on other modules.

See Q310 for code quality overview. See Q314 for cohesion and coupling details. See Q762 for type-level coupling.

defines module qa.codequality.modulecoupling

  defines function

    <?-
      Helper function demonstrating self-contained module design.
      In practice, high module coupling comes from too many 'references'
      declarations pointing to external modules.
    -?>
    calculateTotal()
      -> numbers as List of Integer
      <- total as Integer: 0
      for num in numbers
        total += num

  defines program

    ModuleCouplingDemo()
      stdout <- Stdout()
      numbers <- [10, 20, 30, 40, 50]
      stdout.println(`Total: ${calculateTotal(numbers)}`)

Java: Maven/Gradle track module dependencies but don't enforce count limits. C#: NuGet package count is unchecked. Python: import count is unchecked. Go: module dependency count is unchecked. Rust: crate dependency count is unchecked. EK9: > 10 external module references is a compiler error.

E11016 architecture coupling dependency external module quality references threshold

module coupling
architecture
dependencies
code quality

Explanation: The function calcTotal is not defined in this module. The correct name is calculateTotal. See ek9 -h E50001 for details.


Question: What is the dev context and how does it relate to testing in EK9?

Why can I only use assert in @Test programs?

What is the difference between dev and production builds?

How does EK9 separate test code from production code?

What triggers E81012 production assertion?

Why does EK9 restrict assert to dev context?

Answer:
EK9 enforces a strict compile-time boundary between test code and production code using the 'dev context' concept.

THE DEV CONTEXT
When you compile with -cd, -Cd, or run tests with -t, the compiler includes dev code (programs marked with @Test and all code they reach). When you compile with -c or -C (production builds), @Test programs and their assertions are stripped entirely.

ASSERT VS REQUIRE
- assert: Test-only. Validates expected outcomes. Only allowed in code reachable from @Test programs. Triggers E81012 if found in production code paths.
- require: Production-safe. Enforces preconditions. Works everywhere. Use this in production functions and methods.

THE CALL GRAPH ENFORCES THIS
EK9 performs call graph analysis from all program entry points. If a non-@Test program can reach code containing assert, the compiler raises E81012 (PRODUCTION_ASSERTION). If assert exists in code NOT reachable from any program (test or production), it raises E81011 (ORPHAN_ASSERTION).

WHY THIS MATTERS
1. assert has overhead (expression capture, stack traces) — not wanted in production
2. Mixing test and production validation indicates confused design intent
3. Production code should use require for preconditions, not test assertions
4. Stripping test code from production builds reduces binary size and attack surface

THIS EXAMPLE
The main file has the production program and functions using require. The companion dev file (QA0767_dev_tests.ek9) has the @Test programs using assert. The dev file is only compiled in dev builds (-cd, -t).

See Q155 for writing tests. See Q156 for assertions. See Q157 for running tests. See Q305 for require vs assert vs throw.

defines module qa.codequality.devcontext

  defines function

    <?-
      This function uses 'require' for preconditions.
      It is called ONLY from the PRODUCTION program below.
      If assert were used instead of require, E81012 would trigger
      because the call graph shows it is reachable from non-@Test code.
    -?>
    validateInput()
      -> input as Integer
      <- valid as Boolean: false

      require input?
      require input > 0

      valid: true

    <?-
      Helper for tests only — called exclusively from @Test programs
      in the companion dev file.
    -?>
    doubleValue()
      -> input as Integer
      <- result as Integer: input * 2

  defines program

    <?-
      PRODUCTION PROGRAM — no @Test directive.
      Calls validateInput which uses require (correct).
      If validateInput used assert, E81012 would fire.
    -?>
    ProductionEntry()
      stdout <- Stdout()

      if validateInput(42)
        stdout.println("Input is valid")

Java: assert works everywhere but disabled by default (-ea to enable), JUnit assertions are library methods callable from anywhere — no compile-time enforcement. Python: assert works everywhere, stripped with -O flag — no compile-time boundary. Rust: assert! works everywhere, debug_assert! stripped in release builds — but no call graph analysis to enforce boundaries. Go: no assert keyword, testing.T methods technically callable from non-test code. EK9: compile-time call graph analysis enforces that assert only appears in code reachable from @Test programs.

@Test E81011 E81012 assert boundary call-graph compile context dev production require test

dev context
test vs production
assert vs require
call graph

      require input?
      require input > 0

      assert input?
      assert input > 0

Explanation: Using assert in a non-dev source file triggers E81015 TEST_CONSTRUCT_IN_NON_DEV_SOURCE. The assert keyword is only allowed in files within a dev/ directory or matching the QA####_dev_ naming convention. Use require for production preconditions. See ek9 -h E81015 for details.

#!ek9
defines module qa.codequality.devcontext

  defines program

    //TEST PROGRAM — marked with @Test.
    //This program only exists in dev builds (-cd, -t).
    //It CAN use assert because it is a test program.
    @Test
    TestDoubleValue()
      stdout <- Stdout()

      result <- doubleValue(5)
      assert result == 10

      result2 <- doubleValue(7)
      assert result2 == 14

      stdout.println("doubleValue tests passed")

//EOF


Question: Why can't an abstract method have a body in EK9?

What triggers E07100 abstract but body provided?

How do I define abstract methods in EK9?

What is the difference between abstract and concrete methods in EK9?

Answer:
Abstract methods declare a signature that subclasses must implement. Providing a body contradicts the meaning of abstract — if you provide an implementation, it is not abstract.

ABSTRACT METHOD RULES
1. Mark the method 'as abstract' — no implementation body
2. The containing class must be 'abstract' or 'as open'
3. Subclasses must override and provide the implementation

THIS EXAMPLE
The Shape class below is abstract with one abstract method (area) and one concrete method (describe). The Circle subclass overrides area() with an actual implementation. This is the correct pattern.

COMMON MISTAKE
AI and developers migrating from other languages sometimes write 'as abstract' on a method and then add a body. EK9 treats this as error E07100 because abstract and implementation are mutually exclusive.

HOW TO FIX
1. Remove 'as abstract' if you want the method to have an implementation
2. Remove the body if you want the method to be abstract

See Q93 for defining classes. See Q84 for making classes extensible. See Q86 for abstract classes. See Q274 for common AI mistakes.

defines module qa.classesandoop.abstractmethods

  defines function

    Transformer() as abstract
      -> input as String
      <- output as String?

    UpperTransformer() extends Transformer
      -> input as String
      <- output as String: input.upperCase()

  defines class

    Shape as abstract
      name <- String()

      Shape()
        -> n as String
        name :=: n

      area() as abstract
        <- rtn as Float?

      validate() as abstract
        -> input as String

      describe()
        <- rtn as String: name

      default operator ?

    Circle extends Shape
      radius <- Float()

      Circle()
        -> r as Float
        super("circle")
        radius :=: r

      override area()
        <- rtn as Float: radius * radius * 3.14159

      override validate()
        -> input as String
        require input?

      default operator ?

  defines program

    ShowShapes()
      stdout <- Stdout()
      circle <- Circle(5.0)
      if circle?
        stdout.println("Shape: " + circle.describe())
        circle.validate("test")
        result <- UpperTransformer("hello")
        stdout.println(result)

Java: abstract methods have no body, concrete methods have body — same rule but Java uses braces. Python: raise NotImplementedError() convention for abstract, or abc.abstractmethod decorator. Rust: trait methods without default body are abstract. Kotlin: abstract fun has no body, regular fun has body. C#: abstract methods have no body, virtual methods have body. EK9: same semantics as Java/Kotlin, 'as abstract' modifier with no body allowed.

E07100 abstract body class concrete implement method override signature subclass

      area() as abstract
        <- rtn as Float?

      area() as abstract
        <- rtn as Float: 0.0

Explanation: Adding an initialisation value '0.0' to the return variable creates a method body. Abstract methods must have no implementation — just a signature with unset return type. Remove the initialisation or remove 'as abstract'. See ek9 -h E07100 for details.

      validate() as abstract
        -> input as String
        require input?

Explanation: Adding a require statement creates a body. Even a single precondition check is an implementation. Abstract methods must be pure signatures — the subclass provides the body including any preconditions. See ek9 -h E07100 for details.

    Transformer() as abstract
      -> input as String
      <- output as String: input

Explanation: Initialising the return variable in an abstract function creates a body. Abstract functions must declare an unset return type — the implementing function provides the logic. See ek9 -h E07100 for details.

      override area() as abstract
        <- rtn as Float?

Explanation: Combining 'override' with 'as abstract' is contradictory. Override means 'I am implementing this', abstract means 'I am NOT implementing this'. Choose one: override with a body, or abstract without. See ek9 -h E07020 for details.


Question: Why does EK9 require an explicit constructor when properties are uninitialized?

What triggers E07170 explicit constructor required?

How do I fix uninitialised property errors in EK9?

Why can't I declare a property without a value in EK9?

Answer:
EK9 requires that every property has a known value before use. When you declare a property as unset (using 'as Type?' instead of '<- value'), the compiler cannot auto-generate a constructor because it does not know what values to assign.

TWO PROPERTY STYLES
1. Initialised: 'name <- "default"' — value known, default constructor works
2. Uninitialised: 'name as String?' — value unknown, explicit constructor REQUIRED

THIS EXAMPLE
The User class initialises both properties with '<-'. This allows 'default operator ?' and no explicit constructor is needed. The mutation removes the initialisation, creating an uninitialised property that triggers E07170.

WHY THIS RULE EXISTS
Java allows null fields with no warning. This causes NullPointerException at runtime when fields are used before being set. EK9 moves this check to compile time — if a property has no value, you must provide a constructor that gives it one.

HOW TO FIX
1. Initialise the property at declaration: 'name <- "unknown"'
2. Or provide an explicit constructor that sets it

See Q93 for defining classes. See Q89 for constructors. See Q99 for why explicit constructors matter.

defines module qa.classesandoop.explicitconstructor

  defines class

    User
      name <- String()
      active <- true

      describe()
        <- rtn as String: `${name} active=${active}`

      default operator ?

    Item
      title <- String()
      count <- 0

      getTitle()
        <- rtn as String: title

      default operator ?

  defines program

    ShowUser()
      stdout <- Stdout()
      user <- User()
      if user?
        stdout.println(user.describe())
      item <- Item()
      if item?
        stdout.println(item.getTitle())

Java: fields default to null/0, no compile-time enforcement. Python: __init__ sets fields, no enforcement for completeness. Rust: all struct fields must be initialised at creation, no partial init. Kotlin: lateinit defers but crashes at runtime if used before init. Go: zero values for all fields. EK9: uninitialised properties require explicit constructor — compile-time enforcement.

E07170 class constructor explicit field initialise null property required uninitialised

constructors
E07170
property initialization

Explanation: Changing 'name <- String()' (initialised) to 'name as String?' (uninitialised) means the compiler cannot generate a default constructor. Either initialise the property or add an explicit constructor that sets it. See ek9 -h E07170 for details.

Explanation: Changing initialised property to uninitialised requires an explicit constructor. Without one, the compiler cannot ensure the property has a value before use. See ek9 -h E07170 for details.


Question: Why can't I use 'by' delegation inside a trait?

What triggers E07040 trait by identifier not supported?

How does trait delegation with by work in EK9?

Where can I use the by keyword for delegation?

Answer:
The 'by' keyword delegates trait method implementations to a field. This only works in CLASSES, not in traits. Traits define interfaces and optional default implementations — they cannot hold fields or delegate to other objects.

DELEGATION SYNTAX (CLASS ONLY)
  MyClass with trait of SomeTrait by myField
This tells the compiler: when SomeTrait methods are called on MyClass, forward them to myField.

WHY NOT IN TRAITS
Traits are stateless contracts. Delegation requires a concrete field to forward to — traits have no fields. Attempting 'by' in a trait is a design error caught at compile time.

THIS EXAMPLE
The Renderable and Loggable traits define contracts. The Formatter class delegates Renderable to its helper field using 'by'. This is the correct pattern — delegation in a class.

COMMON MISTAKE
Developers try to compose traits using 'by'. In EK9, traits compose through 'with trait of' (listing multiple traits) or 'extends' (trait hierarchy), never through delegation.

See Q78 for traits. See Q85 for composition. See Q93 for classes.

defines module qa.classesandoop.traitdelegation

  defines trait

    Renderable
      render()
        <- rtn as String?

    Loggable
      log()
        <- rtn as String?

    Combined with trait of Renderable, Loggable

  defines class

    RenderHelper with trait of Renderable
      label <- String()

      RenderHelper()
        -> l as String
        label :=: l

      override render()
        <- rtn as String: label

      default operator ?

    Formatter with trait of Renderable by helper
      helper as Renderable?

      default private Formatter()

      Formatter()
        -> h as Renderable
        require h?
        helper: h

      default operator ?

  defines program

    ShowDelegation()
      stdout <- Stdout()
      helper <- RenderHelper("formatted output")
      doc <- Formatter(helper)
      if doc?
        stdout.println(doc.render())

Java: no delegation keyword, manual forwarding methods required. Kotlin: 'by' keyword works on interfaces in class declarations only. Rust: no delegation, use Deref or manual forwarding. Go: struct embedding provides implicit delegation. C#: no delegation keyword. EK9: 'by' delegation in class declarations only, not in traits.

E07040 by class composition delegate delegation field forward interface trait

Combined with trait of Renderable by renderer, Loggable

Explanation: Adding 'by renderer' to a trait definition attempts delegation inside a trait. Traits have no fields to delegate to — delegation only works in classes. Remove 'by renderer' or move the delegation to a class. See ek9 -h E07040 for details.


Question: Why must a dispatcher method have a body in EK9?

What triggers E07120 dispatcher but no body provided?

How do I write a dispatcher method in EK9?

Answer:
A dispatcher method routes calls to type-specific private overloads at runtime. The base dispatcher method MUST have a body — it serves as the default fallback when no specific overload matches the argument type.

DISPATCHER PATTERN
1. Declare a public method 'as dispatcher' taking a base type
2. Provide a body — this is the default handler
3. Add private overloads for specific types
4. Runtime routes to the most specific match

THIS EXAMPLE
The TypeProcessor class has a dispatcher method handle() that takes Any. The body returns a default description. Private overloads handle Integer and String specifically. The runtime automatically routes to the correct overload.

WHY A BODY IS REQUIRED
Unlike abstract methods (which have no body), dispatchers need a default implementation for the fallback case — when the argument type doesn't match any specific overload. Without this, the dispatcher has no behaviour for unmatched types.

COMMON MISTAKE
Developers assume dispatcher works like abstract — defining just the signature. But abstract means 'subclass provides implementation', while dispatcher means 'I route to overloads and handle the default case myself'.

See Q47 for dispatcher pattern. See Q51 for overloading vs dispatch. See Q768 for abstract methods (the opposite rule).

defines module qa.classesandoop.dispatcherbody

  defines class

    TypeProcessor
      handle() as dispatcher
        -> item as Any
        <- rtn as String: "unhandled type"

      private handle()
        -> item as Integer
        <- rtn as String: `integer ${item}`

      private handle()
        -> item as String
        <- rtn as String: `string ${item}`

      default operator ?

    Describer
      describe() as dispatcher
        -> target as Any
        <- rtn as String: "unknown"

      private describe()
        -> target as Integer
        <- rtn as String: `number ${target}`

      default operator ?

  defines program

    ShowDispatcher()
      stdout <- Stdout()
      processor <- TypeProcessor()

      items <- [1, "hello", 3.14]
      for item in items
        result <- processor.handle(item)
        stdout.println(result)

      describer <- Describer()
      entries <- [99, true]
      for entry in entries
        output <- describer.describe(entry)
        stdout.println(output)

Java: no built-in dispatch, manual instanceof chains or visitor pattern. Python: functools.singledispatch for function-level dispatch. Kotlin: no built-in multi-dispatch, use sealed class + when. Rust: no multi-dispatch, use enum + match. Go: no dispatch, use type switch. EK9: 'as dispatcher' keyword with automatic runtime type routing.

E07120 body default dispatch dispatcher method overload routing runtime type

dispatcher pattern
E07120
multiple dispatch

      handle() as dispatcher
        -> item as Any
        <- rtn as String?

Explanation: Removing the initialisation from the return variable leaves the dispatcher with no body. Dispatcher methods must have a default implementation for the fallback case. Provide a body that handles unmatched types. See ek9 -h E07120 for details.

      describe() as dispatcher
        -> target as Any
        <- rtn as String?

Explanation: The dispatcher describe() has no body — the return variable is declared but not initialised. Dispatchers are not abstract; they need a working default implementation. See ek9 -h E07120 for details.


Question: Why does EK9 reject circular type hierarchies?

What triggers E05020 circular hierarchy detected?

Why can't class A extend class B if B extends A?

How do I fix a circular inheritance chain?

Answer:
EK9 detects when types form a cycle in their inheritance chain — A extends B, B extends C, C extends A. This is logically impossible: you cannot inherit from something that inherits from you.

WHY CYCLES ARE IMPOSSIBLE
Inheritance means 'I am a specialisation of my parent'. If A extends B and B extends A, then A is a specialisation of B which is a specialisation of A — an infinite recursion with no base. Which constructor runs first? Which fields exist first? Undefined.

THIS EXAMPLE
The Vehicle, Car, ElectricCar hierarchy is a valid linear chain. Vehicle is the root, Car extends it, ElectricCar extends Car. No cycles.

COMMON CAUSES
1. Refactoring that accidentally swapped inheritance direction
2. Copy-paste errors when creating similar classes
3. Two classes that need each other's features (use composition instead)

HOW TO FIX
1. Draw the inheritance relationships to visualise the cycle
2. Identify the true root/base class
3. Break the cycle by removing one 'extends'
4. Consider composition over inheritance

See Q77 for inheritance. See Q85 for composition. See Q83 for closed types.

defines module qa.classesandoop.circularhierarchy

  defines class

    Vehicle as open
      make <- String()

      Vehicle()
        -> m as String
        make :=: m

      describe()
        <- rtn as String: make

      default operator ?

    Car extends Vehicle as open
      doors <- Integer()

      Car()
        ->
          m as String
          d as Integer
        super(m)
        doors :=: d

      override describe()
        <- rtn as String: `${super.describe()} ${doors}-door`

      default operator ?

    ElectricCar extends Car as open
      range <- Integer()

      ElectricCar()
        ->
          m as String
          d as Integer
          r as Integer
        super(m, d)
        range :=: r

      override describe()
        <- rtn as String: `${super.describe()} EV range=${range}`

      default operator ?

  defines program

    ShowHierarchy()
      stdout <- Stdout()
      ev <- ElectricCar("Tesla", 4, 350)
      if ev?
        stdout.println(ev.describe())

Java: detects circular inheritance at compile time with 'cyclic inheritance involving X'. Python: detects at runtime with TypeError during C3 linearization. Rust: no inheritance, so no circular hierarchies possible. Go: no inheritance, composition only. Kotlin: same as Java, compile-time detection. EK9: compile-time detection at TYPE_HIERARCHY_CHECKS phase.

E05020 chain circular class composition cycle extends hierarchy inheritance refactoring

Explanation: Changing Car to extend ElectricCar creates a cycle: Car extends ElectricCar, ElectricCar extends Car. Draw the hierarchy and ensure it forms a tree with no loops. See ek9 -h E05020 for details.


Question: Why must a method either have a body or be marked abstract?

What triggers E07110 not abstract and no body provided?

How do I fix a method with no implementation in EK9?

Why does EK9 require explicit abstract keyword?

Answer:
EK9 requires explicit intent for every method. A method with no body and no 'as abstract' keyword is ambiguous — did you forget to implement it, or did you intend it to be abstract? EK9 does not guess.

THE RULE
- Method WITH body: concrete implementation (normal method)
- Method WITHOUT body + 'as abstract': abstract, subclass must implement
- Method WITHOUT body + NO abstract: ERROR E07110

THIS EXAMPLE
The Validator class has a concrete validate() method with a body. The Formatter function has a concrete body. Both are unambiguous.

COMMON CAUSES
1. Forgot to add implementation body
2. Forgot the 'as abstract' keyword
3. Incomplete method definition during development

THIS PAIRS WITH E07100
E07100 is the opposite: abstract WITH a body. Together they enforce that abstract and body are mutually exclusive — you must choose one.

See Q768 for abstract methods (opposite rule). See Q86 for abstract classes. See Q93 for defining classes.

defines module qa.classesandoop.methodbody

  defines function

    Formatter() as pure
      -> input as String
      <- output as String: input.trim()

  defines class

    Validator
      minLength <- Integer()

      Validator()
        -> min as Integer
        minLength :=: min

      validate()
        -> input as String
        <- rtn as Boolean: length input > 0

      default operator ?

  defines program

    ShowValidation()
      stdout <- Stdout()
      checker <- Validator(3)
      if checker?
        stdout.println($checker.validate("hello"))
        stdout.println(Formatter("  padded  "))

Java: interface methods are implicitly abstract, class methods without body are compile errors. Python: no enforcement, pass or raise NotImplementedError by convention. Rust: trait methods without body are implicitly abstract. Kotlin: abstract keyword required on both class and method. Go: interface methods have no body, struct methods must have body. EK9: explicit 'as abstract' required — no implicit abstractions.

E07110 abstract body concrete explicit function implement intent method missing

      validate()
        -> input as String
        <- rtn as Boolean?

Explanation: Removing the initialisation from the return variable leaves the method with no body. The method is not marked 'as abstract', so EK9 cannot determine intent. Either add an implementation or mark the method 'as abstract' (and make the class abstract). See ek9 -h E07110 for details.

    Formatter() as pure
      -> input as String
      <- output as String?

Explanation: The function has no body — the return variable is declared but not initialised. Functions without a body must be marked 'as abstract'. Either provide an implementation or add 'as abstract'. See ek9 -h E07110 for details.


Question: Why can't a constructor be abstract in EK9?

What triggers E07050 abstract constructor?

Can I defer construction to a subclass in EK9?

Why does EK9 reject abstract on constructors?

Answer:
Constructors create instances — that is their sole purpose. Marking a constructor 'abstract' says 'I have no implementation, a subclass must provide it'. But constructors MUST create an instance of their own class, so deferring that to a subclass is logically impossible.

WHY ABSTRACT CONSTRUCTORS ARE IMPOSSIBLE
When you call 'MyClass()', the constructor MUST create a MyClass instance. An abstract constructor would mean 'I do not create anything' — defeating the purpose. If you want polymorphic creation, use an abstract factory method instead.

THIS EXAMPLE
The Shape class is abstract with a concrete constructor that sets the name. The Circle subclass calls super() to initialise the base. Both constructors are concrete — they create their respective instances.

RELATED CONSTRUCTOR RULES
- E07050: abstract constructor (this error) — constructors must create instances
- E07060: override constructor — constructors are not inherited, so override is meaningless
- E07080: default constructor with parameters — default means no parameters

See Q89 for constructors. See Q768 for abstract methods. See Q86 for abstract classes.

defines module qa.classesandoop.constructorabstract

  defines class

    Shape as abstract
      name <- String()

      Shape()
        -> n as String
        name :=: n

      area() as abstract
        <- rtn as Float?

      describe()
        <- rtn as String: name

      default operator ?

    Circle extends Shape
      radius <- Float()

      Circle()
        -> r as Float
        super("circle")
        radius :=: r

      override area()
        <- rtn as Float: radius * radius * 3.14159

      default operator ?

  defines program

    ShowShapes()
      stdout <- Stdout()
      circle <- Circle(5.0)
      if circle?
        stdout.println(circle.describe())

Java: abstract constructors are a compile error. Python: __init__ can be overridden but not marked abstract via abc. Rust: no constructors, new() is a convention. Kotlin: constructors cannot be abstract. Go: no constructors, factory functions used. EK9: constructors must be concrete, use abstract factory methods for polymorphic creation.

E07050 abstract class constructor create default factory impossible instance override

      Shape()
        -> n as String
        name :=: n

      Shape() as abstract
        -> n as String

Explanation: Making the constructor abstract (no body) is logically impossible — constructors must create instances. A constructor cannot defer creation to a subclass. Remove 'as abstract' and provide a body. See ek9 -h E07050 for details.

      Circle() as abstract
        -> r as Float

Explanation: A constructor cannot be abstract. It must create an instance of its class. Remove 'as abstract' and provide the constructor body. See ek9 -h E07050 for details.


Question: Why can't a constructor use the override keyword in EK9?

What triggers E07060 override constructor?

Why does override on a constructor fail?

Answer:
Constructors are NOT inherited in EK9. Each class defines its own constructors independently. The 'override' keyword means 'I am replacing a method inherited from my parent' — since constructors are never inherited, override is meaningless on them.

WHY CONSTRUCTORS ARE NOT INHERITED
A constructor creates an instance of a specific class. ChildClass() creates a ChildClass, not a ParentClass. There is nothing to override because the parent's constructor belongs to the parent.

CALLING PARENT CONSTRUCTORS
Use super() to call the parent constructor from within your constructor. This is delegation, not overriding:
  ChildClass()
    -> name as String
    super(name)

THIS EXAMPLE
The Animal class has a constructor taking a name. Dog extends Animal and has its own constructor that calls super() to initialise the parent. No override needed.

See Q774 for abstract constructor rules. See Q89 for constructor basics. See Q77 for inheritance.

defines module qa.classesandoop.constructoroverride

  defines class

    Animal as open
      name <- String()

      Animal()
        -> n as String
        name :=: n

      speak()
        <- rtn as String: name

      default operator ?

    Dog extends Animal

      Dog()
        -> n as String
        super(n)

      override speak()
        <- rtn as String: `${super.speak()} says woof`

      default operator ?

  defines program

    ShowAnimals()
      stdout <- Stdout()
      dog <- Dog("Rex")
      if dog?
        stdout.println(dog.speak())

Java: constructors are not inherited, override annotation on constructor is compile error. Python: __init__ can be overridden (it is a regular method). Rust: no constructors to override, new() is a convention. Kotlin: constructors not inherited, override not applicable. Go: no constructors. EK9: constructors not inherited, override keyword rejected with E07060.

E07060 child class constructor extend inherited override parent super

      Dog()
        -> n as String
        super(n)

      override Dog()
        -> n as String
        super(n)

Explanation: Adding 'override' to a constructor is invalid — constructors are not inherited, so there is nothing to override. Each class defines its own constructors. Use super() to call the parent constructor. See ek9 -h E07060 for details.


Question: Why can't a default constructor have parameters in EK9?

What triggers E07080 invalid default constructor?

What does the default keyword mean on a constructor?

How do I auto-generate a constructor in EK9?

Answer:
The 'default' keyword on a constructor means 'compiler, generate a zero-argument constructor that initialises all fields to their declared values'. Adding parameters contradicts this — the compiler cannot know how to map parameters to fields.

DEFAULT CONSTRUCTOR RULES
- 'default ClassName()' — no parameters, compiler generates body
- All fields must be initialised at declaration for default to work
- You can have BOTH a default constructor AND parameterised constructors

THIS EXAMPLE
The Config class has two fields initialised at declaration and uses 'default Config()' for zero-argument construction. It also has a parameterised constructor for custom values. Both coexist.

WHEN TO USE DEFAULT
Use default when all fields have sensible initial values and you want a zero-argument constructor without writing the body. Use a regular constructor when you need parameters.

See Q89 for constructor basics. See Q82 for default operators. See Q774 for abstract constructor rules.

defines module qa.classesandoop.defaultconstructor

  defines class

    Config
      name <- "default"
      timeout <- 30

      default Config()

      Config()
        ->
          n as String
          t as Integer
        name :=: n
        timeout :=: t

      describe()
        <- rtn as String: `${name} timeout=${timeout}`

      default operator ?

    Setting
      label <- "none"
      enabled <- false

      default private Setting()

      Setting()
        -> l as String
        label :=: l
        enabled: true

      default operator ?

  defines program

    ShowConfig()
      stdout <- Stdout()
      defaults <- Config()
      custom <- Config("prod", 60)
      if defaults?
        stdout.println(defaults.describe())
      if custom?
        stdout.println(custom.describe())

Java: default constructor is implicit (no-arg, compiler-generated if no constructors defined). Python: __init__ with defaults provides similar pattern. Rust: Default trait derive provides zero-arg construction. Kotlin: default parameter values on primary constructor. Go: zero-value initialization is automatic. EK9: explicit 'default ClassName()' keyword, must be zero-arg, fields must be initialised.

E07080 class constructor default fields generate initialise parameters zero-arg

default constructor
E07080
auto-generation

      default Config()
        -> name as String

Explanation: Adding a parameter to a default constructor is invalid. The 'default' keyword means zero-argument auto-generated constructor. Either remove 'default' and write a parameterised constructor, or remove the parameter. See ek9 -h E07080 for details.

      default private Setting()
        -> initial as Integer

Explanation: Even with the private modifier, a default constructor cannot have parameters. Default means zero-arg. Remove 'default' to create a parameterised private constructor. See ek9 -h E07080 for details.


Question: Why does EK9 reject discarded operator return values?

What triggers E11050 discarded operator return?

Why must I capture the result of an operator?

What is the difference between += and + in EK9?

Answer:
EK9 distinguishes between mutating operators (which change state) and computational operators (which return new values). If you call a computational operator and discard the result, the computation was pointless — dead code.

MUTATING OPERATORS (return Void — side-effect IS the purpose)
  price += tax      // Modifies price in place
  count++           // Increments count
  target :=: source // Copies source into target

COMPUTATIONAL OPERATORS (return values — the VALUE is the purpose)
  total <- price + tax    // Must capture the result
  isEqual <- a == b       // Must use the Boolean
  len <- length name      // Must use the Integer

THIS EXAMPLE
The calculate() function captures the result of every operator. The mutation removes the capture, leaving a bare operator call that discards its result.

See Q238 for operator overview. See Q239 for comparison operators. See Q310 for code quality checks.

defines module qa.codequality.discardedreturn

  defines class

    Calculator

      add() as pure
        ->
          left as Float
          right as Float
        <- rtn as Float: left + right

      multiply() as pure
        ->
          left as Float
          right as Float
        <- rtn as Float: left * right

      default operator ?

  defines program

    ShowCalculation()
      stdout <- Stdout()
      calculator <- Calculator()
      total <- calculator.add(100.0, 15.0)
      stdout.println($total)

Java: silently discards return values of any expression statement. Python: silently discards return values. Rust: warns about unused results with #[must_use]. Go: compile error for unused function returns but not operators. Kotlin: no enforcement. EK9: compile error for discarded computational operator returns — dead code is not allowed.

E11050 capture code computational dead discarded mutating operator result return

      calculator.add(100.0, 15.0)
      stdout.println("done")

Explanation: Calling a pure method that returns a value and discarding the result is dead code. The method has no side effects, so without capturing the return value the call achieves nothing. See ek9 -h E11051 for details.


Question: Why can't I change the access modifier when overriding a method?

What triggers E05130 method access modifiers differ?

Can I make an overridden method private in EK9?

Why must override methods keep the same visibility?

Answer:
When you override a method, the child version must have the same access level as the parent. A protected method in the parent must remain protected in the child. Changing it would break the contract.

ACCESS RULES FOR OVERRIDE
- Public parent method: child must be public
- Protected parent method: child must be protected
- Private methods: cannot be overridden (not inherited)

WHY THIS RULE EXISTS
If Parent.process() is protected and Child makes it private, code in sibling classes that calls process() would fail. EK9 catches this at compile time.

THIS EXAMPLE
The Formatter class has a protected helper() method. The PrefixFormatter overrides it with the correct protected access.

See Q77 for inheritance. See Q84 for open classes. See Q90 for field visibility.

defines module qa.classesandoop.overrideaccess

  defines class

    Formatter as open
      protected helper()
        -> text as String
        <- rtn as String: text

      format()
        -> message as String
        <- rtn as String: helper(message)

      default operator ?

    PrefixFormatter extends Formatter

      override protected helper()
        -> text as String
        <- rtn as String: `[PREFIX] ${text}`

      default operator ?

  defines program

    ShowFormatter()
      formatter <- PrefixFormatter()
      if formatter?
        stdout <- Stdout()
        stdout.println(formatter.format("hello"))

Java: narrowing access on override is a compile error. Python: no access modifiers enforced. Rust: no method overriding. Kotlin: same as Java, cannot narrow access. Go: no method overriding. C#: override must match parent accessibility. EK9: same rule as Java/Kotlin/C#, compile-time enforcement.

E05130 access contract method modifier override private protected visibility

Explanation: Changing the access from 'protected override' to 'private' narrows the visibility. The parent's helper() is protected, so the child must keep it protected. A private method with the same signature is treated as a different method, not an override — but EK9 detects this access mismatch. See ek9 -h E05130 for details.


Question: Why must certain operators return a value in EK9?

What triggers E07400 returning missing on an operator?

How do I define an operator with a return type?

Why does my comparison operator need a return declaration?

Answer:
In EK9, computational operators (like <, ==, +, -, <>) must return a value. These operators are expressions — they compute something. Without a return declaration, the operator has no result and cannot be used in expressions.

OPERATORS THAT MUST RETURN
- Comparison: < must return Boolean
- Equality: == must return Boolean
- Arithmetic: +, - must return the same type
- Spaceship: <=> must return Integer
- Hash: #? must return Integer
- String: $ must return String

MUTATING OPERATORS (no return needed)
- +=, -=, :=:, :~:, :^: modify in place, return Void
- ++, -- increment/decrement in place
- close releases resources

THIS EXAMPLE
The Temperature class defines < and == operators with correct return declarations. The mutation removes the return from the < operator.

See Q81 for operator overview. See Q239 for comparison operators. See Q82 for default operators.

defines module qa.classesandoop.operatorreturn

  defines class

    Temperature
      degrees <- Float()

      Temperature()
        -> d as Float
        degrees :=: d

      operator < as pure
        -> other as Temperature
        <- rtn as Boolean: degrees < other.degrees

      operator == as pure
        -> other as Temperature
        <- rtn as Boolean: degrees == other.degrees

      operator #? as pure
        <- rtn as Integer: #? degrees

      operator $ as pure
        <- rtn as String: $degrees

      default operator ?

  defines program

    CompareTemps()
      stdout <- Stdout()
      hot <- Temperature(35.0)
      cold <- Temperature(5.0)
      if cold < hot
        stdout.println("cold is less than hot")

Java: operator overloading not supported (except + for String). Python: __lt__ must return a value, no enforcement on type. Rust: Ord trait methods have explicit return types. Kotlin: operator fun compareTo returns Int. Go: no operator overloading. EK9: operators have strict return type requirements enforced at compile time.

E07400 boolean comparison declaration missing operator return type value

Explanation: The < operator must return a Boolean but the return declaration is missing. Comparison operators have a specific return type requirement enforced by the compiler. See ek9 -h E07520 for details.

Explanation: The #? (hashcode) operator must return an Integer but the return declaration is missing. See ek9 -h E07550 for details.

Explanation: The $ (string) operator must return a String but the return declaration is missing. See ek9 -h E07570 for details.

      operator < as pure
        ->
          other as Temperature
          extra as Integer
        <- rtn as Boolean: degrees < other.degrees

Explanation: The < operator takes exactly one parameter but two were provided. Comparison operators must accept one argument of the type being compared. See ek9 -h E06280 for details.

      operator == as pure
        <- rtn as Boolean: true

Explanation: The == operator requires one parameter but none was provided. Comparison operators must accept one argument of the type being compared. See ek9 -h E06290 for details.


Question: Why can't I use ++ or -- in an expression in EK9?

What triggers E07950 increment decrement expression not allowed?

Answer:
In EK9, ++ and -- are STATEMENT-ONLY operators. They modify a variable in place and return nothing. You cannot use them where a value is expected (assignments, conditions, function arguments).

WHY STATEMENT-ONLY
In C/Java, 'y <- x++' has confusing semantics: y gets the OLD value before increment (C) or becomes an alias to x (Java). Both are bug sources. EK9 eliminates this by making ++ and -- void operations.

CORRECT USAGE
  count++         // OK — standalone statement
  count--         // OK — standalone statement

INCORRECT USAGE
  y <- count++    // ERROR — ++ returns void, cannot assign
  if count++      // ERROR — ++ returns void, not Boolean

IF YOU NEED THE OLD VALUE
  oldCount <- count
  count++
  // Now oldCount has the previous value

See Q238 for operator overview. See Q239 for comparison operators.

defines module qa.operators.incrementstatement

  defines program

    CountItems()
      stdout <- Stdout()
      count <- 0

      count++
      count++
      count++
      total <- count

      stdout.println($total)

Java: x++ returns old value (post-increment), ++x returns new value (pre-increment). C/C++: same as Java plus undefined behavior in complex expressions. Python: no ++ or -- operators at all. Rust: no ++ or -- operators. Go: ++ and -- are statements only (same as EK9). Kotlin: ++ and -- work as expressions. EK9: ++ and -- are statements only, like Go.

E07950 assign bug decrement expression increment operator statement void

increment decrement
E07950
statement-only operators

Explanation: Using ++ in an assignment expression is not allowed. The ++ operator is statement-only — it modifies the variable and returns void. Increment first, then assign separately. See ek9 -h E07950 for details.


Question: Why does EK9 reject calls with the wrong number of arguments?

What triggers E06280 too many arguments?

How do I fix argument count mismatch in EK9?

Answer:
EK9 enforces exact argument counts at compile time. A function or method defined with two parameters must be called with exactly two arguments — no more, no fewer.

NO DEFAULT PARAMETERS
Unlike Python or Kotlin, EK9 does not support default parameter values. Every parameter must be explicitly provided at the call site.

NO VARARGS
Unlike Java or Python, EK9 does not support variable argument lists. If you need flexible argument counts, use a List parameter.

THIS EXAMPLE
The Pair class constructor takes exactly two arguments. The combine() function takes exactly two Pair arguments.

WHY STRICT COUNTS
1. No ambiguity about which overload is called
2. No hidden default values that change behaviour silently
3. Every call site is explicit and self-documenting

See Q48 for why no default parameters. See Q49 for why no varargs. See Q89 for constructors.

defines module qa.classesandoop.argumentcount

  defines class

    Pair
      label <- String()
      count <- Integer()

      Pair()
        ->
          l as String
          c as Integer
        label :=: l
        count :=: c

      describe()
        <- rtn as String: `${label}=${count}`

      default operator ?

  defines program

    ShowPair()
      stdout <- Stdout()
      pair <- Pair("hello", 42)
      if pair?
        stdout.println(pair.describe())

Java: method overloading provides multiple argument counts, varargs with '...'. Python: default values, *args, **kwargs. Rust: no default params, no varargs. Kotlin: default parameter values, varargs. Go: no default params, variadic with '...'. EK9: no defaults, no varargs, strict argument count enforcement.

E06280
E06290
arguments
constructor
count
function
mismatch
parameters
too few
too many

Explanation: The Pair constructor requires two arguments (String and Integer) but only one was provided. The compiler cannot resolve a Pair(String) constructor. Provide all required arguments. See ek9 -h E50060 for details.

Explanation: The Pair constructor requires two arguments but three were provided. The compiler cannot resolve a Pair(String, Integer, Boolean) constructor. Remove the extra argument. See ek9 -h E50060 for details.


Question: When does a switch need a returning block in EK9?

What triggers E07405 returning required?

What triggers E07406 returning not required?

How do I use switch as an expression in EK9?

Answer:
EK9 switch can be used as a STATEMENT (no return) or as an EXPRESSION (returns a value). The compiler enforces consistency:

SWITCH AS STATEMENT
  switch category
    case "A"
      stdout.println("Category A")
    default
      stdout.println("Other")
No '<-' return block. The switch just executes side effects.

SWITCH AS EXPRESSION
  label <- switch category
    <- rtn as String: String()
    case "A"
      rtn: "Alpha"
    default
      rtn: "Unknown"
The '<- rtn as String' declares the return. Each case assigns to rtn.

RULES
- E07405: Using switch in assignment context WITHOUT a return block
- E07406: Adding a return block to a switch NOT in assignment context

THIS EXAMPLE
The classify() function uses switch as an expression with a correct return block. The showCategory program uses switch as a statement without a return block.

See Q63 for switch basics. See Q68 for switch as expression. See Q80 for loop expressions.

defines module qa.controlflow.switchexpression

  defines function

    classify() as pure
      -> category as String
      <- label as String?

      label: switch category
        <- rtn as String: String()
        case "A"
          rtn: "Alpha"
        case "B"
          rtn: "Beta"
        default
          rtn: "Unknown"

  defines program

    ShowCategories()
      stdout <- Stdout()

      result <- classify("A")
      stdout.println(result)

      category <- "B"
      switch category
        case "A"
          stdout.println("Alpha")
        case "B"
          stdout.println("Beta")
        default
          stdout.println("Unknown")

Java: switch expressions (Java 14+) use -> and yield. Python: match/case (3.10+) cannot return values directly. Rust: match is always an expression. Kotlin: when is always an expression. Go: switch is statement-only. EK9: switch can be either, compiler enforces correct form.

E07405 E07406 assign block expression returning statement switch value

      label: switch category
        case "A"
          label: "Alpha"
        case "B"
          label: "Beta"
        default
          label: "Unknown"

Explanation: A switch used in an assignment context ('label: switch ...') requires a returning block ('<- rtn as String'); without it the switch produces no value to assign. See ek9 -h E07405 for details.

      switch category
        <- rtn as String: String()
        case "A"
          stdout.println("Alpha")

Explanation: Adding a returning block to a switch that is not in an assignment context is pointless — the return value would be discarded. Remove the '<- rtn' declaration. See ek9 -h E07406 for details.


Question: Why does EK9 reject override when there is no parent method?

Why does my override keyword cause an error?

How do I correctly override a parent method in EK9?

Answer:
Note: override applies to both methods and operators (e.g., override operator ? as pure, override operator $ as pure).

The 'override' keyword in EK9 means 'I am replacing an inherited method from my parent'. If no matching method exists in the parent, the compiler rejects it — you cannot override something that does not exist.

COMMON CAUSES
1. Typo in method name — parent has 'process', child says 'override proccess'
2. Wrong parameter types — parent takes String, child takes Integer
3. Method is private in parent — private methods are not inherited
4. Method does not exist in parent — wrong assumption about inheritance

THIS EXAMPLE
The Logger class has a log() method. The FileLogger overrides it correctly. The format() method is new to FileLogger (no override needed).

WHY THIS MATTERS
The override keyword prevents silent shadowing. Without it, a method with the same name as a parent method would hide the parent version — a common source of bugs. EK9 requires explicit intent.

See Q77 for inheritance. See Q775 for constructor override rules. See Q778 for access modifier rules.

defines module qa.classesandoop.doesnotoverride

  defines class

    Logger as open
      log()
        -> message as String
        stdout <- Stdout()
        stdout.println(message)

      default operator ?

    FileLogger extends Logger

      override log()
        -> message as String
        stdout <- Stdout()
        stdout.println(`[FILE] ${message}`)

      format()
        -> message as String
        <- rtn as String: `[FILE] ${message}`

      default operator ?

  defines program

    ShowLogger()
      logger <- FileLogger()
      if logger?
        logger.log("hello")
        stdout <- Stdout()
        stdout.println(logger.format("formatted"))

Java: @Override annotation is optional but recommended. Python: no override concept, methods silently shadow. Rust: no override, trait implementations are explicit. Kotlin: override keyword required (same as EK9). Go: no override, methods on embedded structs can be shadowed. EK9: override keyword mandatory when replacing inherited methods.

E05110 inherit match method override parent shadow signature typo

override keyword
E05110
method inheritance

      override format()
        -> message as String
        <- rtn as String: `[FILE] ${message}`

Explanation: The format() method does not exist in the parent Logger class, so 'override' is incorrect. The method is new to FileLogger — remove the override keyword. See ek9 -h E05110 for details.

      override log()
        -> message as String

      override logging()
        -> message as String

Explanation: The parent has log() but the child says 'override logging()' — the method name does not match any parent method. Fix the typo to match the parent's method name. See ek9 -h E05110 for details.


Question: Why can't I extend a class in EK9?

What triggers E05030 not open to extension?

How do I make a class extensible in EK9?

Answer:
EK9 classes are CLOSED by default — they cannot be extended unless explicitly marked 'as open'. This is the opposite of Java (where classes are open by default) and the same as Kotlin.

WHY CLOSED BY DEFAULT
1. Extending a class creates tight coupling between parent and child
2. The parent must be designed for inheritance (fragile base class problem)
3. Composition is almost always better than inheritance
4. Built-in types (List, Dict, Optional) are deliberately closed

HOW TO MAKE EXTENSIBLE
  MyClass as open       // Now it can be extended
  MyClass as abstract   // Abstract classes are implicitly open

COMPOSITION ALTERNATIVE
Instead of extending a closed class, wrap it as a field:
  MyWrapper
    items as List of String
    add(item as String)
      items += item

THIS EXAMPLE
The Shape class is marked 'as open' so Circle can extend it. The Config class is closed — the program uses it directly.

See Q83 for why closed by default. See Q84 for the open modifier. See Q85 for composition.

defines module qa.classesandoop.closedbydefault

  defines class

    Shape as open
      name <- String()

      Shape()
        -> n as String
        name :=: n

      describe()
        <- rtn as String: name

      default operator ?

    Circle extends Shape
      radius <- Float()

      Circle()
        ->
          n as String
          r as Float
        super(n)
        radius :=: r

      override describe()
        <- rtn as String: `${super.describe()} r=${radius}`

      default operator ?

    ItemHolder
      items <- List() of String

      addItem()
        -> item as String
        items += item

      count()
        <- rtn as Integer: length items

      default operator ?

  defines program

    ShowShapes()
      stdout <- Stdout()
      circle <- Circle("circle", 5.0)
      if circle?
        stdout.println(circle.describe())

      holder <- ItemHolder()
      holder.addItem("one")
      holder.addItem("two")
      stdout.println($holder.count())

Java: classes open by default, use 'final' to close. Python: classes always open. Rust: no class inheritance. Kotlin: classes closed by default, use 'open' keyword (same as EK9). Go: no class inheritance, composition only. C#: classes open by default, use 'sealed' to close. EK9: closed by default like Kotlin, 'as open' to allow extension.

E05030 closed composition default extends final inheritance open sealed

closed by default
E05030
inheritance design

    Shape as open
      name <- String()

Explanation: Removing 'as open' makes Shape closed. Circle cannot extend a closed class. Either add 'as open' to Shape, or use composition instead of inheritance. See ek9 -h E05030 for details.

      items <- List() of String

      addItem()

    ItemList extends List of String

      addItem()

Explanation: List is a built-in closed type — it cannot be extended. Use composition: hold a List as a field instead of inheriting from it. See ek9 -h E05030 for details.


Question: Why must variables be declared before use in EK9?

What triggers E08010 used before defined?

Why does EK9 require declaration before use?

How do I fix a variable used before it is declared?

Answer:
EK9 enforces strict top-to-bottom declaration order within a block. A variable must be declared before any line that references it. This prevents entire categories of bugs where variables are used with undefined or unexpected values.

WHY DECLARATION BEFORE USE
1. No undefined variable reads — every variable has a known value when first used
2. No hoisting surprises — unlike JavaScript, variables don't 'float' to the top
3. Clear data flow — reading top-to-bottom shows the complete picture

THIS EXAMPLE
The process() function declares greeting first, then uses it. The calculate() function declares both operands before using them in the result.

COMMON CAUSES
1. Rearranging code without moving declarations
2. Referencing a variable from a different scope
3. Typo creating a new name instead of using existing variable

See Q22 for variable declarations. See Q19 for assignment operators.

defines module qa.dataflowsafety.usedbeforedefined

  defines program

    ShowGreeting()
      stdout <- Stdout()
      greeting <- "hello"
      stdout.println(greeting)

      first <- 10
      second <- 20
      result <- first + second
      stdout.println($result)

Java: fields can be used before declaration in class scope, locals must be declared first. Python: no declaration needed, NameError at runtime if not assigned. Rust: must declare before use. Kotlin: must declare before use. Go: must declare before use. JavaScript: var is hoisted (surprising), let/const must be declared first. EK9: strict declaration-before-use in all scopes.

E08010 before declare defined hoisting order scope use variable

declaration order
E08010
data flow safety

      stdout.println(greeting)
      greeting <- "hello"

Explanation: The variable 'greeting' is used on the println line before it is declared on the next line. Move the declaration before the first use. See ek9 -h E08010 for details.


Question: Which keywords from other languages are excluded from EK9?

Why does EK9 reject goto, def, elif, None, and self?

What triggers E01076 goto not supported?

Why does my Python-style code fail in EK9?

Answer:
EK9 deliberately excludes several keywords from other languages. These are not missing features — they were designed out of existence based on decades of evidence.

EXCLUDED KEYWORDS
- goto (E01076): Creates spaghetti code. Apple SSL bug (2014) caused by goto.
- def (E01077): Python keyword. EK9 uses indentation-based declaration.
- elif (E01078): Python keyword. EK9 uses 'else if' (two words).
- None (E01079): Python keyword. EK9 uses tri-state (unset, not null/None).
- self (E01080): Python keyword. EK9 uses 'this' for instance reference.
- semicolons (E01074): Not needed. EK9 uses newlines as statement terminators.

ALSO EXCLUDED (not keywords but designed out)
- break, continue, return: See Q84-Q85 for why and alternatives.
- null: EK9 uses tri-state semantics instead.

THIS EXAMPLE
Shows correct EK9 patterns that replace these excluded constructs.

See Q274-Q281 for common AI mistakes. See Q84 for why no break/continue. See Q85 for why no return.

defines module qa.gettingstarted.excludedkeywords

  defines program

    GradeChecker()
      -> score as Integer
      stdout <- Stdout()

      excellentThreshold <- 90
      goodThreshold <- 60

      if score > excellentThreshold
        stdout.println("Excellent")
      else if score > goodThreshold
        stdout.println("Good")
      else
        stdout.println("Needs work")

      stdout.println("Done")

Python: def, elif, None, self are core keywords. Java: goto is reserved but unused, semicolons required. C/C++: goto exists, semicolons required. Rust: no goto, no null (Option instead). Go: goto exists but discouraged, no semicolons needed. EK9: all excluded at the grammar level — compiler rejects them with specific error messages.

excluded keywords
language design
migration

Explanation: The name 'elif' is an excluded keyword from Python and cannot be used as an identifier in EK9. Use 'else if' for chained conditions, and choose a different variable name. See ek9 -h E01078 for details.

Explanation: EK9 does not use semicolons. Newlines are statement terminators. Remove the semicolon. See ek9 -h E01074 for details.


Question: Why can't I use 'this' in a function in EK9?

What triggers E05070 inappropriate use of this?

Answer:
The 'this' keyword refers to the current object instance. It only exists inside class methods, constructors, and operators — places where an object instance is available.

WHERE THIS IS VALID
- Class methods and operators (refers to the class instance)
- Record operators (refers to the record instance)
- Component methods (refers to the component instance)
- Constructors (refers to the object being created)

WHERE THIS IS NOT VALID
- Functions (stateless, no instance)
- Programs (not an object)
- Module-level code

THIS EXAMPLE
The Counter class uses this.count in its methods — valid because methods have an instance. The standalone increment() function has no instance, so it uses parameters instead.

See Q50 for functions vs methods. See Q93 for class basics.

defines module qa.classesandoop.thisinkclasses

  defines function

    increment() as pure
      -> current as Integer
      <- rtn as Integer: current + 1

  defines class

    Counter
      count <- Integer()

      Counter()
        -> initial as Integer
        count :=: initial

      next()
        count: this.count + 1

      current()
        <- rtn as Integer: this.count

      default operator ?

  defines program

    ShowCounter()
      stdout <- Stdout()
      counter <- Counter(0)
      counter.next()
      counter.next()
      stdout.println($counter.current())
      result <- increment(10)
      stdout.println($result)

Java: this available in all instance methods, not in static methods. Python: self is an explicit parameter (same concept). Rust: self/&self parameter in impl methods. Kotlin: this available in class members. Go: receiver parameter serves as this. EK9: this only in class/record/component methods and constructors, never in functions.

E05070 class function instance method scope stateless this

this keyword
E05070
functions vs methods

      <- rtn as Integer: current + 1
      copy <- this()

Explanation: Using this() in a function attempts constructor delegation, but functions have no constructors. The this() call is only valid inside a class constructor. See ek9 -h E05070 for details.


Question: Why does a switch expression require a default case in EK9?

What triggers E07330 default required in switch expression?

Why must switch expressions be exhaustive in EK9?

How do I add a default case to a switch expression?

Answer:
When switch is used as an EXPRESSION (capturing its result), every possible input must produce a value. A missing default means some inputs have no result — the compiler rejects this.

SWITCH EXPRESSION (requires default)
  label <- switch category
    <- rtn as String: String()
    case "A"
      rtn: "Alpha"
    default
      rtn: "Unknown"

SWITCH STATEMENT (default optional)
  switch category
    case "A"
      stdout.println("Alpha")

EXCEPTION: ENUMERATED TYPES
When switching on an enumeration and all values are covered, no default is needed — the compiler knows the match is exhaustive.

THIS EXAMPLE
The describe() function uses switch as expression with a default case. The mutation removes the default.

See Q63 for switch basics. See Q73 for exhaustive enum switch. See Q782 for switch expressions.

defines module qa.controlflow.switchdefault

  defines function

    describe() as pure
      -> category as String
      <- label as String?

      label: switch category
        <- rtn as String: String()
        case "A"
          rtn: "Alpha"
        case "B"
          rtn: "Beta"
        case "C"
          rtn: "Gamma"
        default
          rtn: "unknown category"

  defines program

    ShowCategories()
      stdout <- Stdout()
      stdout.println(describe("A"))
      stdout.println(describe("Z"))

Java: switch expressions (14+) require exhaustive coverage or default. Python: match/case has no exhaustiveness check. Rust: match must be exhaustive (compiler enforced). Kotlin: when used as expression must be exhaustive. Go: switch statement only, no expression form. EK9: switch expression requires default unless enum is fully covered.

E07330 case default exhaustive expression missing required switch

switch default
E07330
exhaustive matching

        default
          rtn: "unknown category"

Explanation: A switch expression must handle all possible inputs. Without a default case, some values of category would produce no result. Add a default case to handle unmatched values. See ek9 -h E07330 for details.


Question: Why does EK9 reject unused variables?

How do I fix an unused variable error in EK9?

Why must every variable be used after declaration?

Answer:
EK9 requires that every declared variable is used after its declaration. An unused variable is dead code — it was either left over from refactoring, or the developer forgot to use it.

WHY UNUSED VARIABLES ARE ERRORS
1. Dead code obscures intent — readers wonder why the variable exists
2. Refactoring remnants — code was changed but cleanup was incomplete
3. Possible bug — the developer meant to use it but used a different name

THIS EXAMPLE
The greet() function declares greeting and uses it in the return. Every variable serves a purpose.

COMMON FIX
1. Remove the unused variable if it is not needed
2. Use it in subsequent code if it was accidentally forgotten
3. If the value is needed for side effects only, assign to a used variable

See Q785 for declaration order. See Q22 for variable declarations.

defines module qa.dataflowsafety.unusedvariable

  defines function

    greet() as pure
      -> name as String
      <- rtn as String?

      greeting <- "hello"
      rtn: `${greeting} ${name}`

  defines program

    ShowGreeting()
      stdout <- Stdout()
      stdout.println(greet("world"))

Java: unused local variables are warnings, not errors. Python: no enforcement, linters can detect. Rust: warnings for unused variables, prefix with _ to suppress. Kotlin: warnings only. Go: unused variables are compile errors (same as EK9). EK9: unused variables are compile errors, like Go.

E08090 code dead declared refactoring referenced unused variable

      greeting <- "hello"
      unused <- "forgotten"

Explanation: The variable 'unused' is declared but never referenced after assignment. Remove it or use it. See ek9 -h E08090 for details.


Question: Why can't I reassign variables in a pure function?

What triggers E08100 no pure reassignment?

What is allowed in a pure context in EK9?

How do I write pure functions without reassignment?

Answer:
In EK9, pure functions cannot reassign variables using ':='. This ensures pure functions have no side effects — they compute a result from their inputs without modifying state.

WHAT IS ALLOWED IN PURE
- Declaration with initialisation: 'result <- computation()'
- Guarded assignment: 'value :=? fallback' (only assigns if unset)
- Return variable initialisation: '<- rtn as Type: expression'

WHAT IS NOT ALLOWED IN PURE
- Reassignment: 'existing := newValue' (E08100)
- Mutation operators: '+=', '-=', ':=:', '++', '--' (E08120)
- Calling non-pure methods (E08130)

THIS EXAMPLE
The add() function is pure. It declares result with '<-' and returns it. No reassignment needed.

WHY PURITY MATTERS
1. Pure functions are thread-safe — no shared mutable state
2. Results are cacheable — same inputs always produce same output
3. Easier to test — no setup or teardown of state
4. Compiler can optimise — knows no side effects occur

See Q43 for pure function basics. See Q560 for purity contracts.

defines module qa.puritycontracts.noreassignment

  defines function

    add() as pure
      ->
        left as Integer
        right as Integer
      <- rtn as Integer?

      total <- left + right
      rtn: total

  defines program

    ShowAdd()
      stdout <- Stdout()
      result <- add(10, 20)
      stdout.println($result)

Java: no purity enforcement, rely on convention. Python: no purity concept. Rust: mut keyword controls mutability but no function-level purity. Kotlin: val prevents reassignment but no pure functions. Go: no purity concept. Haskell: all functions pure by default. EK9: explicit 'as pure' keyword with compiler enforcement.

E08100
function
immutable
pure
reassignment
side effect
thread safe

      total <- 0
      total := left + right

Explanation: Reassigning 'total' with ':=' is not allowed in a pure function. Use declaration with initialisation '<-' instead. Pure functions cannot modify variables after declaration. See ek9 -h E08100 for details.

      total <- left
      total += right

Explanation: Using the mutation operator '+=' in a pure function is not allowed. Mutation operators modify state in place — pure functions must not mutate. Use '<- left + right' to compute the result in a single declaration. See ek9 -h E08120 for details.


Question: Why can't I call a non-pure function from a pure function?

What triggers E08130 none pure call in pure scope?

How do I fix impure call in pure context?

What functions can I call from a pure function?

Answer:
A pure function can only call other pure functions and methods. Calling a non-pure function would allow side effects to leak into the pure context, breaking the purity guarantee.

THE PURITY CHAIN
If A is pure and calls B, then B must also be pure. If B calls C, then C must be pure too. The entire call chain from a pure function must be pure.

THIS EXAMPLE
The double() and quadruple() functions are both pure. quadruple() calls double() — valid because double() is also pure. The logResult() function is NOT pure (it uses Stdout). The mutation adds a call to logResult() inside quadruple().

See Q43 for pure function basics. See Q790 for reassignment in pure.

defines module qa.puritycontracts.impurecall

  defines function

    double() as pure
      -> input as Integer
      <- rtn as Integer: input * 2

    quadruple() as pure
      -> input as Integer
      <- rtn as Integer?

      rtn: double(double(input))

    logResult()
      -> result as Integer
      stdout <- Stdout()
      stdout.println($result)

  defines program

    ShowPurity()
      stdout <- Stdout()
      result <- quadruple(5)
      logResult(result)
      stdout.println($result)

Java: no purity enforcement. Python: no purity concept. Rust: no function-level purity, uses ownership for safety. Kotlin: no purity concept. Haskell: IO monad separates pure from impure. EK9: explicit 'as pure' with transitive enforcement — the entire call chain must be pure.

E08130
call
chain
function
impure
pure
side effect

      rtn: double(double(input))
      logResult(rtn)

Explanation: Calling the non-pure function logResult() from a pure function is not allowed. Pure functions can only call other pure functions. See ek9 -h E08130 for details.


Question: Why must a switch on an enumeration cover all values?

What triggers E07310 not all enumerated values present in switch?

How do I make an exhaustive enum switch in EK9?

Do I need a default when switching on an enum?

Answer:
A switch on an enumeration must do BOTH: cover every declared value AND provide a default. These are two separate requirements, enforced by two different errors — a default does NOT excuse you from listing the cases.

WHY EVERY VALUE MUST BE COVERED (E07310)
If you add a new value to the enum later (e.g. PURPLE), the compiler flags every switch that doesn't handle it.

WHY A DEFAULT IS ALSO REQUIRED (E07320 statement / E07330 expression)
EK9 enumerations are tri-state like every other EK9 type: an enumeration variable can exist but be UNSET. 'Color()' is unset, and 'Color("Purple")' is also unset because the text matches no declared value — EK9 returns an unset Color rather than throwing. No 'case' can ever match the unset state, so 'default' is the branch that handles it.

This is why the default does not weaken exhaustiveness: the cases stay exhaustive over the DECLARED values, and the default covers 'no value at all'.

THIS EXAMPLE
describe() handles all three colours and has a default returning "unset" — the branch taken by describe(Color()).

See Q73 for exhaustive enum switch. See Q87 for enumerations. See Q788 for switch default required.

defines module qa.controlflow.enumswitch

  defines type

    Color
      Red,
      Green,
      Blue

  defines function

    describe() as pure
      -> shade as Color
      <- rtn as String: "unknown"

      switch shade
        case Color.Red
          rtn: "red"
        case Color.Green
          rtn: "green"
        case Color.Blue
          rtn: "blue"
        default
          rtn: "unset"

  defines program

    ShowColors()
      stdout <- Stdout()
      stdout.println(describe(Color.Red))
      stdout.println(describe(Color.Blue))

      //An enumeration is tri-state, so a Color can exist while holding no value.
      //No 'case' can match that - which is why 'default' is mandatory. Both print "unset".
      stdout.println(describe(Color()))
      stdout.println(describe(Color("Purple")))

Java: exhaustive switch on sealed types (Java 21+). Python: match/case has no exhaustiveness check. Rust: match on enum must be exhaustive (compiler enforced). Kotlin: when on sealed class must be exhaustive. Go: no exhaustiveness check on switch. EK9: compiler enforces all declared enum values covered AND a default — unlike Rust/Kotlin, an EK9 enumeration is tri-state and can be UNSET, and the default is the branch that handles that state.

E07310
E07320
E07330
all values
case
default
enum
exhaustive
missing
switch
tri-state
unset

exhaustive enum switch
E07310
enumerations

        case Color.Green
          rtn: "green"

Explanation: The switch on Color only handles Red and Green — Blue is missing, so E07310 fires. Add 'case Color.Blue'. Note the existing 'default' does NOT satisfy this: the default is separately mandatory (E07320/E07330) because it handles the UNSET enumeration state, not the missing case. See ek9 -h E07310 for details.


Question: Why does EK9 reject None and self as identifiers?

What triggers E01079 None not supported?

Why can't I use self in EK9 like Python?

Answer:
EK9 deliberately excludes the Python keywords 'None' and 'self' from the language.

NONE (E01079)
Python uses 'None' as its null equivalent. EK9 uses tri-state semantics (absent/unset/set) instead. The '?' operator checks whether a value is set. There is no null or None concept in EK9.

SELF (E01080)
Python uses 'self' as the explicit instance reference. EK9 uses 'this' (implicit in most contexts), consistent with Java, C++, and JavaScript conventions.

CORRECT PATTERNS
- Check if set: 'if userName?' (not 'if userName == None')
- Unset variable: 'userName <- String()' creates an unset String
- Instance reference: 'this.fieldName' (not 'self.fieldName')

See Q786 for other excluded keywords. See Q39-Q41 for tri-state semantics.

defines module qa.gettingstarted.noneself

  defines program

    TriStateDemo()
      stdout <- Stdout()

      userName <- String()
      greeting <- "Hello"

      if userName?
        stdout.println(`${greeting} ${userName}`)
      else
        stdout.println("No user name set yet")

Python: None is the null equivalent, self is the instance reference. Java: null exists, this is the instance reference. Rust: no null (Option instead), no self keyword for instances. Kotlin: null with nullable types, this is implicit. Go: nil is the zero value, no self/this keyword. EK9: None and self are rejected at the grammar level with specific error messages guiding developers to EK9 alternatives.

E01079 E01080 None Python excluded isSet keyword self tri-state

excluded keywords
None rejected
self rejected

Explanation: The identifier 'None' is an excluded Python keyword and cannot be used as a variable name in EK9. EK9 uses tri-state semantics (absent/unset/set) instead of None/null. See ek9 -h E01079 for details.

Explanation: The identifier 'self' is an excluded Python keyword and cannot be used as a variable name in EK9. EK9 uses 'this' for instance references. See ek9 -h E01080 for details.