Question: What is the difference between require, assert, and throw?

When should I use require vs assert vs throw?

How do I choose between require, assert, and throw?

What is the EK9 error handling hierarchy?

Answer:
EK9 provides three distinct mechanisms for checking conditions, each for a different context.

REQUIRE (Production Preconditions)
Context: Any code (production, libraries, frameworks).
Behavior: When the condition is false or unset, throws an uncatchable exception. Program terminates.
Purpose: Enforce contracts and preconditions. Caller has a bug if this fails.
Catchable: No. There is no recovery.
Always active: Yes. Cannot be disabled.
Example: require quantity >= 0

ASSERT (Test Validation)
Context: Only valid inside @Test programs. Using assert in production code causes compile error E81012.
Behavior: When the condition is false, the test fails with structured output showing file, line, column, and the failed expression text.
Purpose: Verify expected behavior in tests.
Catchable: Not applicable (test infrastructure handles failures).
Always active: Yes, within test context.
Example: assert result == 42

THROW (Recoverable Exceptions)
Context: Any code.
Behavior: Throws a catchable exception that can be handled with try/catch.
Purpose: Signal exceptional but potentially recoverable conditions.
Catchable: Yes, with try/catch.
Example: throw Exception("File not found")

DECISION GUIDE
Is this a test? Use assert.
Is this a programming error or contract violation? Use require.
Could this reasonably happen at runtime and be recovered from? Use throw.
Is this expected failure in normal flow? Use Result type instead.

KEY DISTINCTIONS
- require is stronger than throw because it cannot be caught.
- assert is compile-time restricted to tests, preventing production misuse.
- throw and try/catch are for runtime error recovery.
- Result is for expected failures that are not exceptional.

See Q134 for try/catch. See Q136 for throwing exceptions. See Q139 for try/catch vs Result. See Q304 for require in depth.

defines module qa.errorhandling.require.vs.assert.vs.throwex

  defines class

    <?-
      Simple domain class used in examples below.
    -?>
    Account
      balance <- Float()

      Account()
        -> initialBalance as Float
        require initialBalance?
        require initialBalance >= 0.0
        balance :=: initialBalance

      deposit()
        -> amount as Float
        require amount?
        require amount > 0.0
        balance += amount

      withdraw()
        -> amount as Float
        require amount?
        require amount > 0.0
        if amount > balance
          throw Exception("Insufficient funds")
        balance -= amount

      getBalance() as pure
        <- rtn as Float: balance

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

      operator $ as pure
        <- rtn as String: `Account[balance=${balance}]`

  defines program

    RequireVsAssertVsThrowDemo()
      stdout <- Stdout()

      stdout.println("=== require: enforcing preconditions ===")

      account <- Account(100.0)
      stdout.println("Created: " + $account)

      account.deposit(50.0)
      stdout.println("After deposit: " + $account)

      stdout.println("=== throw: recoverable exception ===")

      try
        account.withdraw(200.0)
      catch
        -> ex as Exception
        stdout.println("Caught: " + ex.reason())

      stdout.println("Account still usable: " + $account)

      account.withdraw(30.0)
      stdout.println("After valid withdraw: " + $account)

      stdout.println("=== Result: expected failures ===")

      stdout.println("Use Result for operations where failure is normal,")
      stdout.println("like parsing user input or searching a collection.")

Java: assert (can be disabled with -da), throw/try/catch, no require equivalent. Python: assert (disabled with -O), raise/try/except, no require. Go: panic (uncatchable without recover), no assert keyword, error return values. Rust: panic! (uncatchable), assert! (active in debug, removed in release with --release), Result for recoverable errors. Kotlin: require() (catchable IllegalArgumentException), assert (JVM assert), throw/try/catch. C/C++: assert() (disabled with NDEBUG), throw/try/catch. EK9: require is always active and uncatchable (stronger than all), assert is compile-time restricted to tests (cannot misuse in production), throw/try/catch for recoverable situations.

E81012 assert catch comparison decision exception handle precondition require test throw uncatchable

Explanation: The require statement needs a Boolean expression. Passing a Float directly triggers E07530 — only compatible with Boolean type. Use a comparison like 'require amount > 0.0' or 'require amount?' to check if set. See ek9 -h E07530 for details.

Explanation: The throw statement requires a value that extends Exception. Throwing a regular class like Account triggers E04030 — type must be of Exception type. Create a custom exception with 'extends Exception' for domain-specific errors. See ek9 -h E04030 for details.


Question: How do I test that code throws an exception in EK9?

How do I verify an exception is thrown in a test?

What is the EK9 equivalent of JUnit assertThrows?

Answer:
EK9 provides assertThrows as a built-in keyword for verifying that an expression throws an expected exception type. It is only valid inside @Test programs.

BASIC SYNTAX
assertThrows(ExceptionType, expression)
The test passes if the expression throws an exception of the specified type. The test fails if no exception is thrown or a different type is thrown.

AS A STATEMENT
Use assertThrows when you only need to verify the exception occurs:
  assertThrows(Exception, riskyFunction())
If riskyFunction() does not throw, the test fails with a structured error message showing the location, expression, expected type, and what actually happened.

AS AN EXPRESSION (Capture)
Capture the thrown exception for further inspection:
  caught <- assertThrows(Exception, riskyFunction())
  assert caught.reason()?
  assert $caught == "Expected error message"
The returned value is the caught exception, allowing inspection of its reason, exit code, and custom fields.

FAILURE OUTPUT
When assertThrows fails, EK9 provides structured diagnostic output:
  assertThrows FAILED
    Location: ./dev/tests.ek9:5:3
    Expression: riskyFunction()
    Expected: org.ek9.lang::Exception
    Actual: No exception was thrown
This structured output is captured at compile time from the AST, not generated at runtime.

CUSTOM EXCEPTION TYPES
assertThrows works with custom exception types:
  assertThrows(ValidationError, validate(badInput))
The exception type must match exactly. A parent type will not catch a child type in assertThrows.

COMPILE-TIME RESTRICTION
assertThrows is only valid in @Test programs. Using it in production code causes a compile error. This prevents test assertions from accidentally appearing in production.

See Q134 for try/catch. See Q305 for require vs assert vs throw. See Q307 for assertDoesNotThrow. See Q209 for exception testing patterns.

defines module qa.errorhandling.assertthrows

  defines class

    <?-
      Custom exception carrying a field name for validation errors.
    -?>
    ValidationError extends Exception
      field <- String()

      ValidationError()
        ->
          reason as String
          fieldName as String
        super(reason)
        this.field :=: fieldName

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

      default operator ?

  defines function

    <?-
      Always throws an exception for testing purposes.
    -?>
    failingOperation()
      throw Exception("Operation failed")

    <?-
      Validates and throws ValidationError for bad input.
    -?>
    validatePositive()
      -> number as Integer
      <- rtn as Integer: number

      if number < 0
        ex <- ValidationError("Must be positive", "number")
        throw ex

    <?-
      Safe operation that does not throw.
    -?>
    safeAdd() as pure
      ->
        a as Integer
        b as Integer
      <- rtn as Integer: a + b

  defines program

    @Test
    AssertThrowsStatementTest()
      stdout <- Stdout()

      stdout.println("=== assertThrows as statement ===")

      assertThrows(Exception, failingOperation())
      stdout.println("Verified: failingOperation() throws Exception")

      assertThrows(ValidationError, validatePositive(-5))
      stdout.println("Verified: validatePositive(-5) throws ValidationError")

    @Test
    AssertThrowsCaptureTest()
      stdout <- Stdout()

      stdout.println("=== assertThrows as expression (capture) ===")

      caught <- assertThrows(Exception, failingOperation())
      assert caught?
      stdout.println("Caught reason: " + caught.reason())

      valError <- assertThrows(ValidationError, validatePositive(-10))
      assert valError?
      stdout.println("Validation field: " + valError.field())
      stdout.println("Validation reason: " + valError.reason())

Java: JUnit assertThrows(() -> code, ExceptionType.class) returns the exception. Python: pytest.raises(ExceptionType) as context manager. Rust: #[should_panic] attribute on test function (coarse). Go: manual check with recover() in test helper. Kotlin: assertThrows<ExceptionType> { code }. C#: Assert.Throws<ExceptionType>(() => code). EK9: assertThrows(ExceptionType, expression) is a built-in keyword, not a library function. Compile-time restricted to @Test programs. Structured failure output with AST-captured source location.

assertThrows capture catch diagnostic exception handle structured test testing verify

Explanation: The first argument to assertThrows must be an Exception type or subclass. Using a non-exception type like String triggers E04030 — type must be of Exception type. Use Exception or a custom exception class that extends Exception. See ek9 -h E04030 for details.


Question: How do I verify that code does not throw an exception in EK9?

How does assertDoesNotThrow work in EK9?

How do I test the happy path does not throw?

What is the EK9 equivalent of JUnit assertDoesNotThrow?

Answer:
EK9 provides assertDoesNotThrow as a built-in keyword for verifying that an expression completes without throwing any exception. It is only valid inside @Test programs.

SYNTAX
assertDoesNotThrow(expression)
The test passes if the expression completes normally. The test fails if any exception is thrown.

USAGE
Use assertDoesNotThrow to verify no exception occurs:
  assertDoesNotThrow(safeFunction())
If safeFunction() throws, the test fails with structured diagnostic output.

FAILURE OUTPUT
When assertDoesNotThrow fails, EK9 provides structured output:
  assertDoesNotThrow FAILED
    Location: ./dev/tests.ek9:5:3
    Expression: riskyFunction()
    Expected: No exception
    Actual: org.ek9.lang::Exception
    Message: Division by zero
The output includes the exception type and message, captured at compile time from the AST.

WHEN TO USE
assertDoesNotThrow is valuable when:
- Testing boundary conditions that should not throw (edge cases).
- Verifying error recovery leaves the system in a stable state.
- Testing that previously throwing code has been fixed.
- Documenting in tests that certain operations are safe.

ASSERTTHROWS VS ASSERTDOESNOTTHROW
assertThrows(ExceptionType, expr): Verifies an exception IS thrown. Can capture the exception.
assertDoesNotThrow(expr): Verifies NO exception is thrown.
Both are compile-time restricted to @Test programs.

See Q306 for assertThrows. See Q305 for require vs assert vs throw. See Q156 for basic assertions. See Q209 for exception testing patterns.

defines module qa.errorhandling.assertdoesnot

  defines function

    <?-
      A safe division that returns Result instead of throwing.
      Used to demonstrate safe operations.
    -?>
    safeDivide()
      ->
        a as Integer
        b as Integer
      <-
        rtn as Integer: 0

      if b == 0
        throw Exception("Division by zero")
      rtn: a / b

    <?-
      Safe string processing that never throws.
    -?>
    processName() as pure
      -> name as String
      <- rtn as String: `Hello, ${name}!`

  defines program

    @Test
    AssertDoesNotThrowStatementTest()
      stdout <- Stdout()

      stdout.println("=== assertDoesNotThrow as statement ===")

      assertDoesNotThrow(processName("EK9"))
      stdout.println("Verified: processName does not throw")

      assertDoesNotThrow(safeDivide(10, 2))
      stdout.println("Verified: safeDivide(10, 2) does not throw")

    @Test
    AssertDoesNotThrowMultipleTest()
      stdout <- Stdout()

      stdout.println("=== multiple assertDoesNotThrow checks ===")

      assertDoesNotThrow(processName("World"))
      stdout.println("Verified: processName(\"World\") is safe")

      assertDoesNotThrow(safeDivide(100, 5))
      stdout.println("Verified: safeDivide(100, 5) is safe")

      assertDoesNotThrow(safeDivide(0, 1))
      stdout.println("Verified: safeDivide(0, 1) is safe")

Java: JUnit assertDoesNotThrow(() -> code) wraps Executable. Python: no built-in equivalent, just run the code and let pytest catch unexpected exceptions. Rust: no equivalent, tests fail on panic by default. Go: no equivalent, tests fail on unrecovered panic. Kotlin: assertDoesNotThrow { code } from kotlin.test. EK9: assertDoesNotThrow(expression) is a built-in keyword, compile-time restricted to @Test programs, with structured failure output including exception type and message.

assertDoesNotThrow
catch
diagnostic
exception
handle
happy path
no throw
safe
test
testing
verify

processName() as pure
      -> name as String
      <- rtn as String: "Hello!"

Explanation: The parameter name is declared but never used in the function body. All parameters must be referenced. See ek9 -h E08091 for details.


Question: How do I handle different exception types in EK9 without casting?

How do I use the dispatcher pattern with exceptions?

How do I route exceptions to different handlers in EK9?

How does EK9 handle multiple exception types without instanceof?

Answer:
EK9 uses the dispatcher pattern to route different exception types to specific handlers without casting or instanceof checks. Since EK9 has single catch per try block (always catching as base Exception), the dispatcher provides type-safe multi-type exception handling.

THE PATTERN
1. Catch as base Exception in the try/catch block.
2. Pass the caught exception to a dispatcher method.
3. The dispatcher routes to the correct private overload based on actual runtime type.
4. Each overload accesses the specific exception type's methods directly.

DISPATCHER METHOD
Declare the dispatcher with the base Exception type:
  private handleException() as dispatcher
    -> ex as Exception
    <- rtn as String: $ex
This is the fallback for any exception type not specifically handled.

SPECIFIC OVERLOADS
Add private overloads for each exception type to handle:
  private handleException()
    -> ex as NetworkError
    <- rtn as String: "Network: " + ex.host()
  private handleException()
    -> ex as TimeoutError
    <- rtn as String: "Timeout: " + $ex.seconds() + "s"
Each overload receives the correctly typed exception, no casting needed.

USAGE IN CATCH
  try
    riskyOperation()
  catch
    -> ex as Exception
    message <- handleException(ex)
    stderr.println(message)
The dispatcher automatically routes to the correct overload based on the actual exception type.

WHY DISPATCHER
- No casting: each overload receives the correct type.
- No instanceof checks: EK9 does not have instanceof.
- Type-safe: compiler verifies each overload's parameter type.
- Extensible: add new exception types by adding new overloads.
- Clean separation: each handler is a focused, single-purpose method.

See Q138 for exception subtypes. See Q134 for try/catch. See Q136 for throwing exceptions. See Q121 for the dispatcher pattern generally.

defines module qa.errorhandling.exdispatcher

  defines class

    <?-
      Network-related exception with host information.
    -?>
    NetworkError extends Exception
      host <- String()

      NetworkError()
        ->
          reason as String
          host as String
        super(reason)
        this.host :=: host

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

      default operator ?

    <?-
      Timeout exception with duration information.
    -?>
    TimeoutError extends Exception
      seconds <- Integer()

      TimeoutError()
        ->
          reason as String
          seconds as Integer
        super(reason)
        this.seconds :=: seconds

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

      default operator ?

    <?-
      Demonstrates the dispatcher pattern for exception handling.
      Catches base Exception then dispatches to type-specific handlers.
    -?>
    ServiceClient
      stderr as Stderr: Stderr()

      <?-
        Simulate an operation that can throw different exception types.
      -?>
      callService()
        -> endpoint as String
        <- rtn as String: String()

        if endpoint == "network"
          ex <- NetworkError("Connection refused", "api.example.com")
          throw ex
        else if endpoint == "timeout"
          ex <- TimeoutError("Request timed out", 30)
          throw ex
        else if endpoint == "generic"
          throw Exception("Unknown service error")

        rtn: "Success from " + endpoint

      <?-
        Try calling the service and handle any exception via dispatcher.
      -?>
      safeCall()
        -> endpoint as String
        <- rtn as String: String()

        try
          rtn: callService(endpoint)
        catch
          -> ex as Exception
          rtn: handleException(ex)

      <?-
        Dispatcher: fallback for unrecognised exception types.
      -?>
      private handleException() as dispatcher
        -> ex as Exception
        <- rtn as String: "General error: " + ex.reason()

      <?-
        Handler for NetworkError: access host-specific information.
      -?>
      private handleException()
        -> ex as NetworkError
        <- rtn as String: `Network error on ${ex.host()}: ${ex.reason()}`

      <?-
        Handler for TimeoutError: access timeout-specific information.
      -?>
      private handleException()
        -> ex as TimeoutError
        <- rtn as String: `Timeout after ${ex.seconds()}s: ${ex.reason()}`

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

  defines program

    ExceptionDispatcherDemo()
      stdout <- Stdout()

      client <- ServiceClient()

      stdout.println("=== Dispatcher routes exceptions by type ===")

      result1 <- client.safeCall("ok")
      stdout.println("OK endpoint: " + result1)

      result2 <- client.safeCall("network")
      stdout.println("Network endpoint: " + result2)

      result3 <- client.safeCall("timeout")
      stdout.println("Timeout endpoint: " + result3)

      result4 <- client.safeCall("generic")
      stdout.println("Generic endpoint: " + result4)

Java: catch blocks with instanceof or multi-catch (catch IOException | SQLException), or visitor pattern. Python: multiple except clauses ordered by specificity. Go: errors.Is/errors.As with type switches. Rust: match on enum variants in Result. Kotlin: multiple catch blocks or when expression with is checks. EK9: dispatcher pattern eliminates casting and instanceof entirely. Single catch block delegates to dispatcher which routes by runtime type.

casting catch dispatcher exception handle handler instanceof overload polymorphic routing sealed type-safe visitor

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

Oracle can generate a dispatcher for handling different exception types without casting.


Question: Why does string concatenation with + cause a compiler error in EK9?

Why can't I use + to build strings in EK9?

How do I fix PREFER_STRING_INTERPOLATION error in EK9?

Answer:
EK9 enforces string interpolation over concatenation chains. If you concatenate 3 or more parts with the + operator where the result is a String, the compiler produces error E11068: PREFER_STRING_INTERPOLATION.

WHY IT IS AN ERROR
Each + on strings calls the _add operator, creating a new intermediate String. A chain of N concatenations creates N-1 temporary objects. String interpolation compiles to a bespoke STRING_INTERPOLATION IR instruction that backends optimise to one allocation:
  JVM: invokedynamic StringConcatFactory.makeConcatWithConstants()
  LLVM: single allocation with pre-calculated length + memcpy

Beyond performance, interpolation shows the output shape directly. Concatenation requires mentally assembling fragments separated by + operators.

WHAT TRIGGERS E11068
A chain of 3+ parts where the result type is String:
  result <- "Hello " + name + "!"         3 parts: error
  result <- a + " " + b + " " + c         5 parts: error
  result <- prefix() + text + "]"          3 parts: error

WHAT DOES NOT TRIGGER E11068
  result <- first + second                  2 parts: allowed
  total <- a + b + c                        numeric +: not String type
  combined <- list1 + list2 + list3         list +: not String type

HOW TO FIX
Convert the concatenation chain to string interpolation:
  BAD:  result <- "Hello " + name + "!"
  GOOD: result <- `Hello ${name}!`

  BAD:  result <- host + ":" + $port
  GOOD: result <- `${host}:${$port}`

  BAD:  result <- "(" + $x + ", " + $y + ")"
  GOOD: result <- `(${$x}, ${$y})`

Use the $ operator inside ${...} to convert non-String values.

See Q174 for all string combination methods. See Q37 for string basics. See Q43 for escape sequences and literal dollar signs in interpolation.

defines module qa.stringops.preferinterpolation

  defines function

    // Two-part concatenation is allowed
    twoPartConcat()
      ->
        first as String
        second as String
      <- result as String: first + second

    // Interpolation for 3+ parts
    greetPerson()
      -> name as String
      <- result as String: `Hello ${name}!`

    // Mixed types with $ operator
    formatEndpoint()
      ->
        host as String
        port as Integer
      <- result as String: `${host}:${port}`

    // Complex interpolation
    formatCoordinate()
      ->
        x as Integer
        y as Integer
      <- result as String: `(${x}, ${y})`

  defines program
    InterpolationDemo()
      stdout <- Stdout()

      // Two-part concat is fine
      stdout.println(twoPartConcat("Hello", "World"))

      // Interpolation for everything else
      stdout.println(greetPerson("Steve"))
      stdout.println(formatEndpoint("localhost", 8080))
      stdout.println(formatCoordinate(10, 20))

      // Numeric addition is unaffected (not String type)
      a <- 1
      b <- 2
      c <- 3
      total <- a + b + c
      stdout.println(`Total: ${total}`)

Java: no restriction on + chains (StringBuilder optimisation by javac since Java 9, invokedynamic since Java 11). Python: no restriction, but f-strings recommended by PEP 498. Rust: format!() macro preferred over + chains but not enforced. Go: no restriction, but fmt.Sprintf() recommended. JavaScript: template literals recommended over + chains by ESLint prefer-template rule but not a hard error. Kotlin: string templates recommended but + chains compile fine. Swift: string interpolation \\(expr) recommended but + chains compile fine, no enforcement. EK9: 3+ part + chains on String are a hard compiler error (E11068) because EK9 has a bespoke STRING_INTERPOLATION IR instruction that generates provably better code.

E11068 PREFER_STRING_INTERPOLATION backtick compile compiler concatenation debug error interpolation performance plus string swift text

<- result as String: "Hello " + name + "!"

Explanation: Concatenating 3 or more string parts with + triggers E11068. Use backtick interpolation which compiles to a single allocation. See ek9 -h E11068 for details.

Explanation: Concatenating three or more String parts with '+' triggers E11068 — use backtick interpolation `${host}:${port}`, which compiles to a single allocation. See ek9 -h E11068 for details.


Question: How does EK9 enforce code quality at compile time?

How does EK9 enforce quality without external tools?

What quality gates does the EK9 compiler check?

Answer:
EK9 enforces code quality as a mandatory compilation step. If your code compiles, it has passed ALL quality gates. There is no separate linting, SAST scanning, or code review tool needed.

THE QUALITY PYRAMID (5 LAYERS)
Layer 1 - Grammar: dangerous constructs (break, continue, return, null, goto) removed from the language entirely.
Layer 2 - Type system: null safety, type coercion, operator correctness, purity enforcement.
Layer 3 - Code flow: unreachable code, uninitialised variables, guard completeness.
Layer 4 - Security: sanitized parameters, injection detection, purity for data integrity.
Layer 5 - Code quality: complexity, cohesion, coupling, naming, code smells, magic literals.

NO-WARNINGS PHILOSOPHY
EK9 has no warnings. Every check either passes or produces an error. This eliminates the industry pattern of accumulating thousands of ignored warnings. If the compiler is silent, every quality dimension has been satisfied.

VERTICAL INTEGRATION
Traditional projects use 15-20 separate tools: compiler, linter, SAST scanner, coverage tool, style checker, complexity analyser, dependency checker, and more. Each is optional, each has different configuration. EK9 collapses ALL of these into the compiler. One tool, absolute enforcement, no bypass.

WHY THIS MATTERS FOR TEAMS
Code review arguments about style, complexity, and naming disappear. The compiler has already decided. Teams focus on architecture and design instead of bikeshedding over formatting.

See Q311 for the full catalog of quality checks. See Q312 for complexity metrics. See Q313 for code smell detection. See Q321 for the quality report dashboard. See Q144 for how eliminating break/continue/return is part of the quality pyramid. See Q625 for why EK9 enforces a standard code format.
See Q693 for captured operator returns. See Q695 for named constants. See Q696 for complexity limits.
See Q728 for nesting depth boundary. See Q730 for data clump boundary. See Q731 for type traversal boundary. See Q756 for the fuzzing quality loop.

defines module qa.codequality.compiletime

  defines function

    <?-
      A pure function that demonstrates passing all quality gates.
      If this compiles, it satisfies naming, complexity, coupling,
      and every other quality check automatically.
    -?>
    calculateDiscount() as pure
      ->
        originalPrice as Float
        discountPercent as Float
      <-
        discountedPrice as Float: originalPrice

      minimumDiscount <- 0.0
      maximumDiscount <- 100.0

      if discountPercent > minimumDiscount and discountPercent <= maximumDiscount
        reduction <- originalPrice * discountPercent / maximumDiscount
        discountedPrice: originalPrice - reduction

  defines program

    QualityAtCompileTimeDemo()
      stdout <- Stdout()

      fullPrice <- 99.99
      tenPercent <- 10.0
      salePrice <- calculateDiscount(fullPrice, tenPercent)

      stdout.println(`Original: ${fullPrice}`)
      stdout.println(`Discounted: ${salePrice}`)

Java: SonarQube, Checkstyle, PMD, SpotBugs, JaCoCo, ErrorProne (all separate, all optional). Python: pylint, flake8, mypy, bandit, radon (all separate, all optional). Rust: clippy (built-in but warnings only, can be ignored). Go: go vet + staticcheck (some built-in, still separate tools). JavaScript: ESLint, TypeScript, SonarJS (all separate, all optional). EK9: ALL quality checks in the compiler, mandatory, no external tools, no configuration, no bypass.

clean-code compile enforce gate integration mandatory metric pyramid quality sonarqube vertical warnings

code quality
quality checks
static analysis

salePriceXYZ <- calculateDiscount(fullPrice, tenPercent)

Explanation: Renaming the variable means later references to 'salePrice' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Removing the variable declaration means later references to the variable become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: What code quality checks does the EK9 compiler perform?

What are all the EK9 quality error codes?

Answer:
The EK9 compiler performs over 30 distinct code quality checks, each with a specific error code. These are grouped into families.

COMPLEXITY (E11010-E11012, E11020-E11021)
E11010: Cyclomatic complexity exceeds threshold (default 45).
E11011: Nesting depth exceeds threshold (default 6 levels).
E11012: Statement count exceeds threshold.
E11020: Combined complexity product exceeds threshold.
E11021: Cognitive complexity exceeds threshold (default 35).

COHESION AND COUPLING (E11014-E11017)
E11014: Lack of Cohesion (LCOM4) exceeds threshold.
E11015: Efferent coupling (Ce) exceeds threshold.
E11016: Module-level coupling exceeds threshold.
E11017: Module-level cohesion below threshold.

INHERITANCE (E11019)
E11019: Inheritance depth exceeds threshold (class/trait 4, function 3, record 2).

RESPONSIBILITY (E11022-E11025)
E11022: Class should be a component (too many injected dependencies).
E11023: God class detected (too many methods and fields).
E11024: Missing delegation (class implements trait without 'by' delegation).
E11025: Hybrid aggregate (mixes data and behaviour excessively).

NAMING (E11030-E11032)
E11030: Type name does not follow conventions.
E11031: Non-descriptive variable name (temp, data, obj, etc.).
E11032: Variable name shadows operator keyword.

INJECTION (E11040)
E11040: Component has too many injection fields.

DISCARDED RETURNS (E11050-E11055)
E11050: Computational operator result discarded.
E11051: Pure function return value discarded.
E11052: Result or Optional return value not checked.
E11055: Constructor result discarded (always wrong).

CODE SMELLS (E11053-E11054, E11061-E11062, E11068)
E11053: Data clump detected (extract a record).
E11054: Law of Demeter violation (train wreck).
E11061: Named argument required at call site.
E11062: Named argument position mismatch.
E11068: String concatenation should use interpolation.

MAGIC LITERALS (E11064-E11067)
E11064: Magic literal in comparison.
E11065: Repeated literal (3+ per file or 4+ per module).
E11066: Constant not in dedicated constant block.
E11067: Constant naming convention violation.

SELF-OPERATIONS (E08080-E08081)
E08080: Self-assignment is pointless.
E08081: Self-comparison always produces a constant result.

FLOW CONDITIONS (E08082-E08089)
E08082: Constant comparison (both operands are literals).
E08083: Constant arithmetic (both operands are literals).
E08084: Redundant boolean comparison (comparing boolean with true/false).
E08085: Logical tautology (expression always true or always false).
E08086: Condition always true (flow-sensitive dead code).
E08087: Condition always false (flow-sensitive dead code).
E08088: Redundant isSet check (variable known to be set).
E08089: Never-set isSet check (variable known to be unset).

UNUSED (E08091, E11018)
E08091: Unused parameter detected.
E11018: Unused closure capture.

EMPTY CHECKS (E08092-E08093)
E08092: Redundant empty check (collection known to be non-empty).
E08093: Never-empty check (collection known to be empty).

See Q312 for complexity detail. See Q313 for code smells. See Q314 for cohesion and coupling. See Q315 for inheritance depth. See Q316 for discarded returns. See Q317 for magic literals. See Q318 for self-operations. See Q319 for unused parameters. See Q290 for naming rules. See Q557 for dead code detection. See Q558 for tautological conditions. See Q559 for redundant isSet. See Q247 for reading error messages. See Q248 for error code lookup.

defines module qa.codequality.catalog

  defines function

    <?-
      Demonstrates a function that passes all quality checks.
      Every check in the catalog has been satisfied for this to compile.
    -?>
    gradeScore() as pure
      -> score as Integer
      <- grade as String: "F"

      excellentThreshold <- 90
      goodThreshold <- 70
      passThreshold <- 50

      if score >= excellentThreshold
        grade: "A"
      else if score >= goodThreshold
        grade: "B"
      else if score >= passThreshold
        grade: "C"

  defines program

    QualityChecksCatalogDemo()
      stdout <- Stdout()

      testScore <- 85
      result <- gradeScore(testScore)
      stdout.println(`Score ${testScore} earns grade: ${result}`)

Java: SonarQube has 600+ rules but all optional. PMD has 300+ rules but all configurable. EK9: 30+ mandatory rules, zero configuration, cannot be disabled. Rust: clippy has 500+ lints but most are allow-by-default. Go: go vet checks a small subset, staticcheck adds more but all optional. Python: pylint has 200+ checks but all configurable. EK9 is the only language where ALL quality checks are mandatory compiler errors.

Explanation: Renaming the variable means later references to 'testScore' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Renaming the variable means later references to 'result' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: How does EK9 measure complexity?

How does EK9 calculate cyclomatic complexity?

Answer:
EK9 measures complexity using five metrics, all enforced at compile time. Exceeding any threshold is a compilation error, not a warning.

CYCLOMATIC COMPLEXITY (E11010)
Measures the number of independent paths through a function. Each if, else if, while, for, switch case, and guard adds one. Threshold: 45. Based on McCabe 1976 research showing defect rate increases sharply above this level.

NESTING DEPTH (E11011)
Measures the deepest level of nesting in a function. Each nested if, for, while, switch, or try adds one level. Threshold: 6. Deep nesting correlates with bugs because humans lose track of context beyond 3-4 levels.

STATEMENT COUNT (E11012)
Counts executable statements in a function. Long functions are harder to understand, test, and modify. The threshold encourages decomposition into smaller, focused functions.

COMBINED COMPLEXITY (E11020)
A product formula combining cyclomatic complexity, nesting depth, and statement count. This catches functions that are individually below each threshold but collectively too complex. A function with 40 cyclomatic, 5 nesting, and high statements can still be flagged.

COGNITIVE COMPLEXITY (E11021)
Measures how hard code is to understand, not just how many paths exist. Cognitive complexity penalises nesting more heavily than flat structures. A deeply nested if-else chain scores higher than the same logic refactored into flat guard expressions. Threshold: 35.

WHAT ADDS COMPLEXITY
Each of these adds to complexity:
- if/else if/else branches
- for/while loops
- switch cases
- try/catch handlers
- guard expressions (but less than nested ifs)
- Boolean operators in conditions

DECOMPOSITION PATTERN
When a function is too complex, extract helper functions. Each extracted function has its own complexity budget. This is exactly the decomposition that EK9 philosophy encourages.

See Q311 for the full quality checks catalog. See Q313 for code smell detection. See Q146 for decomposition over return. See Q630 for identifying hot methods via profiling.

defines module qa.codequality.complexity

  defines function

    <?-
      A well-decomposed function stays within complexity thresholds.
      Each helper handles one concern with low individual complexity.
    -?>
    isWeekday() as pure
      -> dayNumber as Integer
      <- rtn as Boolean: false

      mondayIndex <- 1
      fridayIndex <- 5
      if dayNumber >= mondayIndex and dayNumber <= fridayIndex
        rtn: true

    describeDayType() as pure
      -> dayNumber as Integer
      <- description as String: "weekend"

      if isWeekday(dayNumber)
        description: "weekday"

    formatDayReport() as pure
      ->
        dayName as String
        dayNumber as Integer
      <-
        report as String: ""

      dayType <- describeDayType(dayNumber)
      report: `${dayName} is a ${dayType}`

  defines program

    ComplexityMetricsDemo()
      stdout <- Stdout()

      mondayNumber <- 1
      saturdayNumber <- 6

      mondayReport <- formatDayReport("Monday", mondayNumber)
      saturdayReport <- formatDayReport("Saturday", saturdayNumber)

      stdout.println(mondayReport)
      stdout.println(saturdayReport)

Java: SonarQube measures cyclomatic and cognitive complexity but as optional warnings. PMD has CyclomaticComplexity rule but configurable threshold. Rust: no built-in complexity measurement, clippy has cognitive_complexity lint (allow by default). Go: gocyclo is a third-party tool, not built-in. Python: radon measures complexity but is a separate optional tool. EK9: all five complexity metrics built into the compiler as mandatory errors with fixed thresholds.

E11010 E11011 E11020 E11021 clean-code cognitive complexity cyclomatic depth mccabe metric metrics nesting quality threshold

Explanation: Renaming the variable means later references to 'mondayNumber' become unresolved, triggering E50001. See ek9 -h E50001 for details.

mondayReportXYZ <- formatDayReport("Monday", mondayNumber)

Explanation: Renaming the variable means later references become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: How does EK9 detect code smells?

How does EK9 detect god classes and data clumps?

Answer:
EK9 detects several categories of code smell at compile time and rejects code that exhibits them.

RESPONSIBILITY SMELLS (E11022-E11025)
E11022 Class should be component: When a class has too many injected dependencies, it should be restructured as a component.
E11023 God class: A class with too many methods and fields is doing too much. Split into smaller, focused classes.
E11024 Missing delegation: A class implements a trait but does not use 'by' delegation. Manual forwarding is error-prone.
E11025 Hybrid aggregate: A type mixes data storage and complex behaviour. Separate into a record (data) and a class (behaviour).

DATA CLUMPS (E11053)
When the same group of parameters appears together in multiple function signatures, extract them into a record. Records give the group a name and make the code self-documenting.

LAW OF DEMETER (E11054)
Chained method calls like a.getB().getC().doSomething() violate the Law of Demeter. Each object should only talk to its immediate collaborators. EK9 detects excessive chaining and flags it.

STRING CONCATENATION (E11068)
Using the + operator to build strings with three or more parts should use string interpolation instead. Interpolation is clearer, faster, and avoids intermediate string allocations.

NAMED ARGUMENTS (E11061-E11062)
Calls with many positional arguments are hard to read. EK9 requires named arguments beyond a threshold to prevent parameter order mistakes.

See Q311 for the full quality checks catalog. See Q312 for complexity metrics. See Q314 for cohesion and coupling. See Q109 for composition patterns. See Q267 for anti-patterns including god classes.

defines module qa.codequality.smells

  defines record

    <?-
      Extract related fields into a record to avoid data clumps.
      Instead of passing street, city, postcode separately everywhere,
      group them into an Address record.
    -?>
    Address
      street as String: String()
      city as String: String()
      postcode as String: String()

      Address()
        ->
          street as String
          city as String
          postcode as String
        this.street :=? street
        this.city :=? city
        this.postcode :=? postcode

      override operator ? as pure
        <- rtn as Boolean: street? and city? and postcode?

      operator $ as pure
        <- rtn as String: `${street}, ${city} ${postcode}`

  defines function

    <?-
      Uses the Address record instead of three separate parameters.
      This eliminates the data clump smell.
    -?>
    formatMailingLabel() as pure
      ->
        recipientName as String
        recipientAddress as Address
      <-
        label as String: `${recipientName}\n${recipientAddress}`

  defines program

    CodeSmellsDemo()
      stdout <- Stdout()

      homeAddress <- Address("10 High Street", "London", "SW1A 1AA")
      mailingLabel <- formatMailingLabel("Jane Smith", homeAddress)
      stdout.println(mailingLabel)

Java: SonarQube detects god classes, data clumps, and feature envy but as optional warnings. PMD has GodClass and LawOfDemeter rules but configurable. Rust: clippy has no code smell detection beyond simple patterns. Go: no code smell detection built-in. Python: pylint detects some smells but all optional. EK9: code smell detection built into the compiler as mandatory errors.

E11022 E11053 E11068 class clean-code clump delegation demeter god hybrid interpolation metric quality responsibility smell

label as String: recipientName + "\n" + $recipientAddress

Explanation: Building a String from three or more parts with + should use interpolation; `recipientName + "\n" + $recipientAddress` triggers E11068 - use a backtick `${...}` template. See ek9 -h E11068 for details.

mailingLabelXYZ <- formatMailingLabel("Jane Smith", homeAddress)

Explanation: Renaming the variable means later references to 'mailingLabel' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: How does EK9 measure cohesion and coupling?

How does EK9 detect tightly coupled modules?

Answer:
EK9 measures both cohesion and coupling at class and module level, using established software engineering metrics.

LACK OF COHESION - LCOM4 (E11014)
LCOM4 counts the number of connected components in a class. A connected component is a group of methods that share fields. A perfectly cohesive class has LCOM4 of 1 (all methods use overlapping fields). Higher values mean the class should be split. Thresholds: 8 for classes, 10 for components. Built-in types are excluded from this check.

EFFERENT COUPLING - Ce (E11015)
Efferent coupling counts how many other types a class depends on. High Ce means a class is tightly coupled to many other types and will break when any of them change. Thresholds: 8 for records, 12 for classes, 15 for components. Built-in types (String, Integer, List, etc.) are excluded because depending on standard library types is normal.

MODULE COUPLING (E11016)
Counts how many other modules a module depends on. High module coupling means changes ripple across module boundaries. Threshold: 10 external module dependencies.

MODULE COHESION (E11017)
Measures how related the contents of a module are. Low module cohesion suggests the module is a grab-bag of unrelated functionality and should be split.

CHIDAMBER AND KEMERER EVIDENCE
These metrics come from Chidamber and Kemerer (1994) research on object-oriented metrics. Their study of C++ projects showed that classes with high LCOM and high coupling had significantly more defects.

See Q311 for the full quality checks catalog. See Q312 for complexity metrics. See Q315 for inheritance depth.
See Q694 for named arguments pattern. See Q698 for excessive Boolean params.

defines module qa.codequality.cohesion

  defines class

    <?-
      A cohesive class where all methods share fields.
      LCOM4 is 1 because every method uses the shared state.
    -?>
    TemperatureConverter
      celsiusReading as Float: Float()

      TemperatureConverter()
        -> initialCelsius as Float
        this.celsiusReading :=? initialCelsius

      celsius() as pure
        <- rtn as Float: celsiusReading

      toFahrenheit() as pure
        <- rtn as Float: Float()

        conversionFactor <- 9.0 / 5.0
        freezingOffset <- 32.0
        rtn: celsiusReading * conversionFactor + freezingOffset

      toKelvin() as pure
        <- rtn as Float: Float()

        kelvinOffset <- 273.15
        rtn: celsiusReading + kelvinOffset

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

      operator $ as pure
        <- rtn as String: `${celsiusReading}C`

  defines program

    CohesionCouplingDemo()
      stdout <- Stdout()

      boilingPoint <- 100.0
      reading <- TemperatureConverter(boilingPoint)

      stdout.println(`Celsius: ${reading.celsius()}`)
      stdout.println(`Fahrenheit: ${reading.toFahrenheit()}`)
      stdout.println(`Kelvin: ${reading.toKelvin()}`)

Java: SonarQube measures LCOM4 and coupling but as informational metrics, not enforcement. JDepend measures package coupling but is a separate tool. Rust: no cohesion or coupling metrics built-in or in clippy. Go: no cohesion or coupling metrics. Python: pylint has no coupling metrics. C++: CppDepend measures metrics but is a commercial separate tool. EK9: LCOM4, efferent coupling, and module coupling are mandatory compiler errors with fixed thresholds.

E11014 E11015 E11016 LCOM4 chidamber clean-code cohesion component connected coupling efferent kemerer metric migrate module quality

Explanation: Renaming the variable means later references to 'reading' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Renaming the variable means later references to 'boilingPoint' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: Why does EK9 limit inheritance depth?

What is the maximum inheritance depth in EK9?

Why can I only extend 4 levels deep in EK9?

Answer:
EK9 limits inheritance depth to prevent the fragile base class problem and deep hierarchies that become impossible to reason about.

THRESHOLDS (E11019)
Class and trait: maximum 4 levels of inheritance.
Function: maximum 3 levels.
Record: maximum 2 levels.

Records have the strictest limit because they represent data. Data types rarely need deep hierarchies. Functions have a moderate limit. Classes and traits have the most generous limit, but still capped.

THE FRAGILE BASE CLASS PROBLEM
When a class inherits through many levels, changes to any ancestor can break descendants in unexpected ways. A change to a method in level 1 cascades through levels 2, 3, 4, and beyond. The developer modifying level 1 cannot predict the impact on level 5.

JAVA AWT/SWING CAUTIONARY TALE
Java AWT and Swing have inheritance depths of 6-8 levels. Component to Container to JComponent to JPanel to custom panels. This made Swing notoriously difficult to customise and debug. Any override had to account for behaviour at every level of the hierarchy.

USE DELEGATION INSTEAD
When you hit the inheritance depth limit, use the 'by' delegation keyword. Delegation composes behaviour without creating deep hierarchies. The delegating class controls exactly which methods are forwarded.

See Q109 for composition with delegation. See Q311 for the full quality checks catalog. See Q212 for composition over inheritance.

defines module qa.codequality.inheritance

  defines class

    <?-
      A shallow, well-structured hierarchy.
      Level 0: Shape (abstract base)
      Level 1: Polygon (concrete with delegation-ready design)
    -?>
    Shape as abstract
      shapeName as String: String()

      Shape()
        -> shapeName as String
        this.shapeName :=? shapeName

      name() as pure
        <- rtn as String: shapeName

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

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

      operator $ as pure
        <- rtn as String: shapeName

    <?-
      Level 1 of inheritance.
      Keeps the hierarchy shallow and focused.
    -?>
    Rectangle is Shape
      rectangleWidth as Float: Float()
      rectangleHeight as Float: Float()

      Rectangle()
        ->
          width as Float
          height as Float
        super("Rectangle")
        this.rectangleWidth :=? width
        this.rectangleHeight :=? height

      override area() as pure
        <- rtn as Float: rectangleWidth * rectangleHeight

      override operator ? as pure
        <- rtn as Boolean: rectangleWidth? and rectangleHeight?

      override operator $ as pure
        <- rtn as String: `Rectangle(${rectangleWidth} x ${rectangleHeight})`

  defines program

    InheritanceDepthDemo()
      stdout <- Stdout()

      width <- 5.0
      height <- 3.0
      rect <- Rectangle(width, height)

      stdout.println(`Shape: ${rect}`)
      stdout.println(`Area: ${rect.area()}`)

Java: no inheritance depth limit, AWT/Swing goes 6-8 deep, frameworks encourage deep hierarchies. Python: no inheritance depth limit, multiple inheritance can create diamond depth issues. C++: no inheritance depth limit, virtual inheritance adds complexity. Rust: no inheritance at all (trait composition only). Go: no inheritance (struct embedding only). Kotlin: no depth limit but sealed classes limit hierarchy breadth. EK9: hard limits (class 4, function 3, record 2) enforced by compiler.

AWT E11019 Swing base class clean-code delegation depth fragile hierarchy inheritance limit metric migrate quality

Explanation: Calling the pure area() method as a bare statement discards its Float return value, which EK9 forbids - capture or use the result. See ek9 -h E11051 for details.

unusedWidth <- 5.0
      rect <- Rectangle(5.0, 3.0)

Explanation: Declaring a variable that is never referenced is dead code. Every variable must be used. See ek9 -h E08090 for details.


Question: How does EK9 detect discarded return values?

What happens if I ignore a function return value in EK9?

Why does EK9 require capturing operator results?

Answer:
EK9 detects four categories of discarded return values and rejects code that ignores them.

DISCARDED CONSTRUCTOR (E11055)
A constructor creates an object. Calling a constructor as a bare statement and discarding the result is always either a bug (forgot the assignment) or a misuse of constructors. If you want side effects without creating an object, use a function. This was discovered by the EK9 fuzzer: removing an assignment from valid code creates dead constructor calls that crash later compilation phases.

COMPUTATIONAL OPERATORS (E11050)
Operators like +, -, *, / produce new values. Calling them without capturing the result is always a bug. The operator did work but the result was thrown away. Example: calling 'list + item' without assigning the new list means the addition had no effect.

PURE FUNCTION RETURNS (E11051)
A pure function has no side effects. If you call a pure function and discard the return value, the call had no observable effect. This is dead code. EK9 flags it because it usually means the developer forgot to capture the result or called the wrong function.

RESULT AND OPTIONAL (E11052)
Result and Optional return types MUST be checked. These types exist specifically to force the caller to handle success/failure or presence/absence. Discarding them defeats their entire purpose. This is equivalent to catching an exception and ignoring it.

RUST ANALOGY
Rust has the #[must_use] attribute that warns when a return value is ignored. EK9 goes further: it is automatic for all constructors, all pure functions, all computational operators, and all Result/Optional types. No annotation needed, no way to suppress.

See Q311 for the full quality checks catalog. See Q48 for Result type. See Q47 for Optional type. See Q50 for function return values. See Q641 for pure function return capture patterns.
See Q693 for captured operator returns. See Q696 for complexity within limits. See Q700 for parameter count limits.
See Q732 for discarded return boundary examples.

defines module qa.codequality.discarded

  defines function

    <?-
      Pure function: its return value MUST be captured.
      Calling this without capturing would trigger E11051.
    -?>
    doubleAmount() as pure
      -> amount as Integer
      <- rtn as Integer: amount * 2

    <?-
      Another pure function demonstrating the pattern.
    -?>
    formatCurrency() as pure
      -> amount as Float
      <- rtn as String: `\$${amount}`

  defines program

    DiscardedReturnsDemo()
      stdout <- Stdout()

      // Correct: capturing the return value
      originalAmount <- 50
      doubled <- doubleAmount(originalAmount)
      stdout.println(`Doubled: ${doubled}`)

      // Correct: using the return value in an expression
      price <- 29.99
      formatted <- formatCurrency(price)
      stdout.println(`Price: ${formatted}`)

      // Correct: using operator result
      items <- List() of String
      updatedItems <- items + "first"
      stdout.println(`Items: ${updatedItems.length()}`)

Java: return values can always be silently discarded, no compiler enforcement. Rust: #[must_use] attribute warns but can be suppressed with let _ = expr. Go: return values can be discarded with _ blank identifier. Python: return values always silently discarded. C++: [[nodiscard]] attribute warns but can be cast to void. Kotlin: return values silently discarded. Swift: @discardableResult annotation opts specific functions into allowing discard (default is warn), can be silenced with _ = expr. EK9: automatic must-use for all pure functions, computational operators, and Result/Optional, cannot be suppressed.

E11050 E11051 E11052 E11055 absent capture clean-code code constructor dead discard discardableResult error guard immutable metric migrate must_use ok operator optional pure quality result return safe side-effect swift

Explanation: Renaming the variable means later references to 'doubled' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Renaming the variable means later references to 'updatedItems' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: How does EK9 detect magic literals?

Why does EK9 reject literal values in comparisons?

How do I fix E11064 magic literal error?

Answer:
EK9 detects magic literals using a two-tier system. Magic literals are unnamed numeric or string values embedded directly in code. They obscure intent and make code harder to maintain.

TIER 1: COMPARISON LITERALS (E11064)
Any literal value used in a comparison is always flagged. Comparisons are decision points where the meaning of the value matters most. Instead of 'if age >= 18', use a named constant 'if age >= legalAdultAge'. This applies to ==, <>, <, >, <=, >=, and <=>, contains, and matches.

TIER 2: REPEATED LITERALS (E11065)
A literal that appears 3 or more times in the same file, or 4 or more times in the same module, is flagged. Repetition means the value has significance worth naming. Exempt from duplication checking: Boolean values, single characters, regular expressions, 0 and 1, and empty string.

CONSTANT ORGANISATION (E11066-E11067)
E11066: Constants should be defined in a dedicated constant block, not scattered through code.
E11067: Constant names should follow naming conventions (UPPER_CASE or descriptive camelCase).

EXEMPTIONS
The following contexts are exempt from magic literal detection: data definitions, fixture/test data, template output, and constant initialisation. This prevents false positives when defining legitimate data structures.

NAMED CONSTANTS PATTERN
Define meaningful names at the top of the function or in a constants block:
  maximumRetries <- 3
  timeoutSeconds <- 30
  minimumPasswordLength <- 8
The name documents WHY this value was chosen.

See Q311 for the full quality checks catalog. See Q140 for defining constants. See Q141 for constant immutability.

defines module qa.codequality.magic

  defines function

    <?-
      Demonstrates using named constants instead of magic literals.
      Every threshold has a meaningful name that documents its purpose.
    -?>
    classifyTemperature() as pure
      -> temperatureCelsius as Float
      <- classification as String: "moderate"

      freezingPoint <- 0.0
      coldThreshold <- 10.0
      hotThreshold <- 30.0

      if temperatureCelsius <= freezingPoint
        classification: "freezing"
      else if temperatureCelsius <= coldThreshold
        classification: "cold"
      else if temperatureCelsius >= hotThreshold
        classification: "hot"

    <?-
      Named constants make password validation self-documenting.
    -?>
    isPasswordStrong() as pure
      -> password as String
      <- rtn as Boolean: false

      minimumLength <- 8
      if length password >= minimumLength
        rtn: true

  defines program

    MagicLiteralsDemo()
      stdout <- Stdout()

      currentTemp <- 25.0
      weatherReport <- classifyTemperature(currentTemp)
      stdout.println(`Temperature ${currentTemp}C is ${weatherReport}`)

      testPassword <- "SecurePass123"
      strongEnough <- isPasswordStrong(testPassword)
      stdout.println(`Password strong: ${strongEnough}`)

Java: SonarQube has MagicNumber rule but optional and configurable. Checkstyle MagicNumber only catches numerics, not strings. Rust: clippy has no magic number detection. Go: no magic number detection. Python: pylint has magic-value-comparison but optional. C++: no standard magic number detection. EK9: two-tier magic literal detection (comparison always, duplication 3+/file), mandatory compiler error, covers both numbers and strings.

E11064 E11065 clean-code comparison constant literal magic metric named number quality repeated value

Explanation: Renaming the variable means later references to 'weatherReport' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Using a raw literal 8 in a comparison is a magic literal. Name it minimumLength to document why this value was chosen. See ek9 -h E11064 for details.


Question: How does EK9 detect self-assignment and self-comparison?

What is E08080 self-assignment in EK9?
How does EK9 catch copy-paste errors?
What does E08081 self-comparison mean?

Answer:
EK9 detects two categories of self-referential operations that are always programming errors.

SELF-ASSIGNMENT (E08080)
Assigning a variable to itself is always pointless. The value does not change. This typically happens from copy-paste errors where the developer duplicated a line and forgot to change one side. Example: 'name: name' assigns name to itself.

SELF-COMPARISON (E08081)
Comparing a variable to itself always produces a constant result. 'x == x' is always true. 'x <> x' is always false. 'x < x' is always false. These are never intentional and indicate a copy-paste error where the developer meant to compare two different variables.

FLAGGED OPERATORS
Self-comparison is detected for all comparison and containment operators: ==, <>, <, >, <=, >=, <=>, <~>, contains, and matches. Each of these produces a constant result when both sides are the same variable.

COPY-PASTE ERROR DETECTION
These checks exist specifically to catch copy-paste errors. When duplicating code, it is easy to write 'if price == price' instead of 'if price == discountedPrice'. Without this check, the bug silently evaluates to true and the discount logic is bypassed.

See Q311 for the full quality checks catalog. See Q239 for comparison operators. See Q637 for safe comparison patterns with distinct variable names.

defines module qa.codequality.selfops

  defines function

    <?-
      Correct comparisons use different variables on each side.
      The compiler would reject x == x or x: x.
    -?>
    findCheaper() as pure
      ->
        priceA as Float
        priceB as Float
      <-
        cheaperPrice as Float: priceA

      if priceB < priceA
        cheaperPrice: priceB

    compareNames() as pure
      ->
        firstName as String
        lastName as String
      <-
        sameIdentity as Boolean: firstName == lastName

  defines program

    SelfOperationsDemo()
      stdout <- Stdout()

      shopPrice <- 29.99
      onlinePrice <- 24.99
      bestPrice <- findCheaper(shopPrice, onlinePrice)
      stdout.println(`Best price: ${bestPrice}`)

      givenName <- "John"
      familyName <- "Smith"
      identicalName <- compareNames(givenName, familyName)
      stdout.println(`Same name: ${identicalName}`)

Java: SonarQube has SelfAssignment and SelfComparison rules but optional. SpotBugs detects SA_FIELD_SELF_ASSIGNMENT. Rust: clippy has eq_op lint for self-comparison (warn by default). Go: go vet detects self-assignment in some cases. Python: no self-comparison detection in standard tools. C++: compiler warnings for some self-comparisons with -Wall. EK9: mandatory compiler error for both self-assignment and self-comparison, covers all comparison operators.

E08080 E08081 assignment clean-code comparison constant copy error metric paste pointless quality self

Explanation: Comparing a variable to itself always produces a constant result (always false for <). This is a copy-paste error. See ek9 -h E08081 for details.

Explanation: Assigning a variable to itself is pointless and indicates a copy-paste error. See ek9 -h E08080 for details.


Question: How does EK9 detect unused parameters?

Why does EK9 flag unused function arguments?

How does EK9 detect unused closure captures?

Answer:
EK9 detects parameters and closure captures that are declared but never used in the function body.

UNUSED PARAMETERS (E08091)
If a function declares a parameter but never reads it, the parameter is dead code. It increases the function signature complexity without contributing to the result. This usually means the implementation is incomplete or the parameter was left over from a refactoring.

UNUSED CAPTURES (E11018)
Dynamic functions can capture variables from their enclosing scope. If a captured variable is never used inside the dynamic function, the capture is unnecessary. Unused captures waste memory and obscure the function's actual dependencies.

EXEMPT CONTEXTS
Some parameters are legitimately unused:
- Abstract methods: parameters define the contract but have no body.
- Override methods: the overriding method must match the parent signature even if it does not use all parameters.
- Dispatcher methods: the parameter type drives dispatch but may not be used in the base method.
- Operators: operator signatures are fixed by convention.
- Parameterised types: generic type parameters define the shape.

GO AND RUST ANALOGY
Go requires all variables to be used (unused variable is a compilation error). Rust warns on unused variables with _ prefix convention. EK9 follows Go's philosophy of treating unused code as an error, but is more nuanced with exemptions for abstract, override, and dispatcher contexts.

See Q311 for the full quality checks catalog. See Q49 for function basics. See Q53 for closure captures.

defines module qa.codequality.unused

  defines function

    <?-
      Every parameter is used in the function body.
      Removing any parameter would change the behaviour.
    -?>
    calculateArea() as pure
      ->
        width as Float
        height as Float
      <-
        rtn as Float: width * height

    <?-
      Both parameters contribute to the output.
    -?>
    greetPerson() as pure
      ->
        title as String
        surname as String
      <-
        greeting as String: `Hello, ${title} ${surname}`

  defines program

    UnusedParametersDemo()
      stdout <- Stdout()

      roomWidth <- 5.5
      roomHeight <- 3.2
      roomArea <- calculateArea(roomWidth, roomHeight)
      stdout.println(`Room area: ${roomArea} sq metres`)

      personalGreeting <- greetPerson("Dr", "Watson")
      stdout.println(personalGreeting)

Java: no unused parameter detection in the compiler, relies on IDE inspections or SonarQube. Rust: warns on unused variables, _ prefix suppresses. Go: unused variables are compilation errors, unused function parameters are not checked. Python: no unused parameter detection in standard tools. C++: -Wunused-parameter warns but can be suppressed. Kotlin: IDE inspection only, not a compilation error. EK9: unused parameters and captures are mandatory compiler errors with intelligent exemptions.

E08091 E11018 argument capture clean-code code dead incomplete metric migrate parameter quality refactor unused

roomAreaXYZ <- calculateArea(roomWidth, roomHeight)

Explanation: Renaming the variable means later references to 'roomArea' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: Why does EK9 report on readability?

What is the ARI readability score in EK9?

How does EK9 check identifier naming quality?

Answer:
EK9 calculates Automated Readability Index (ARI) scores for identifiers and reports them in quality dashboards. This is informational only, NOT a compilation error.

ARI SCORES
The ARI score estimates the reading level needed to understand an identifier. Scores range from 1 (simple, short names) to 20+ (very long, complex names). Lower scores mean more readable code. The score considers identifier length, word count (from camelCase splitting), and syllable complexity.

WHY INFORMATIONAL ONLY
Some domains require long identifiers. Finance has 'annualisedPercentageRate'. Chemistry has 'dihydrogenMonoxide'. Medical software has 'electrocardiogramResult'. Enforcing short names in these domains would reduce clarity, not improve it. So EK9 reports the score but does not reject high-scoring code.

WHERE SCORES APPEAR
ARI scores are visible in the -t6 HTML quality dashboard. Each function and class shows its average identifier readability. Module-level summaries highlight areas where naming could be simplified.

HELPS HUMANS AND AI
Readable identifiers benefit both human developers and AI assistants. For humans, clearer names reduce cognitive load. For AI, descriptive names mean fewer tokens needed to understand context, leading to more accurate code suggestions.

See Q311 for the full quality checks catalog. See Q290 for variable naming rules. See Q321 for the quality report dashboard.

defines module qa.codequality.readability

  defines function

    <?-
      Short, descriptive names score well on readability.
      'age' is better than 'currentAgeOfTheUserInYears'.
    -?>
    isAdult() as pure
      -> age as Integer
      <- rtn as Boolean: false

      legalAge <- 18
      if age >= legalAge
        rtn: true

    <?-
      Domain-specific names are acceptable even when longer.
      'interestRate' is clear in a financial context.
    -?>
    calculateInterest() as pure
      ->
        principal as Float
        interestRate as Float
        years as Integer
      <-
        totalInterest as Float: 0.0

      annualInterest <- principal * interestRate
      totalInterest: annualInterest * years

  defines program

    ReadabilityScoresDemo()
      stdout <- Stdout()

      customerAge <- 25
      adultStatus <- isAdult(customerAge)
      stdout.println(`Adult: ${adultStatus}`)

      loanAmount <- 10000.0
      annualRate <- 0.05
      loanYears <- 3
      interest <- calculateInterest(loanAmount, annualRate, loanYears)
      stdout.println(`Interest over ${loanYears} years: ${interest}`)

Java: no readability scoring in any standard tool. SonarQube measures cognitive complexity but not identifier readability. Rust: no readability scoring. Go: no readability scoring, though short names are a cultural convention. Python: no readability scoring, PEP 8 has length guidelines only. C++: no readability scoring. EK9: ARI readability scoring calculated automatically, shown in HTML dashboard, informational metric for continuous improvement.

ARI clean-code cognitive dashboard identifier informational metric naming quality readability readable score

Explanation: Using a raw literal 18 in a comparison is a magic literal. Extract it into a named constant that documents the intent. See ek9 -h E11064 for details.


Question: How do I see the code quality report?

How do I view complexity and coverage together?

Answer:
EK9 generates a comprehensive HTML quality dashboard using the -t6 flag. This combines test results, coverage data, and code quality metrics in a single page.

GENERATING THE REPORT
  ek9 -t6 myproject.ek9
This runs all tests, collects coverage, calculates quality metrics, and generates an HTML dashboard.

FOUR QUALITY DIALS
The dashboard header shows four gauges:
1. Average cyclomatic complexity across all functions.
2. Maximum cyclomatic complexity (the worst function).
3. Average cognitive complexity across all functions.
4. Maximum cognitive complexity (the worst function).
Green zone means healthy. Yellow means approaching thresholds. Red means at risk.

MODULE BREAKDOWN
Each module shows:
- Number of types, functions, and programs.
- Module coupling count (how many external modules referenced).
- Module cohesion score.
- Per-type LCOM4 and efferent coupling.

FUNCTION-LEVEL DETAIL
Each function in the source view shows complexity badges:
- Cyclomatic complexity number.
- Cognitive complexity number.
- Nesting depth indicator.
- Coverage percentage (if tests were run with coverage).

ADDING PROFILING (-t6p)
Append 'p' to include profiling data:
  ek9 -t6p myproject.ek9
This adds call counts and timing badges to each function in the source view.

COVERAGE AND QUALITY ON ONE PAGE
The -t6 report combines coverage heatmaps with quality metrics. You can see which functions are both poorly tested AND highly complex, the highest risk areas in your codebase.

See Q206 for test coverage. See Q157 for running tests. See Q312 for complexity metrics. See Q322 for profiling data. See Q207 for test output formats. See Q629 for reading flame graphs.

defines module qa.codequality.report

  defines function

    <?-
      Simple functions to demonstrate code that appears in a quality report.
      Each function would show its own complexity badge.
    -?>
    addTax() as pure
      ->
        netPrice as Float
        taxRate as Float
      <-
        grossPrice as Float: netPrice * (1.0 + taxRate)

    applyDiscount() as pure
      ->
        price as Float
        discountFraction as Float
      <-
        rtn as Float: price * (1.0 - discountFraction)

  defines program

    QualityReportDemo()
      stdout <- Stdout()

      basePrice <- 100.0
      standardTaxRate <- 0.20
      memberDiscount <- 0.10

      withTax <- addTax(basePrice, standardTaxRate)
      finalPrice <- applyDiscount(withTax, memberDiscount)

      stdout.println(`Base: ${basePrice}`)
      stdout.println(`With tax: ${withTax}`)
      stdout.println(`After discount: ${finalPrice}`)

      luxuryThreshold <- 500.0
      stdout.println(`Threshold: ${luxuryThreshold}`)
      isLuxury <- finalPrice > luxuryThreshold
      stdout.println(`Luxury: ${isLuxury}`)

Java: separate tools generate separate reports. JaCoCo for coverage HTML, SonarQube for quality dashboard, JMH for profiling. No single unified view. Python: coverage.py generates HTML, pylint generates text/JSON, no unified dashboard. Rust: tarpaulin for coverage, no quality dashboard. Go: go tool cover for coverage HTML, no quality dashboard. EK9: single -t6 flag generates unified HTML dashboard with coverage, complexity, cohesion, coupling, and profiling all on one page.

HTML badge clean-code complexity coverage dashboard dials metric module quality report t6

Explanation: Removing the variable declaration means later references to the variable become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Comparing against a raw literal 500.0 hides the intent behind a 'magic number' and triggers E11064. Extract it to a named constant (luxuryThreshold <- 500.0) so the threshold documents itself. See ek9 -h E11064 for details.


Question: How do I see how many times code was called and how long it took?

How do I profile EK9 code?
Does EK9 have a built-in profiler?
How do I identify slow code in EK9?

Answer:
EK9 has built-in profiling. Append 'p' to any test flag to enable profiling data collection.

ENABLING PROFILING
  ek9 -tp myproject.ek9     Human-readable profiling summary.
  ek9 -t2p myproject.ek9    JSON profiling data (for AI and CI tools).
  ek9 -t6p myproject.ek9    HTML dashboard with flame graph.

METRICS COLLECTED
For each function and method:
- Call count: how many times it was invoked.
- Total time: wall-clock time including callees.
- Self time: time spent in this function only, excluding callees.
- Average time per call.
- Minimum and maximum call times.
- Percentiles: p50, p95, p99 for latency distribution.

FLAME GRAPH (-t6p)
The HTML dashboard includes an interactive flame graph. Each frame represents a function. Width represents total time.
- Wide red frames: high self-time. These are the functions doing the most work. Optimise these first.
- Wide blue frames: orchestrators that call many other functions. High total time but low self-time. Optimising these means restructuring call patterns.
- Narrow frames: rarely called or fast functions. Usually not worth optimising.

HOT FUNCTION TABLE
Below the flame graph, a sorted table lists functions by self-time. The top entries are your optimisation targets. Each entry links to the source view.

JSON OUTPUT FOR CI
The -t2p flag produces JSON output suitable for automated analysis:
- CI pipelines can fail builds if p99 latency exceeds thresholds.
- AI assistants can read JSON profiling data to suggest optimisations.
- Trend analysis tools can track performance over time.

See Q321 for the quality report dashboard. See Q157 for running tests. See Q252 for compiler flags. See Q207 for test output formats. See Q246 for debugging strategies. See Q627 for profiling deep dive. See Q629 for reading flame graphs. See Q630 for identifying hot methods. See Q631 for benchmarking approaches.
See Q694 for named arguments pattern. See Q696 for complexity within limits.
See Q728 for nesting depth boundary. See Q733 for combined complexity boundary.

defines module qa.codequality.profiling

  defines function

    <?-
      Functions that would show different profiling characteristics.
      A frequently called helper vs a one-time orchestrator.
    -?>
    fibonacci() as pure
      -> position as Integer
      <- rtn as Integer: 0

      firstBase <- 0
      secondBase <- 1
      thresholdForRecursion <- 2
      if position == firstBase
        rtn: firstBase
      else if position == secondBase
        rtn: secondBase
      else if position >= thresholdForRecursion
        previousValue <- fibonacci(position - 1)
        beforePrevious <- fibonacci(position - 2)
        rtn: previousValue + beforePrevious

    formatResult() as pure
      ->
        label as String
        number as Integer
      <-
        rtn as String: `${label}: ${number}`

  defines program

    ProfilingDemo()
      stdout <- Stdout()

      targetPosition <- 10
      result <- fibonacci(targetPosition)
      output <- formatResult("Fibonacci", result)
      stdout.println(output)

Java: JMH for microbenchmarks, async-profiler or JFR for profiling (all external tools). Python: cProfile, line_profiler, py-spy (all external). Rust: perf, flamegraph crate, criterion for benchmarks (all external). Go: go tool pprof built-in with -cpuprofile flag. JavaScript: Chrome DevTools profiler. EK9: append 'p' to any test flag for built-in profiling, flame graph in HTML dashboard, JSON output for AI/CI integration.

bottleneck call clean-code count flame graph metric p95 p99 performance profile profiling quality self time total

if position == 0
        rtn: 0
      else if position == 1
        rtn: 1

Explanation: Using raw literals 0, 1, 2 in comparisons is a magic literal error. Extract them into named constants like firstBase, secondBase, and thresholdForRecursion. See ek9 -h E50001 for details.


Question: Can AI help me improve code quality?

What EK9 commands help AI understand my code?

How does EK9 support AI-assisted development?

Answer:
EK9 is designed from the start for AI-assisted development. Multiple commands produce machine-readable output that AI tools can consume directly.

STEP 1: RICH ERROR MESSAGES (-E3)
  ek9 -c -E3 mycode.ek9
The -E3 flag produces detailed error explanations with diagnosis, rationale, and fix examples. AI assistants can read these to understand exactly what went wrong and generate correct fixes.

STEP 2: JSON PROFILING AND COVERAGE (-t2p)
  ek9 -t2p mycode.ek9
JSON output with profiling data, test results, and coverage. AI tools can parse this to identify undertested code, performance hotspots, and quality trends.

STEP 3: REFERENCE DATA (forAI.json)
The forAI.json file contains the complete EK9 knowledge base in machine-readable format. AI assistants can load this as context to understand EK9 syntax, operators, and patterns without guessing.

STEP 4: KNOWLEDGE SEARCH (-q)
  ek9 -q "how to handle errors"
Search the Q&A knowledge base from the command line. AI tools can use this to find correct patterns for specific problems instead of hallucinating incorrect syntax.

STEP 5: TRAINING DATA DUMP (-Q)
  ek9 -Q
Dumps the entire Q&A knowledge base in JSONL training format. This can fine-tune local AI models on EK9 patterns.

THE AI QUALITY WORKFLOW
1. Write code (or have AI generate it).
2. Compile with -E3 for rich feedback.
3. Fix errors using Q&A search (-q) for correct patterns.
4. Run tests with -t2p for JSON quality data.
5. Feed quality data back to AI for continuous improvement.

EK9 DESIGNED FOR AI
The compiler error messages are specifically designed as AI control signals. Each error produces a cascade that guides AI toward correct patterns. When the compiler is silent, the code has converged across all quality dimensions.

See Q281 for verifying AI-generated code. See Q252 for all compiler flags. See Q311 for quality checks catalog. See Q321 for the quality report.

See Q340 for AI-friendly transaction patterns.

defines module qa.codequality.ai

  defines function

    <?-
      A function that demonstrates clean, AI-friendly code.
      Clear names, simple structure, obvious purpose.
    -?>
    categoriseResponse() as pure
      -> statusCode as Integer
      <- category as String: "unknown"

      successLower <- 200
      successUpper <- 299
      clientErrorLower <- 400
      clientErrorUpper <- 499
      serverErrorLower <- 500
      serverErrorUpper <- 599

      if statusCode >= successLower and statusCode <= successUpper
        category: "success"
      else if statusCode >= clientErrorLower and statusCode <= clientErrorUpper
        category: "client error"
      else if statusCode >= serverErrorLower and statusCode <= serverErrorUpper
        category: "server error"

  defines program

    AiCodeQualityDemo()
      stdout <- Stdout()

      okStatus <- 200
      notFoundStatus <- 404

      okCategory <- categoriseResponse(okStatus)
      errorCategory <- categoriseResponse(notFoundStatus)

      stdout.println(`Status ${okStatus}: ${okCategory}`)
      stdout.println(`Status ${notFoundStatus}: ${errorCategory}`)

Java: SonarQube has API output but requires separate server. IDE plugins provide some AI integration. No unified CLI workflow. Python: pylint has JSON output, coverage has JSON, but no unified AI workflow. Rust: clippy has JSON output with --message-format=json. No knowledge base. Go: go vet has JSON output. No integrated knowledge base. EK9: purpose-built for AI with -E3 rich errors, -t2p JSON quality data, forAI.json reference, -q knowledge search, -Q training dump, all from a single CLI.

E3 ai assistant clean-code forAI json knowledge llm machine metric quality readable training workflow

Explanation: Renaming the variable means later references to 'okCategory' become unresolved, triggering E50001. See ek9 -h E50001 for details.

Explanation: Renaming the variable means later references to 'errorCategory' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: What is the EK9 equivalent of Spring @Autowired?

How do I inject dependencies in EK9 like Spring @Autowired?

How do I wire dependencies without @Autowired?

Answer:
EK9 replaces Spring's @Autowired with the injection suffix '!' on field declarations. Any field ending with '!' is an injection point that the compiler validates at compile time.

SPRING @AUTOWIRED VS EK9 '!'
In Spring, @Autowired marks a field for runtime injection:
  @Autowired private UserService userService;
In EK9, the '!' suffix marks a field for compile-time validated injection:
  userService as UserService!
The '!' suffix is shorter, more visible, and validated before any code runs.

COMPILE-TIME GUARANTEE
Spring discovers missing @Autowired bindings at runtime (NoSuchBeanDefinitionException, sometimes minutes into startup). EK9 discovers missing injection bindings during compilation. If your program compiles, every injection point is satisfied.

FIELD INJECTION ONLY
EK9 supports field injection (the '!' suffix). There is no constructor injection or method injection — fields with '!' are populated during the application's prepare phase before the program body executes.

WHY NO CONSTRUCTOR INJECTION
Spring advocates constructor injection for immutability and testability. EK9 achieves both through different mechanisms: compile-time validation ensures completeness, and the singleton lifecycle ensures stability. Constructor injection adds complexity without additional safety in EK9's model.

See Q227 for compile-time DI validation. See Q228 for registration ordering. See Q231 for program-application linking. See Q331 for testing injected components.

See Q667 for abstract injection requirement. See Q669 for injection reassignment.

defines module qa.di.spring.autowired

  defines component

    UserRepository as abstract
      findUser() as abstract
        -> userId as String
        <- userName as String?

      default operator ?

    InMemoryUserRepository is UserRepository
      override findUser()
        -> userId as String
        <- userName as String: "User-" + userId

      default operator ?

    <?-
      UserService uses '!' to mark injection point.
      This is the EK9 equivalent of @Autowired.
    -?>
    UserService as abstract
      getUser() as abstract
        -> userId as String
        <- greeting as String?

      default operator ?

    DefaultUserService is UserService
      repo as UserRepository!

      override getUser()
        -> userId as String
        <- greeting <- String()
        userName <- repo.findUser(userId)
        greeting: "Hello, " + userName

      default operator ?

  defines application

    AutowiredEquivalentApp
      register InMemoryUserRepository() as UserRepository
      register DefaultUserService() as UserService

  defines program

    AutowiredDemo() with application of AutowiredEquivalentApp
      stdout <- Stdout()

      // === '!' SUFFIX IS THE EK9 EQUIVALENT OF @Autowired ===

      service as UserService!

      result <- service.getUser("42")
      stdout.println(result)

      stdout.println("Injection validated at compile time, not runtime")

Java Spring: @Autowired on fields, constructors, or setters. Runtime injection via reflection. Missing beans throw NoSuchBeanDefinitionException at startup. Constructor injection recommended for immutability. Guice: @Inject annotation, similar to @Autowired. .NET: constructor injection via IServiceProvider. Python: no built-in DI. Go: no built-in DI. EK9: '!' suffix on field declarations, compile-time validation, zero runtime injection failures.

autowired binding dependency exclamation field inject migration spring suffix wiring

Explanation: Injection fields must declare abstract component types. The '!' suffix marks a field for injection, but the type must be the abstract contract, not the concrete implementation. See ek9 -h E08150 for details.

Explanation: Removing the UserRepository registration leaves DefaultUserService's injection field 'repo as UserRepository!' unsatisfied. Every injection field must have a matching registration. See ek9 -h E08210 for details.


Question: What is the EK9 equivalent of Spring @Component and @Service?

How do I define injectable components in EK9?

What replaces @Component and @Service annotations in EK9?

Answer:
EK9 replaces Spring's @Component and @Service with the 'defines component' construct. Components in EK9 are types that can be registered in applications and injected into programs.

SPRING ANNOTATIONS VS EK9 CONSTRUCTS
Spring uses @Component/@Service annotations on classes that are auto-scanned:
  @Service public class OrderService { ... }
EK9 uses the 'defines component' section to declare injectable types:
  defines component
    OrderService as abstract ...
    DefaultOrderService is OrderService ...

NO AUTO-SCANNING
Spring scans the classpath for annotated classes. EK9 requires explicit registration in the application definition. This is intentional: explicit registration is auditable, predictable, and compile-time verified.

ABSTRACT + CONCRETE PATTERN
EK9 components follow the abstract/concrete pattern: define an abstract component (the contract), then implement concrete components. Register the concrete as the abstract in the application. Programs inject the abstract type.

WHY NO @SERVICE/@REPOSITORY DISTINCTION
Spring has @Component, @Service, @Repository, and @Controller. These are functionally identical (all are component stereotypes). EK9 has one construct: component. Semantic distinctions belong in naming, not annotations.

See Q111 for component basics. See Q227 for compile-time validation. See Q324 for @Autowired equivalent. See Q326 for @Bean/@Configuration equivalent.

defines module qa.di.spring.component

  defines component

    <?-
      Abstract component is the contract (like Spring interface).
    -?>
    NotificationService as abstract
      notify() as abstract
        -> message as String
        <- confirmation as String?

      default operator ?

    <?-
      Concrete component is the implementation (like @Service class).
    -?>
    EmailNotifier is NotificationService
      override notify()
        -> message as String
        <- confirmation as String: "Email sent: " + message

      default operator ?

    AuditService as abstract
      record() as abstract
        -> action as String
        <- entry as String?

      default operator ?

    SimpleAuditService is AuditService
      notifier as NotificationService!

      override record()
        -> action as String
        <- entry <- String()
        entry: notifier.notify("Audit: " + action)

      default operator ?

  defines application

    ComponentApp
      register EmailNotifier() as NotificationService
      register SimpleAuditService() as AuditService

  defines program

    ComponentServiceDemo() with application of ComponentApp
      stdout <- Stdout()

      // === COMPONENTS REPLACE @Component AND @Service ===

      audit as AuditService!

      result <- audit.record("user.login")
      stdout.println(result)

      stdout.println("Components defined in 'defines component' section")

Java Spring: @Component, @Service, @Repository, @Controller stereotypes with classpath scanning. Guice: @Provides methods in Module classes. .NET: services.AddTransient/AddScoped/AddSingleton in ConfigureServices. Python: no language-level component declaration. Go: no language-level DI. EK9: 'defines component' section with abstract/concrete pattern, explicit registration in application, no classpath scanning.

annotation component construct define dependency inject injectable migration register service spring stereotype

Explanation: Injection fields must declare the abstract component type. The concrete EmailNotifier is registered against the abstract NotificationService in the application, not injected directly. See ek9 -h E08150 for details.

register SimpleAuditService() as AuditService
      register EmailNotifier() as NotificationService

Explanation: SimpleAuditService injects NotificationService, so NotificationService must be registered before SimpleAuditService. Reversing the order causes a missing dependency at that point in the registration sequence. See ek9 -h E08200 for details.

Explanation: Renaming the variable means later references to 'result' become unresolved, triggering E50001. See ek9 -h E50001 for details.


Question: What is the EK9 equivalent of Spring @Bean and @Configuration?

How do I configure bean creation in EK9?

What replaces @Configuration classes in EK9?

How do I register third-party types for injection in EK9?

Answer:
EK9 replaces Spring's @Configuration classes and @Bean methods with the 'defines application' construct. The application definition is where you register concrete implementations against abstract types.

SPRING @BEAN VS EK9 APPLICATION REGISTRATION
Spring uses @Configuration with @Bean methods:
  @Configuration
  class AppConfig {
    @Bean DataSource dataSource() { return new HikariDataSource(); }
  }
EK9 uses explicit registration in the application:
  defines application
    MyApp
      register HikariDataSource() as DataSource

EXPLICIT CONSTRUCTION
In Spring, @Bean methods can contain arbitrary construction logic. In EK9, the register statement calls the component's constructor directly. Complex construction logic belongs in the component's constructor, not in configuration.

ORDERING IS EXPLICIT
Spring resolves bean creation order automatically (and sometimes gets it wrong). EK9 requires registration in dependency order: dependencies before dependents. The compiler validates this ordering at compile time (see Q228).

NO CONDITIONAL BEANS
Spring has @Conditional, @Profile, @ConditionalOnProperty for conditional bean creation. EK9 does not support conditional registration. If you need different configurations, define different applications. This eliminates an entire class of 'hidden missing bean' bugs.

See Q227 for compile-time validation. See Q228 for registration ordering. See Q324 for @Autowired equivalent. See Q325 for @Component equivalent.

defines module qa.di.spring.bean.config

  defines component

    CacheService as abstract
      lookup() as abstract
        -> key as String
        <- hit as String?

      default operator ?

    InMemoryCache is CacheService
      override lookup()
        -> key as String
        <- hit as String: "cached:" + key

      default operator ?

    DataService as abstract
      fetch() as abstract
        -> query as String
        <- result as String?

      default operator ?

    CachedDataService is DataService
      cache as CacheService!

      override fetch()
        -> query as String
        <- result <- String()
        result: cache.lookup(query)

      default operator ?

  defines application

    <?-
      This application definition replaces @Configuration class.
      Each register statement replaces a @Bean method.
    -?>
    BeanConfigApp
      register InMemoryCache() as CacheService
      register CachedDataService() as DataService

  defines program

    BeanConfigDemo() with application of BeanConfigApp
      stdout <- Stdout()

      // === APPLICATION DEFINITION REPLACES @Configuration ===

      dataService as DataService!

      result <- dataService.fetch("users")
      stdout.println(result)

      stdout.println("Application construct replaces @Configuration + @Bean")

Java Spring: @Configuration + @Bean methods, auto-wiring, @Conditional/@Profile for conditional beans, FactoryBean for complex creation. Guice: Module.configure() with bind() statements. .NET: ConfigureServices with AddTransient/AddScoped/AddSingleton. Python: manual construction or DI frameworks. Go: wire for compile-time DI, manual construction. EK9: 'defines application' with explicit 'register' statements, dependency ordering enforced by compiler, no conditional registration.

application bean conditional configuration create dependency explicit factory inject migration register spring

register CachedDataService() as DataService
      register InMemoryCache() as CacheService

Explanation: CachedDataService injects CacheService, so CacheService must be registered first. Unlike Spring which resolves order automatically, EK9 requires explicit dependency-first ordering. See ek9 -h E08200 for details.

Explanation: Injection fields must use the abstract type. The application maps concrete to abstract via 'register X() as Y', and fields inject the abstract Y. See ek9 -h E08150 for details.


Question: How does EK9 DI compare to Spring Boot auto-configuration?

Does EK9 have auto-configuration like Spring Boot?

How does EK9 handle what Spring Boot starters provide?

What is different between Spring Boot and EK9 dependency injection?

Answer:
Spring Boot's auto-configuration automatically creates beans based on classpath contents and properties. EK9 deliberately rejects auto-configuration in favor of explicit registration.

SPRING BOOT AUTO-CONFIGURATION
Spring Boot scans for @AutoConfiguration classes, evaluates @Conditional annotations, and creates beans based on what's on the classpath. A single dependency like spring-boot-starter-web auto-configures an embedded Tomcat, Jackson, DispatcherServlet, and dozens of other beans.

WHY EK9 REJECTS AUTO-CONFIGURATION
1. INVISIBLE BEHAVIOUR: Auto-configured beans appear from nowhere. Developers cannot tell which beans exist without running the application and inspecting the context.
2. FRAGILE ORDERING: Auto-configuration order depends on classpath scanning, which can change between builds. This causes intermittent failures.
3. CONDITIONAL COMPLEXITY: @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty create a combinatorial explosion of possible configurations.
4. DEBUGGING NIGHTMARE: When auto-configuration goes wrong, developers must trace through AbstractAutowireCapableBeanFactory and dozens of post-processors.

EK9'S EXPLICIT APPROACH
In EK9, every component registration is visible in the application definition. There is no magic. If you can read the 'defines application' section, you know exactly what components exist.

BENEFITS OF EXPLICITNESS
1. AUDITABLE: Security teams can review exactly what's registered
2. PREDICTABLE: Same registration, same behavior, every time
3. COMPILE-TIME VERIFIED: Compiler checks completeness and ordering
4. AI-FRIENDLY: AI can read and generate correct registrations

See Q227 for compile-time validation. See Q326 for @Bean equivalent. See Q332 for runtime overhead comparison.

defines module qa.di.spring.boot.comparison

  defines component

    Logger as abstract
      log() as abstract
        -> message as String
        <- output as String?

      default operator ?

    ConsoleLogger is Logger
      override log()
        -> message as String
        <- output as String: "[LOG] " + message

      default operator ?

    Repository as abstract
      find() as abstract
        -> identifier as String
        <- record as String?

      default operator ?

    SimpleRepository is Repository
      logger as Logger!

      override find()
        -> identifier as String
        <- record <- String()
        record: logger.log("Found: " + identifier)

      default operator ?

    AppController as abstract
      handle() as abstract
        -> request as String
        <- response as String?

      default operator ?

    MainController is AppController
      repo as Repository!

      override handle()
        -> request as String
        <- response <- String()
        response: repo.find(request)

      default operator ?

  defines application

    <?-
      Every registration is explicit and visible.
      No auto-configuration, no classpath magic.
    -?>
    ExplicitApp
      register ConsoleLogger() as Logger
      register SimpleRepository() as Repository
      register MainController() as AppController

  defines program

    SpringBootComparisonDemo() with application of ExplicitApp
      stdout <- Stdout()

      // === EXPLICIT REGISTRATION VS AUTO-CONFIGURATION ===

      controller as AppController!

      result <- controller.handle("user-123")
      stdout.println(result)

      stdout.println("Every component explicitly registered and verified")

Java Spring Boot: @SpringBootApplication triggers auto-configuration, starters provide pre-configured beans, @Conditional for conditional creation, spring.factories/META-INF for discovery. Quarkus: build-time configuration similar to EK9 philosophy. Micronaut: compile-time DI, closer to EK9 approach. .NET: Host.CreateDefaultBuilder with auto-configuration. EK9: explicit registration only, no auto-configuration, no classpath scanning, compile-time validation.

auto
classpath
configuration
dependency
explicit
inject
invisible
migration
registration
scanning
spring boot
starter

register MainController() as AppController
      register SimpleRepository() as Repository
      register ConsoleLogger() as Logger

Explanation: Unlike Spring Boot auto-configuration which resolves order automatically, EK9 requires dependencies registered before dependents. MainController depends on Repository which depends on Logger, so Logger must come first. See ek9 -h E08200 for details.