Question: What are the basic rules for EK9 service definitions?

What triggers E07240 SERVICE_METHOD_NEEDS_BODY?

What triggers E07670 SERVICE_OPERATOR_NOT_SUPPORTED?

What triggers E07740 SERVICE_NON_WEB_METHOD_NOT_ALLOWED?

What operators can services use for CRUD?

Answer:
EK9 services enforce three fundamental rules:

1. CONCRETE BODIES REQUIRED (E07240)
Every service method must have a concrete implementation body. Abstract methods cannot serve HTTP requests.

2. SUPPORTED OPERATORS ONLY (E07670)
Services support specific operators for CRUD patterns:
  += for POST (create)
  -= for DELETE
  :~: for PATCH (merge)
  :^: for PUT (replace)
Other operators like ==, <, + are not valid on services.

3. NO PROTECTED NON-WEB METHODS (E07740)
All service methods must be HTTP-bound (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS). Private helper methods and protected non-web methods are not allowed because services expose only HTTP endpoints.

See Q657 for URI mapping. See Q659 for HTTPResponse. See Q660 for CRUD operators. See Q685 for method bodies.

defines module qa.services.basics.overview

  defines class

    <?-
      Models an HTTP response with status and content.
      Demonstrates the pattern services use for endpoint handlers.
    -?>
    HealthResponse
      statusCode as Integer: 200
      responseBody as String: `{"status": "ok"}`
      responseType as String: "application/json"

      HealthResponse()
        ->
          givenStatus as Integer
          givenBody as String
        this.statusCode :=: givenStatus
        this.responseBody :=: givenBody

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

      responseBody()
        <- rtn as String: String(responseBody)

      responseType()
        <- rtn as String: String(responseType)

      operator $ as pure
        <- rtn as String: `HealthResponse[${statusCode}]`
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

    <?-
      Simulates a service endpoint handler.
      In real EK9 services, methods must have concrete bodies (E07240).
    -?>
    HealthHandler
      endpoint as String: "/api/health"

      handleGet()
        <- rtn as HealthResponse: HealthResponse(200, `{"status": "ok"}`)

      endpoint()
        <- rtn as String: String(endpoint)

      operator $ as pure
        <- rtn as String: `Handler[${endpoint}]`
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

  defines program

    ServiceBasicsDemo()
      stdout <- Stdout()
      handler <- HealthHandler()
      healthResult <- handler.handleGet()

      stdout.println("Service basics:")
      stdout.println("  Methods must have concrete bodies")
      stdout.println("  Only CRUD operators: +=, -=, :~:, :^:")
      stdout.println("  All methods must be HTTP-bound")
      stdout.println($healthResult)

Java Spring: controllers can have private helper methods. Python Flask: handler functions are plain Python. Go: handlers are plain functions. EK9: services are strictly HTTP-endpoint containers with enforced method rules.

CRUD E07240 E07670 E07740 basics body method operator service web


Question: How do HTTP verbs, URI path variables, and parameter qualifiers work in EK9 services?

What triggers E07680 SERVICE_HTTP_VERB_REQUIRED?

What triggers E07690 SERVICE_URI_PATH_REQUIRED?

What triggers E07710 SERVICE_PARAM_NEEDS_QUALIFIER?

What triggers E07720 SERVICE_DUPLICATE_QUALIFIER?

How do I bind path variables to parameters?

Answer:
EK9 service methods require explicit HTTP configuration:

1. HTTP VERB REQUIRED (E07680)
Every method needs an HTTP verb: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.

2. URI PATH REQUIRED (E07690)
Methods must specify a URI path after 'for :/path'.

3. PARAMETER QUALIFIERS (E07710)
Parameters in service methods need qualifiers like PATH, QUERY, HEADER, COOKIE, or REQUEST. Path variables are bound automatically from URI segments.

4. NO DUPLICATE QUALIFIERS (E07720)
Each parameter qualifier can only be used once per method.

See Q657 for URI mapping. See Q846 for parameter types. See Q868 for path parameter types.

defines module qa.services.uripath.params

  defines constant

    JSON_TYPE <- "application/json"

  defines class

    <?-
      Models a user lookup response.
      Demonstrates how path variables bind to parameters in services.
    -?>
    UserResponse
      userId as Integer: 0
      contentKind as String: JSON_TYPE

      UserResponse()
        -> givenUserId as Integer
        this.userId :=: givenUserId

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

      contentKind()
        <- rtn as String: String(contentKind)

      toJson()
        <- rtn as String: `{"userId": ${userId}}`

      operator $ as pure
        <- rtn as String: `UserResponse[${userId}]`
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

    <?-
      Models a user list response.
    -?>
    UserListResponse
      userCount as Integer: 0

      UserListResponse()
        -> givenCount as Integer
        this.userCount :=: givenCount

      toJson()
        <- rtn as String: `{"users": [], "count": ${userCount}}`

      operator $ as pure
        <- rtn as String: `UserListResponse[${userCount}]`
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

    <?-
      Simulates a user service with path parameter binding.
      In real services, GET /users/{userId} binds userId from the URI.
    -?>
    UserEndpoint

      findUser()
        -> userId as Integer
        <- rtn as UserResponse: UserResponse(userId)

      listUsers()
        <- rtn as UserListResponse: UserListResponse(0)

      operator $ as pure
        <- rtn as String: "UserEndpoint"
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

  defines program

    ServiceUriParamsDemo()
      stdout <- Stdout()
      endpoint <- UserEndpoint()
      userResult <- endpoint.findUser(42)
      listResult <- endpoint.listUsers()

      stdout.println("Service URI parameters:")
      stdout.println("  GET /users/{userId} -> path variable binding")
      stdout.println("  GET /users/list -> no parameters")
      stdout.println(userResult.toJson())
      stdout.println(listResult.toJson())

Java Spring: @GetMapping, @PathVariable, @RequestParam. Python Flask: @app.route with <param>. Go: mux.Vars(r) for path variables. EK9: HTTP verb and URI path required in method signature, parameters bound from path segments.


Question: How must path variable counts match parameters in EK9 services?

What triggers E07730 SERVICE_PATH_PARAM_COUNT_MISMATCH?

What triggers E07770 SERVICE_HTTPREQUEST_ONLY_WITH_REQUEST?

What triggers E07780 SERVICE_HTTPREQUEST_AT_MOST_ONE?

How do I use HTTPRequest in a service method?

Answer:
EK9 validates service method signatures for path consistency:

1. PATH PARAM COUNT MUST MATCH (E07730)
The number of {variables} in the URI must equal the number of parameters declared on the method.
  findItem() as GET for :/{catId}/{itemId}
    -> catId as Integer
    -> itemId as Integer
Two path variables, two parameters — correct.

2. HTTPREQUEST RULES (E07770, E07780)
HTTPRequest can only be used with :=: REQUEST binding, not as a path/query parameter. At most one HTTPRequest parameter per method.

See Q657 for URI mapping. See Q846 for parameter type rules. See Q868 for path parameter types.

defines module qa.services.path.matching

  defines constant

    JSON_CONTENT <- "application/json"

  defines class

    <?-
      Models a catalog item found by category and item identifiers.
      Demonstrates how multi-segment path variables map to parameters.
    -?>
    CatalogItem
      categoryId as Integer: 0
      itemId as Integer: 0
      contentKind as String: JSON_CONTENT

      CatalogItem()
        ->
          givenCategoryId as Integer
          givenItemId as Integer
        this.categoryId :=: givenCategoryId
        this.itemId :=: givenItemId

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

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

      contentKind()
        <- rtn as String: String(contentKind)

      toJson()
        <- rtn as String: `{"cat": ${categoryId}, "item": ${itemId}}`

      operator $ as pure
        <- rtn as String: `CatalogItem[${categoryId}, ${itemId}]`
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

    <?-
      Simulates a catalog endpoint with multi-segment path binding.
      In real services: GET /catalog/{catId}/{itemId} requires two parameters.
      The compiler validates path variable count matches parameter count (E07730).
    -?>
    CatalogEndpoint

      findItem()
        ->
          catId as Integer
          itemId as Integer
        <- rtn as CatalogItem: CatalogItem(catId, itemId)

      operator $ as pure
        <- rtn as String: "CatalogEndpoint"
      operator #^ as pure
        <- rtn as String: $this
      default operator ?

  defines program

    ServicePathMatchingDemo()
      stdout <- Stdout()
      endpoint <- CatalogEndpoint()
      catalogResult <- endpoint.findItem(10, 25)

      stdout.println("Path matching rules:")
      stdout.println("  URI variables must match parameter count")
      stdout.println("  GET /catalog/{catId}/{itemId} -> 2 params")
      stdout.println(catalogResult.toJson())

Java Spring: @PathVariable count must match. Python Flask: <param> count must match function args. Go: manual extraction from URL. EK9: compile-time validation that URI variable count matches parameter count.

E07730 E07770 E07780 HTTPRequest count match parameter path service variable


Question: Which types are allowed for typed program arguments in EK9?

What triggers E07600 PROGRAM_ARGUMENT_TYPE_NOT_SUPPORTED?

Why can't I use custom types as program arguments?

What types can a program accept on the command line?

Answer:
Program arguments support: String, Integer, Float, Boolean, Character. NOT custom types.

EK9 programs with typed parameters (style 2) can only use types that the runtime can parse from command-line strings. Custom classes, records, and complex types cannot be automatically parsed.

SUPPORTED TYPES
String, Integer, Float, Boolean, Date, DateTime, Duration, Millisecond, Colour, Dimension, Money, Path, RegularExpression, Character.

UNSUPPORTED (E07600)
Custom classes, records, traits, enumerations, List, Dict, Optional, or any user-defined type.

WORKAROUND
Use String parameters and parse manually:
  MyProgram()
    -> configPath as String
    // Parse configPath into your custom type

See Q889 for program argument styles. See Q888 for program return type.

defines module qa.syntax.programargtypes

  defines program

    <?-
      Program with typed arguments.
      Only built-in parseable types are allowed.
    -?>
    ProgramArgumentTypesDemo()
      ->
        name as String
        age as Integer
        verbose as Boolean

      stdout <- Stdout()
      stdout.println(`Name: ${name}`)
      stdout.println(`Age: ${age}`)
      stdout.println(`Verbose: ${verbose}`)

Java: main(String[]) only. Python: sys.argv strings. Go: os.Args strings. Rust: std::env::args strings. EK9: compiler-parsed typed parameters from built-in parseable types only.

Boolean E07600 Float Integer String argument custom program supported type

Explanation: Program arguments are limited to a finite set of parseable built-in types; GUID (like any custom or unsupported type) is rejected with E07600 - use String and parse manually. See ek9 -h E07600 for details.


Question: Why must generic types always be parameterized when used?

What triggers E04080 PARAMETERIZATION_REQUIRED?

Why can't I use bare List without 'of String'?

How do I correctly use Dict with type parameters?

Answer:
List needs 'of String', Dict needs 'of (String, Integer)'. Can't use bare generic name.

EK9 requires all generic types to be fully parameterized when used. A bare generic name like 'List' or 'Dict' without type arguments triggers E04080.

CORRECT USAGE
  names <- List() of String
  mapping <- Dict() of (String, Integer)
  maybe <- Optional() of Float

INCORRECT USAGE (E04080)
  names <- List()          // ERROR: needs 'of String'
  mapping <- Dict()        // ERROR: needs 'of (K, V)'

WHY REQUIRED
Without type parameters, the compiler cannot verify type safety of elements added to or retrieved from the collection. Unlike Java's raw types, EK9 never allows unparameterized generics.

See Q654 for parameter count validation. See Q196 for multi-parameter generics. See Q642 for constructor inference.

defines module qa.generics.paramsrequired

  defines program

    GenericParamsRequiredDemo()
      stdout <- Stdout()

      //Correctly parameterized generics
      names <- List() of String
      names += "Alice"
      names += "Bob"
      stdout.println(`Names count: ${length names}`)

      ages <- Dict() of (String, Integer)
      stdout.println(`Dict set: ${ages?}`)

      maybe <- Optional() of Float
      stdout.println(`Optional set: ${maybe?}`)

      //Constructor inference also works
      inferred <- List("one")
      stdout.println(`Inferred list count: ${length inferred}`)

Java: raw types allowed but deprecated (List vs List<String>). Python: untyped collections common. Go: generics always parameterized (Go 1.18+). Rust: generics always parameterized. EK9: E04080 enforces parameterization, no raw types ever.

Dict E04080 List generic parameter parameterization raw required type

Explanation: Generic types must be parameterized. List requires 'of String' (or another type). Using a bare generic name without type parameters triggers E06010. See ek9 -h E06010 for details.


Question: What are the rules for using 'default operator' in EK9?

What triggers E07190 DEFAULT_OPERATOR_IN_EXTENDING_CLASS?

What triggers E07200 PROPERTY_TYPE_MISSING_OPERATOR?

Can I use default operator in a class that extends another?

Why do my properties need operators for default to work?

Answer:
default operator rules: can't use in extending classes if parent has it. Property types must support required operators.

1. EXTENDING CLASS RESTRICTION (E07190)
If a parent class already uses 'default operator' for a given operator, the extending class cannot also use 'default operator' for the same operator. Override it explicitly instead.

2. PROPERTY TYPE REQUIREMENTS (E07200)
When you use 'default operator ==', all property types in the class must support ==. If any property's type lacks ==, compilation fails with E07200.

DEFAULT OPERATOR BEHAVIOR
'default operator' generates field-by-field implementations:
  default operator == compares all fields
  default operator $ concatenates field string representations
  default operator ? checks all fields are set

See Q116 for default operator syntax. See Q245 for custom operators.

defines module qa.operators.defaultrules

  defines class

    <?-
      Class using default operators.
      All property types (String, Integer) support ==, $, ?.
    -?>
    Person
      name <- String()
      age <- Integer()

      default private Person() as pure

      Person() as pure
        ->
          name as String
          age as Integer
        this.name :=: name
        this.age :=: age

      //default generates field-by-field comparison
      default operator <=>
      default operator ==
      default operator <>
      default operator $
      default operator #?
      default operator :=:

      override operator ? as pure
        <- rtn as Boolean: name? and age?

  defines program

    DefaultOperatorRulesDemo()
      stdout <- Stdout()

      p1 <- Person("Alice", 30)
      p2 <- Person("Alice", 30)
      p3 <- Person("Bob", 25)

      stdout.println(`p1 == p2: ${p1 == p2}`)
      stdout.println(`p1 == p3: ${p1 == p3}`)
      stdout.println(`p1: ${p1}`)
      stdout.println(`p1 set: ${p1?}`)

Java: IDE-generated equals/hashCode. Python: dataclass auto-generates. Rust: derive(Eq, Hash). Kotlin: data class auto-generates. EK9: 'default operator' with compile-time validation that property types support the required operator.

E07190 E07200 auto default extending field generate operator property type

Explanation: The class already has 'default operator <=>' defined. Changing == to <=> creates a duplicate operator definition, triggering E02030. Each operator can only be defined once. See ek9 -h E02030 for details.


Question: Where can the 'sanitized' keyword NOT be used in EK9?

What triggers E07941 SANITIZED_NOT_ON_CAPTURED?

What triggers E07943 SANITIZED_NOT_ON_DECLARATION?

Why can't captured variables be sanitized?

Where is the sanitized modifier restricted?

Answer:
sanitized keyword can't be on captured variables or declarations.

1. NOT ON CAPTURES (E07941)
When a dynamic function or class captures a variable, the capture cannot be marked sanitized. Sanitization is a property of function parameters, not captures.

2. NOT ON DECLARATIONS (E07943)
Local variable declarations cannot use sanitized. Only function/method parameters can be marked sanitized because sanitized tracks external untrusted input entering a function boundary.

VALID USAGE
  processInput() as pure
    -> input as sanitized String
    <- result as String: String(input)

INVALID USAGE
  localVar as sanitized String    // E07943

WHY RESTRICTED
Sanitization is about trust boundaries. Only parameters represent data crossing a trust boundary. Local variables and captures are already inside the trusted zone.

See Q215 for sanitized parameter basics. See Q217 for pure interaction. See Q269 for input validation.

defines module qa.security.sanitizedrestrictions

  defines function

    <?-
      Valid use: sanitized on function parameter.
    -?>
    validateInput() as pure
      -> userInput as sanitized String
      <- result as String?

      safeCopy <- String(userInput)
      result: "Validated: " + safeCopy

    processQuery() as pure
      -> sql as sanitized String
      <- output as String?

      localCopy <- String(sql)
      output: "Query: " + localCopy

  defines program

    SanitizedRestrictionsDemo()
      stdout <- Stdout()

      r1 <- validateInput("user data")
      if r1?
        stdout.println(r1)

      r2 <- processQuery("SELECT 1")
      if r2?
        stdout.println(r2)

      stdout.println("sanitized only on function parameters")

Java: no language-level sanitization. Python: no taint tracking. Rust: newtype for manual tracking. EK9: sanitized restricted to function parameters only, enforced at compile time.

E07941 E07943 boundary captured declaration parameter restriction sanitized security trust

Explanation: The sanitized modifier is only valid on incoming function/method parameters, not on a return or local declaration. See ek9 -h E07920 for details.


Question: What happens when you pass the wrong kind of construct as a parameter?

Why can't I pass a class where a function is expected?

Answer:
Parameter genus (fundamental KIND) must match — can't pass a class where a function is expected.

EK9 validates not just types but the fundamental kind (genus) of each argument. A class, function, record, trait, and service are different genera. Passing one where another is expected triggers E50090.

GENUS CATEGORIES
  CLASS — instantiated objects with methods
  FUNCTION — callable units
  RECORD — data-only aggregates
  TRAIT — interface contracts
  SERVICE — HTTP endpoints

EXAMPLE
If a function expects a function parameter, passing a class instance fails:
  doWork()
    -> action as Assessor of String
    ...

  doWork(MyClass())    // E50090 if MyClass is not an Assessor

See Q254 for Any type. See Q258 for type coercion. See Q255 for method resolution.

defines module qa.types.parametergenus

  defines function

    <?-
      Function that accepts a predicate.
    -?>
    countMatching()
      ->
        items as List of String
        check as Predicate of String
      <- result as Integer: 0

      for item in items
        if check(item)
          result++

  defines program

    ParameterGenusDemo()
      stdout <- Stdout()

      names <- List() of String
      names += "Alice"
      names += "Bob"
      names += "Charlie"

      //Dynamic function matching Predicate of String
      minLength <- 4

      longCheck <- (minLen: minLength) is Predicate of String as pure function
        r: t? and length t >= minLen

      longCount <- countMatching(names, longCheck)
      stdout.println(`Long names: ${longCount}`)

      shortCheck <- (maxLen: minLength) is Predicate of String as pure function
        r: t? and length t < maxLen

      shortCount <- countMatching(names, shortCheck)
      stdout.println(`Short names: ${shortCount}`)

Java: interface vs class distinction at compile time. Python: duck typing, no genus check. Rust: trait vs struct distinction. Go: interface vs struct. EK9: genus-level validation ensures fundamental construct kind matches.

E50090 class construct function genus kind mismatch parameter record trait

Explanation: The parameter expects a Predicate (function genus), not a Stdout instance. Passing the wrong kind of construct as a parameter causes a parameter mismatch. See ek9 -h E06270 for details.


Question: Why must EK9 constructs be used in their valid context?

What triggers E09010 CONSTRUCT_IN_WRONG_CONTEXT?

Why can't I define a class inside a function?

Where can I place different construct types?

Answer:
Language constructs must be used in their valid context.

EK9 enforces that constructs appear only where they are valid:

VALID CONTEXTS
  'defines class' — inside module, contains classes
  'defines function' — inside module, contains functions
  'defines record' — inside module, contains records
  'defines trait' — inside module, contains traits
  'defines service' — inside module, contains services
  'defines program' — inside module, contains programs
  'defines constant' — inside module, contains constants
  'defines application' — inside module, contains applications

STRUCTURE
Every .ek9 file starts with 'defines module <name>' and contains one or more 'defines <section>' blocks. Each section can only contain its declared construct type.

See Q725 for class body ordering. See Q726 for module reference syntax. See Q893 for section headers.

defines module qa.syntax.wrongcontext

  defines class

    <?-
      Classes go in 'defines class' section.
    -?>
    Point
      x <- Float()
      y <- Float()

      default private Point() as pure

      Point() as pure
        ->
          x as Float
          y as Float
        this.x :=: x
        this.y :=: y

      default operator <=>
      default operator ==
      default operator $
      default operator :=:

      override operator ? as pure
        <- rtn as Boolean: x? and y?

  defines program

    ConstructContextDemo()
      stdout <- Stdout()

      p1 <- Point(3.0, 4.0)
      p2 <- Point(3.0, 4.0)

      stdout.println(`p1: ${p1}`)
      stdout.println(`p1 == p2: ${p1 == p2}`)
      stdout.println("Each section holds its own construct type")

Java: class inside method is allowed (local class). Python: class inside function allowed. Rust: impl blocks must be at module level. Go: type definitions at package level. EK9: strict section-based structure, each section contains only its declared construct type.

E09010 class construct context function module placement record section structure

Explanation: Using a type name like 'Point' directly as a value expression is inappropriate. Types are not values. See ek9 -h E09010 for details.


Question: How do @Test programs work with command-line arguments, timeouts, and expected output?

What triggers E81001 MISSING_EXPECTED_OUTPUT?

What triggers E81002 ORPHAN_EXPECTED_OUTPUT?

What triggers E81003 ARGUMENT_COUNT_MISMATCH?

What triggers E81004 UNEXPECTED_ARGUMENTS?

What triggers E81006 TYPE_CONVERSION_ERROR?

Can @Test programs accept typed parameters?

How do I pass arguments to test programs?

Answer:
@Test programs accept TYPED PARAMETERS (not just String) — the EK9 runtime introspects the program's parameter signature and parses commandline_arg lines into the declared types. The E81xxx codes flag config issues (paired-file naming, arg counts, type conversion); the E82xxx codes flag execution issues (timeout, no-output).

TEST PROGRAM SIGNATURES
Tests ARE programs. They CAN have typed parameters:
  @Test
  NumberProcessor()
    ->
      num1 as Integer
      num2 as Integer
    assert num1?
    assert num2?

The runtime auto-parses each commandline_arg line into the declared type — Integer, Date, Duration, DateTime, Money, Boolean, etc. Per parameterizedHybrid.ek9 corpus example.

COMPANION FILES (live in test directory)
  expected_output.txt — single-case stdout (parameter-less test)
  expected_output_t1.txt / _t2.txt — numbered multi-case stdout (at PACKAGE ROOT)
  commandline_arg_<name>.txt + expected_case_<name>.txt — named-case pair (INSIDE dev/)
  env_vars.txt — KEY=VALUE environment
  expected_stderr.txt — expected stderr (supports {{DateTime}} / {{Millisecond}})
  test_data.txt — auxiliary input data

E81xxx CONFIG ERRORS (per TestConfigurationError.java)
  E81001 MISSING_EXPECTED_OUTPUT — commandline_arg_X.txt without expected_case_X.txt
  E81002 ORPHAN_EXPECTED_OUTPUT — expected_case_X.txt without commandline_arg_X.txt
  E81003 ARGUMENT_COUNT_MISMATCH — program declares N params, file provides not-N args
  E81004 UNEXPECTED_ARGUMENTS — commandline file exists but program has no params
  E81005 MISSING_ARGUMENTS — program declares params but no commandline files found
  E81006 TYPE_CONVERSION_ERROR — string can't parse as declared parameter type
  E81007 EMPTY_TEST — @Test program has no asserts AND no expected_output
  E81008 DUPLICATE_CASE_ID — same case name used twice in one directory
  E81009 INVALID_FILE_PATTERN — typo in filename (fuzzy-match suggestion)
  E81010 INVALID_PLACEHOLDER — bad {{...}} syntax in expected output
  E81013 ENVVARS_PARSE_ERROR — malformed line in env_vars.txt (no =)
  E81014 ORPHAN_ENVVARS — env_vars file without matching commandline_arg
  E81016 ORPHAN_STDIN — orphan stdin file

E82xxx EXECUTION ERRORS
  E82001 TEST_TIMEOUT — execution exceeded timeout (default 30s, EK9_TEST_TIMEOUT_MS configurable)
  E82002 NO_OUTPUT — test ran but produced no stdout despite expected_output.txt

STDOUT MISMATCH
Note: stdout-content-doesn't-match-expected is NOT a numbered E81xx error. It's reported in the test runner's pass/fail report.

See Q1296 for capture isolation. See Q204 for black-box patterns. See Q205 for parameterized tests.

defines module qa.testing.programargs

  defines program

    <?-
      Demonstrates the @Test program pattern with typed parameters
      and the companion-file model. In a real test, prefix with @Test.
      Programs (and tests) accept typed parameters that the EK9 runtime
      auto-parses from commandline_arg files.
    -?>
    TestProgramArgsDemo()
      stdout <- Stdout()

      stdout.println("@Test program facts:")
      stdout.println("  Tests ARE programs (defines program + @Test directive)")
      stdout.println("  Typed parameters supported (Integer, Date, Money, etc.)")
      stdout.println("  Runtime auto-parses commandline_arg lines into types")
      stdout.println("  E81xxx codes flag config issues (paired files, arg counts, type conversion)")
      stdout.println("  E82001 = TEST_TIMEOUT (default 30s)")
      stdout.println("  E82002 = NO_OUTPUT (test ran but produced no stdout)")

Java JUnit: @Test annotation, @ParameterizedTest with @ValueSource for typed params. Python pytest: fixtures + parametrize for typed params. Go: testing.T with table-driven tests. Rust: #[test] with proptest for parameterised. EK9: @Test directive on a program that accepts typed parameters; the runtime auto-parses commandline_arg files into the declared types, no manual parsing or annotations needed.

test program args
typed parameters
companion files
E81xxx config errors
E82xxx execution errors


Question: How do I use collect as to accumulate stream items into a custom type in EK9?

How does collect as work with custom types in EK9?

What does my type need to work with collect as?

What triggers E07835 COLLECT_REQUIRES_DEFAULT_CONSTRUCTOR?

Answer:
The 'collect as Type' stream terminal accumulates pipeline items into a custom type instance.

Your type needs TWO things:
  1. A default constructor (no arguments) to create the initial empty accumulator
  2. An 'operator |' that accepts the pipeline element type to receive each item

HOW IT WORKS
The stream pipeline:
  result <- cat [1, 2, 3] | collect as Counter

Is equivalent to:
  collector <- Counter()         Step 1: default constructor
  collector | 1                  Step 2: pipe each item
  collector | 2
  collector | 3
  result <- collector            Step 3: return accumulator

USING A RECORD
Records are ideal for collect-as because field initializers auto-generate a default constructor:
  Counter
    count as Integer: 0
    operator |
      -> item as Integer
      count++

WITH FOR-RANGE
Combine with for-range to count or sum ranges:
  total <- for i in 1 ... 10 | collect as Counter

If your type only has parameterized constructors (no default), the compiler reports E07835.

See Q235 for all stream operations. See Q931 for tail and skip. See Q122 for collect into built-in types.

defines module qa.streams.collectas

  defines record

    <?-
      A simple counter that accumulates Integer items from a stream.
      Has a default constructor (via field initializer) and operator |.
    -?>
    Counter
      count as Integer: 0

      operator |
        -> item as Integer
        if item?
          count++

      operator $ as pure
        <- rtn as String: String()
        if count?
          rtn :=? $count

      default operator ?

  defines trait

    <?-
      A trait has no constructors — cannot be used with 'collect as' (E07835).
    -?>
    Accumulator
      operator |
        -> item as Integer

  defines program

    CollectAsDemo()
      stdout <- Stdout()

      //Collect integers into a Counter using 'collect as'
      result <- cat [1, 2, 3, 4, 5] | collect as Counter
      stdout.println(`Count: ${result}`)

      //Works with for-range too
      rangeResult <- for i in 1 ... 10 | collect as Counter
      stdout.println(`Range count: ${rangeResult}`)

Java: Stream.collect(Collector) with Supplier/Accumulator/Combiner. Python: functools.reduce(). Rust: Iterator::fold() or collect() with FromIterator. Go: manual loop accumulation. EK9: collect as Type with default constructor + operator | pattern.

E07835
accumulate
aggregate
collect
collect as
counter
custom type
default constructor
fold
operator pipe
record
reduce
stream

collect as custom type
stream accumulation
E07835

Explanation: The collect-as pattern needs to create an initial empty instance via the default constructor. If your type only has parameterized constructors, add a default constructor or use a record with field initializers. See ek9 -h E07835 for details.


Question: Why does EK9 report E06260 when defining package properties?

What triggers E06260 PARAMETER_MISMATCH in packages?

Why must package version be a VersionNumber type?

How do I correctly declare package properties?

Answer:
Package properties in EK9 have strict type requirements. The 'version' property must be a VersionNumber literal, 'description' and 'license' must be Strings, and 'publicAccess' must be Boolean. Using the wrong type triggers E06260.

PACKAGE PROPERTY TYPES
  version <- 1.0.0-0           // VersionNumber literal
  description <- "My package"  // String literal
  license <- "MIT"             // String literal
  publicAccess <- true          // Boolean literal

WHY ENFORCED
Package metadata is used by the build system and dependency management. Invalid types would break version resolution, licensing, and publication. The compiler validates each property type at parse time.

See Q10 for module structure. See Q852 for reserved module names. See Q14 for package organization.

defines module qa.classes.paramtypemismatch

  defines package

    version <- 1.0.0-0
    description <- "Demonstrates correct package property types"

  defines program

    PackagePropertyDemo()
      stdout <- Stdout()
      stdout.println("Package properties use strict types")
      stdout.println("version: VersionNumber, description: String")

Java: pom.xml version is a free-form string. Python: setup.py version is a string. Rust: Cargo.toml version uses semver strings. Go: go.mod uses semver tags. EK9: version is a typed VersionNumber literal validated at compile time, E06260 if wrong type.

E06260 VersionNumber build metadata mismatch package parameter property type version


Question: Why can't I constrain a Boolean type in EK9?

What triggers E04010 TYPE_CANNOT_BE_CONSTRAINED?

Which types cannot be constrained in EK9?

Why does constraining Boolean cause a compiler error?

Answer:
Not all types can be constrained in EK9. Boolean already has exactly two values (true/false), making constraints meaningless. JSON has dynamic structure with undefined comparison semantics. Abstract types, functions, and traits cannot be instantiated as values.

NON-CONSTRAINABLE TYPES (E04010)
  Boolean — already maximally constrained (two values)
  JSON — dynamic structure, no defined comparison
  Abstract classes — cannot be instantiated
  Traits — cannot be instantiated
  Functions — not value types

CONSTRAINABLE TYPES
  String — constrain with regex or equality
  Integer — constrain with range (> 0 and < 100)
  Float — constrain with range
  Date, Time, DateTime — constrain with range
  Money — constrain with range
  Colour — constrain with specific values
  Records/Classes with comparison operators

See Q720 for constrainable types. See Q257 for constrained type overview. See Q721 for constrained types as parameters.

defines module qa.advancedtypes.constraintinvalid

  defines type

    ValidAge as Integer constrain as
      > 0 and < 150

    ShortName as String constrain as
      matches /^[A-Za-z]{1,20}$/

  defines program

    TypeConstraintDemo()
      stdout <- Stdout()

      if validAge <- ValidAge(25)
        stdout.println(`Valid age: ${validAge}`)

      tooOldValue <- 200
      badAge <- ValidAge(tooOldValue)
      stdout.println(`Bad age set: ${badAge?}`)

      if name <- ShortName("Alice")
        stdout.println(`Valid name: ${name}`)

Java: no built-in type constraints, use Bean Validation annotations at runtime. Python: runtime-only with pydantic. Rust: newtype pattern with constructor validation. Ada: subtype constraints on scalar types. EK9: compile-time constraint syntax with E04010 for inappropriate types.

Boolean E04010 JSON abstract constrain constraint invalid range type value

Explanation: Boolean cannot be constrained because it already has exactly two values. Use Integer, String, or other constrainable types. See ek9 -h E04010 for details.


Question: Why can I only inject components, not classes, in EK9?

What triggers E08160 COMPONENT_INJECTION_NOT_POSSIBLE?

Why does injection with ! fail for class types?

What types support dependency injection in EK9?

Answer:
EK9 dependency injection (the '!' suffix) only works with abstract component types. Classes, records, functions, and traits cannot be injected. E08160 is raised when you attempt to inject a non-component type.

WHY COMPONENTS ONLY
Components are EK9's managed service layer. The DI container knows how to create, wire, and lifecycle-manage components. Classes are user-constructed objects — the container has no way to manage their lifecycle.

INJECTABLE (component genus)
  defines component
    Logger as abstract
      log() as abstract -> message as String

NOT INJECTABLE (E08160)
  defines class — user-managed objects
  defines record — data-only aggregates
  defines trait — interface contracts
  defines function — callable units

CORRECT PATTERN
  notifier as NotificationService!    // OK: NotificationService is abstract component

INCORRECT PATTERN
  helper as HelperClass!              // E08160: HelperClass is a class, not a component

See Q668 for injectable contexts. See Q111 for component basics. See Q672 for program-application linking.

defines module qa.di.componentinjectioncontext

  defines class

    <?-
      A regular class — not a component. Cannot be injected.
    -?>
    HelperClass
      label <- "helper"

      default HelperClass()

      getLabel()
        <- rtn as String: label

      default operator ?

  defines component

    <?-
      Abstract component — can be injected via DI.
    -?>
    NotificationService as abstract

      sendNotification() as abstract
        -> recipient as String

      default operator ?

    <?-
      Concrete component implementing the abstract service.
    -?>
    EmailNotifier extends NotificationService

      override sendNotification()
        -> recipient as String
        stdout <- Stdout()
        stdout.println(`Email to ${recipient}`)

      default operator ?

  defines application

    NotifyApp
      register EmailNotifier() as NotificationService

  defines program

    ComponentInjectionDemo() with application of NotifyApp
      stdout <- Stdout()

      notifier as NotificationService!
      notifier.sendNotification("demo-user")

      stdout.println("Component injection works for abstract components")

Java Spring: @Autowired works on any managed bean. Python: inject anywhere. Go: manual wiring. Rust: no DI framework. EK9: injection limited to abstract components for architectural clarity, E08160 if wrong construct genus.

DI E08160 abstract class component construct genus injection managed service

Explanation: Only abstract component types can be injected with '!'. Classes are not managed by the DI container. Use a component for injectable dependencies. See ek9 -h E08160 for details.


Question: When should I use 'by' delegation instead of manual method forwarding?

What triggers E11023 MISSING_BY_DELEGATION?

Why does the compiler complain about manual delegation?

How does EK9 detect redundant delegation boilerplate?

Answer:
When a class implements a trait without 'by', has a compatible delegate field, and manually overrides 70% or more of the trait methods (minimum 5 methods), the compiler raises E11023.

WHY THIS MATTERS
Manually forwarding every method to a delegate field is boilerplate that the 'by' keyword eliminates. The compiler detects this pattern and enforces cleaner code.

THRESHOLD
All three conditions must be met:
1. The trait has at least 5 abstract methods
2. 70% or more are overridden in this class
3. A compatible field of the trait type exists

THIS EXAMPLE
The TaskHandler class implements Orchestrator (7 abstract methods) and overrides all 7. However, it has NO compatible delegate field of type Orchestrator, so E11023 does not trigger.

The typicalError mutation adds a delegate field, causing the checker to detect manual forwarding that should use 'by'.

See Q770 for trait by delegation basics. See Q313 for code smells catalog.

defines module qa.quality.missingbydelegation

  defines trait
    Orchestrator
      checkPreconditions() abstract
      executeCore() abstract
      handleCompletion() abstract
      inspectState() abstract
      prepareOutput() abstract
      runCleanup() abstract
      finalizeResult() abstract

  defines class
    OrchestratorImpl with trait of Orchestrator
      override checkPreconditions()
        require true
      override executeCore()
        require true
      override handleCompletion()
        require true
      override inspectState()
        require true
      override prepareOutput()
        require true
      override runCleanup()
        require true
      override finalizeResult()
        require true

  defines class

    <?-
      TaskHandler implements Orchestrator with all 7 methods overridden.
      No compatible delegate field exists, so E11023 does NOT trigger.
      Adding a delegate field of type Orchestrator would trigger E11023.
    -?>
    TaskHandler with trait of Orchestrator

      override checkPreconditions()
        require true
      override executeCore()
        require true
      override handleCompletion()
        require true
      override inspectState()
        require true
      override prepareOutput()
        require true
      override runCleanup()
        require true
      override finalizeResult()
        require true

      default operator ?

  defines program

    MissingByDelegationDemo()
      stdout <- Stdout()
      stdout.println("TaskHandler overrides all 7 methods without delegate field")
      stdout.println("No E11023 because no compatible delegate field exists")

Java: no delegation syntax, manual forwarding always allowed. Kotlin: 'by' exists but optional. Go: struct embedding is all-or-nothing. EK9: compiler detects when 'by' should be used and enforces it.

E11023 boilerplate by clean-code delegation forwarding override quality threshold trait

    TaskHandler with trait of Orchestrator
      delegate Orchestrator: OrchestratorImpl()

      override checkPreconditions()
        delegate.checkPreconditions()

Explanation: Adding a compatible delegate field of type Orchestrator while overriding all 7 methods triggers E11023. The compiler sees 7/7 (100%) overrides with a compatible field and requires 'by delegate'. See ek9 -h E11023 for details.


Question: What is the hybrid class-component anti-pattern in EK9?

What triggers E11024 HYBRID_CLASS_COMPONENT?

Why does EK9 reject classes with delegation and many service fields?

How many service fields can a class with 'by' delegation have?

Answer:
When a class uses 'by' delegation AND has 3 or more additional service fields (trait/abstract typed), it is mixing class and component responsibilities. E11024 triggers.

WHY THIS MATTERS
A class using 'by' delegation is extending behavior through composition (a class pattern). Having 3+ service fields means it is also coordinating multiple services (a component pattern). Mixing these patterns creates maintenance problems.

THRESHOLD
- Must have at least 1 'by' delegation
- Must have 3 or more additional service-type fields (not counting 'by' delegate fields)

THIS EXAMPLE
DocumentManager uses 'by' delegation for Storable trait and has 2 additional service fields (formatter and encoder). With only 2 service fields, this is under the threshold of 3.

The typicalError mutation adds a third service field, hitting the threshold and triggering E11024.

See Q314 for cohesion and coupling. See Q111 for component basics.

defines module qa.quality.excessiveservicefields

  defines trait

    Storable
      store() abstract
      retrieve() abstract

    Formatter
      formatContent() abstract

    Encoder
      encodeContent() abstract

    Compressor
      compress() abstract

  defines class

    StorableImpl with trait of Storable
      override store()
        require true
      override retrieve()
        require true

    PlainFormatter with trait of Formatter
      override formatContent()
        require true

    SimpleEncoder with trait of Encoder
      override encodeContent()
        require true

    BasicCompressor with trait of Compressor
      override compress()
        require true

  defines class

    <?-
      DocumentManager uses 'by' delegation for Storable
      and has 2 additional service fields (under threshold of 3).
      Adding a 3rd service field would trigger E11024.
    -?>
    DocumentManager with trait of Storable by persistence
      persistence Storable: StorableImpl()

      formatter Formatter: PlainFormatter()
      encoder Encoder: SimpleEncoder()

      title as String: "untitled"

      processDocument()
        formatter.formatContent()
        encoder.encodeContent()
        persistence.store()

      default operator ?

  defines program

    ExcessiveServiceFieldsDemo()
      stdout <- Stdout()
      stdout.println("DocumentManager has 1 by-delegation and 2 service fields")
      stdout.println("Under the threshold of 3 additional services")

Java: no 'by' delegation, no construct distinction. Spring beans mix data and services freely. C#: no built-in delegation detection. Go: no class vs component distinction. EK9: detects hybrid patterns at compile time.

E11024 by class component delegation field hybrid quality service threshold

      formatter Formatter: PlainFormatter()
      encoder Encoder: SimpleEncoder()
      compressor Compressor: BasicCompressor()

Explanation: Adding a third service field (compressor) to a class with 'by' delegation triggers E11024 (3 >= threshold of 3). Split into a component for service coordination. See ek9 -h E11024 for details.


Question: Why does EK9 limit injection fields per component to four?

What triggers E11040 EXCESSIVE_INJECTION_FIELDS?

How many injected dependencies can a component have?

How do I refactor a component with too many injections?

Answer:
A component with more than 4 injection fields (marked with '!') triggers E11040. This enforces the Single Responsibility Principle based on Clean Architecture research.

WHY FOUR IS THE LIMIT
Research from Robert C. Martin (Clean Architecture), Mark Seemann (Dependency Injection in .NET), and Shatnawi et al. (IEEE 2010) demonstrates that components with 5+ dependencies are typically doing too much.

HOW TO FIX
Group related dependencies into a facade component:
- Identify clusters of services used together
- Create a facade that wraps the cluster
- Inject the facade instead of individual services

THIS EXAMPLE
The TransactionCoordinator component has 4 injection fields, exactly at the limit. This compiles because 4 is the maximum allowed.

The typicalError mutation adds a fifth injection field, triggering E11040.

See Q862 for injection field limit overview. See Q227 for compile-time DI validation.

defines module qa.di.repeatedinjection

  defines component

    <?-
      Abstract service contracts for transaction processing.
    -?>
    LedgerService as abstract
      recordEntry() as abstract
        -> description as String
      default operator ?

    AuthorizationService as abstract
      authorize() as abstract
        -> principal as String
        <- allowed as Boolean?
      default operator ?

    MonitoringService as abstract
      logActivity() as abstract
        -> activity as String
      default operator ?

    DispatchService as abstract
      sendResult() as abstract
        -> recipient as String
      default operator ?

    ArchivingService as abstract
      archiveRecord() as abstract
        -> recordId as String
      default operator ?

    <?-
      Concrete implementations.
    -?>
    InMemoryLedger extends LedgerService
      override recordEntry()
        -> description as String
        Stdout().println("[LEDGER] " + description)
      default operator ?

    SimpleAuthorizer extends AuthorizationService
      override authorize()
        -> principal as String
        <- allowed as Boolean: true
      default operator ?

    ConsoleMonitor extends MonitoringService
      override logActivity()
        -> activity as String
        Stdout().println("[MONITOR] " + activity)
      default operator ?

    LocalDispatcher extends DispatchService
      override sendResult()
        -> recipient as String
        Stdout().println("[DISPATCH] " + recipient)
      default operator ?

    FileArchiver extends ArchivingService
      override archiveRecord()
        -> recordId as String
        Stdout().println("[ARCHIVE] " + recordId)
      default operator ?

    <?-
      TransactionCoordinator has exactly 4 injection fields.
      This is at the limit but valid. Adding a 5th triggers E11040.
    -?>
    TransactionCoordinator

      ledger as LedgerService!
      authorizer as AuthorizationService!
      monitor as MonitoringService!
      dispatcher as DispatchService!

      processTransaction()
        -> txnLabel as String
        permitted <- authorizer.authorize(txnLabel)
        if permitted
          ledger.recordEntry(txnLabel)
          monitor.logActivity(txnLabel)
          dispatcher.sendResult(txnLabel)

      default operator ?

  defines application

    TransactionApp
      register InMemoryLedger() as LedgerService
      register SimpleAuthorizer() as AuthorizationService
      register ConsoleMonitor() as MonitoringService
      register LocalDispatcher() as DispatchService

  defines program

    RepeatedInjectionDemo()
      stdout <- Stdout()
      stdout.println("TransactionCoordinator has 4 injection fields (at limit)")
      stdout.println("Adding a 5th would trigger E11040")

Java Spring: unlimited @Autowired fields. C#: unlimited DI constructor parameters. Python: no DI framework limits. Go Wire: no limits. EK9: compile-time limit of 4 injection fields per component, enforced by E11040.

E11040 SRP component dependency excessive facade field injection limit quality

      ledger as LedgerService!
      authorizer as AuthorizationService!
      monitor as MonitoringService!
      dispatcher as DispatchService!
      archiver as ArchivingService!

Explanation: 5 injection fields exceeds the limit of 4. Extract related services into a facade component and inject the facade instead. See ek9 -h E11040 for details.


Question: What is the maximum number of registrations in an EK9 application block?

What triggers E11042 EXCESSIVE_APPLICATION_REGISTRATIONS?

Why does EK9 limit application registrations?

How do I split a large application block?

Answer:
An application block can have at most 12 registrations. Exceeding 12 triggers E11042, indicating the application is becoming a monolith.

WHY TWELVE
Large composition roots (the place where all dependencies are wired together) become hard to understand and maintain. Research in component-based software engineering shows that focused, modular composition is more maintainable.

THE FIX
Split into sub-applications using 'with application of':
  defines application
    InfrastructureApp
      register DbService() as AbstractDb

  defines application
    BusinessApp with application of InfrastructureApp
      register OrderService() as AbstractOrder

THIS EXAMPLE
The PlatformApp has exactly 11 registrations, safely under the limit of 12. The typicalError mutation adds 2 more registrations to reach 13, triggering E11042.

See Q892 for application registration limits. See Q227 for DI overview.

defines module qa.di.excessivemethodinjection

  defines component

    Contract01 as abstract
      action01() as abstract
        <- rtn as String?
      default operator ?
    Impl01 extends Contract01
      override action01()
        <- rtn <- "c01"
      default operator ?

    Contract02 as abstract
      action02() as abstract
        <- rtn as String?
      default operator ?
    Impl02 extends Contract02
      override action02()
        <- rtn <- "c02"
      default operator ?

    Contract03 as abstract
      action03() as abstract
        <- rtn as String?
      default operator ?
    Impl03 extends Contract03
      override action03()
        <- rtn <- "c03"
      default operator ?

    Contract04 as abstract
      action04() as abstract
        <- rtn as String?
      default operator ?
    Impl04 extends Contract04
      override action04()
        <- rtn <- "c04"
      default operator ?

    Contract05 as abstract
      action05() as abstract
        <- rtn as String?
      default operator ?
    Impl05 extends Contract05
      override action05()
        <- rtn <- "c05"
      default operator ?

    Contract06 as abstract
      action06() as abstract
        <- rtn as String?
      default operator ?
    Impl06 extends Contract06
      override action06()
        <- rtn <- "c06"
      default operator ?

    Contract07 as abstract
      action07() as abstract
        <- rtn as String?
      default operator ?
    Impl07 extends Contract07
      override action07()
        <- rtn <- "c07"
      default operator ?

    Contract08 as abstract
      action08() as abstract
        <- rtn as String?
      default operator ?
    Impl08 extends Contract08
      override action08()
        <- rtn <- "c08"
      default operator ?

    Contract09 as abstract
      action09() as abstract
        <- rtn as String?
      default operator ?
    Impl09 extends Contract09
      override action09()
        <- rtn <- "c09"
      default operator ?

    Contract10 as abstract
      action10() as abstract
        <- rtn as String?
      default operator ?
    Impl10 extends Contract10
      override action10()
        <- rtn <- "c10"
      default operator ?

    Contract11 as abstract
      action11() as abstract
        <- rtn as String?
      default operator ?
    Impl11 extends Contract11
      override action11()
        <- rtn <- "c11"
      default operator ?

    Contract12 as abstract
      action12() as abstract
        <- rtn as String?
      default operator ?
    Impl12 extends Contract12
      override action12()
        <- rtn <- "c12"
      default operator ?

    Contract13 as abstract
      action13() as abstract
        <- rtn as String?
      default operator ?
    Impl13 extends Contract13
      override action13()
        <- rtn <- "c13"
      default operator ?

  defines application

    <?-
      PlatformApp has 11 registrations, under the limit of 12.
      Adding 2 more would trigger E11042.
    -?>
    PlatformApp
      register Impl01() as Contract01
      register Impl02() as Contract02
      register Impl03() as Contract03
      register Impl04() as Contract04
      register Impl05() as Contract05
      register Impl06() as Contract06
      register Impl07() as Contract07
      register Impl08() as Contract08
      register Impl09() as Contract09
      register Impl10() as Contract10
      register Impl11() as Contract11

  defines program

    ExcessiveMethodInjectionDemo() with application of PlatformApp
      stdout <- Stdout()
      svc as Contract01!
      result <- svc.action01()
      stdout.println(`Result: ${result}`)
      stdout.println("PlatformApp has 11 registrations (under limit of 12)")

Java Spring: unlimited beans per context. Python: no DI limits. Go Wire: no limits. Rust: no standard DI. EK9: compiler enforces 12-registration limit per application block.

DI E11042 application composition limit modular monolith quality registration split

      register Impl10() as Contract10
      register Impl11() as Contract11
      register Impl12() as Contract12
      register Impl13() as Contract13

Explanation: Adding 2 more registrations brings the total to 13, exceeding the limit of 12. Split into focused sub-applications. See ek9 -h E11042 for details.


Question: What is a data clump and how does EK9 detect it?

What triggers E11053 DATA_CLUMP_DETECTED?

How many functions sharing parameters trigger the data clump error?

How do I extract a record from repeated parameters?

Answer:
A data clump occurs when 3 or more callables share 4 or more parameters with the same types and names. EK9 detects this at compile time and raises E11053.

WHY THIS MATTERS
Repeated parameter groups signal a missing abstraction. Martin Fowler identified data clumps as a code smell in 'Refactoring' (1999). The repeated parameters should be extracted into a record.

THRESHOLD
- 4 or more parameters with matching types and names
- Shared across 3 or more callables in the same module

THE FIX
Extract a record:
  defines record
    Coordinate
      latitude Float: 0.0
      longitude Float: 0.0
      altitude Float: 0.0
      heading Float: 0.0
Then pass the record instead of 4 separate parameters.

THIS EXAMPLE
Two functions share 4 Float parameters (latitude, longitude, altitude, heading). With only 2 functions sharing the clump, this is below the threshold of 3.

The typicalError mutation adds a third function with the same 4 parameters, triggering E11053.

See Q313 for code smells. See Q310 for quality overview.

defines module qa.quality.dataclump

  defines function

    <?-
      Computes a distance metric from navigation coordinates.
      Uses 4 Float parameters: latitude, longitude, altitude, heading.
    -?>
    computeDistance()
      ->
        latitude as Float
        longitude as Float
        altitude as Float
        heading as Float
      <- result as Float: latitude + longitude + altitude + heading

    <?-
      Calculates a heading adjustment from the same navigation coordinates.
      Same 4 parameters as computeDistance, but only 2 functions share them.
      Below the threshold of 3 callables.
    -?>
    calculateHeading()
      ->
        latitude as Float
        longitude as Float
        altitude as Float
        heading as Float
      <- result as Float: heading + latitude + longitude + altitude

  defines program

    DataClumpDemo()
      stdout <- Stdout()
      distance <- computeDistance(latitude: 51.5, longitude: 0.12, altitude: 100.0, heading: 270.0)
      bearing <- calculateHeading(latitude: 51.5, longitude: 0.12, altitude: 100.0, heading: 270.0)
      stdout.println(`Distance: ${distance}`)
      stdout.println(`Bearing: ${bearing}`)

Java: data clumps detected only by SonarQube or PMD (optional). Python: no detection. C++: no detection. Go: convention recommends structs but not enforced. EK9: compile-time detection when 3+ callables share 4+ matching parameters.

E11053 clump data extract function parameter quality record refactoring smell

    calculateHeading()
      ->
        latitude as Float
        longitude as Float
        altitude as Float
        heading as Float
      <- result as Float: heading + latitude + longitude + altitude

    estimateArrival()
      ->
        latitude as Float
        longitude as Float
        altitude as Float
        heading as Float
      <- estimate as Float: latitude + longitude + altitude + heading

Explanation: Adding a third function with the same 4 Float parameters (latitude, longitude, altitude, heading) triggers E11053. Extract a Coordinate record instead. See ek9 -h E11053 for details.


Question: How does the <? coalescing minimum operator work in EK9?

How do I find the minimum of two values safely in EK9?

Answer:
The <? operator is a COALESCING MINIMUM. It returns the smaller of two SET values. If either value is UNSET, the result depends on which is set:

- Both SET: returns the smaller value (like Math.min)
- Left SET, right UNSET: returns left (the SET one)
- Left UNSET, right SET: returns right (the SET one)
- Both UNSET: returns UNSET

This is unique to EK9 — no other language has coalescing comparators.

The >? operator is the coalescing MAXIMUM (same rules but returns the larger).

Bridge: There is no equivalent in Java, Python, Kotlin, or Rust. The closest analogy is a null-safe Math.min() that automatically handles missing values.

See Q899 for the full coalescing operator family. See Q24 for tri-state semantics.

defines module qa.operators.coalescingmin

  defines function

    <?-
      Find the cheaper price using <? coalescing minimum.
      If either price is unset, returns the other.
    -?>
    bestDeal() as pure
      ->
        onlinePrice as Float
        storePrice as Float
      <-
        rtn as Float: onlinePrice <? storePrice

    <?-
      Find the closest deadline using <? coalescing minimum on dates.
      Works with any type that has the <=> comparison operator.
    -?>
    nearestDeadline() as pure
      ->
        deadlineA as Date
        deadlineB as Date
      <-
        rtn as Date: deadlineA <? deadlineB

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

    chainedMinFloat() as pure
      ->
        a as Float
        b as Float
        c as Float
      <- rtn as Float: a <? b <? c

  defines program

    CoalescingMinDemo()
      stdout <- Stdout()

      // Both values set — returns the smaller
      cheapest <- bestDeal(29.99, 19.99)
      stdout.println(`Cheapest: ${cheapest}`)

      // One value unset — returns the SET one
      knownPrice <- 25.0
      unknownPrice <- Float()
      safePrice <- knownPrice <? unknownPrice
      stdout.println(`Safe price: ${safePrice}`)

      // Both unset — result is unset
      noPrice1 <- Float()
      noPrice2 <- Float()
      noResult <- noPrice1 <? noPrice2
      if noResult?
        stdout.println("Should not reach here")
      else
        stdout.println("Both unset, result is unset")

      // >? coalescing maximum — returns the larger
      topScore <- maxOf(85, 92)
      stdout.println(`Top score: ${topScore}`)

      // Chaining: find minimum of three values
      lowest <- chainedMinFloat(30.0, 15.0, 22.0)
      stdout.println(`Lowest of three: ${lowest}`)

Java: Math.min(a, b) requires null checks. Python: min(a, b) throws on None. Kotlin: minOf(a, b) requires ?: null handling. EK9's <? combines comparison + unset handling in one operator.

coalescing
less than
minimum
safe comparison
unset handling

Explanation: EK9 has no min() function so the call cannot be resolved — use the <? coalescing minimum operator instead. See ek9 -h E50001 for details.


Question: How does the >? coalescing maximum operator work in EK9?

How do I find the maximum of two values safely in EK9?

Answer:
The >? operator is a COALESCING MAXIMUM. It returns the larger of two SET values. If either value is UNSET, the result depends on which is set:

- Both SET: returns the larger value (like Math.max)
- Left SET, right UNSET: returns left (the SET one)
- Left UNSET, right SET: returns right (the SET one)
- Both UNSET: returns UNSET

This is unique to EK9 — no other language has coalescing comparators.

The <=? and >=? operators also exist for 'less-or-equal minimum' and 'greater-or-equal maximum'.

See Q899 for the full coalescing operator family. See Q963 for <? coalescing minimum.

defines module qa.operators.coalescingmax

  defines function

    <?-
      Find the highest temperature using >? coalescing maximum.
      If either reading is unset, returns the other.
    -?>
    peakTemperature() as pure
      ->
        morningReading as Float
        afternoonReading as Float
      <-
        rtn as Float: morningReading >? afternoonReading

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

    minOrEqual() as pure
      ->
        left as Integer
        right as Integer
      <- rtn as Integer: left <=? right

    maxOrEqual() as pure
      ->
        left as Integer
        right as Integer
      <- rtn as Integer: left >=? right

  defines program

    CoalescingMaxDemo()
      stdout <- Stdout()

      // Both values set — returns the larger
      highest <- maxOf(85, 92)
      stdout.println(`Highest: ${highest}`)

      // One value unset — returns the SET one
      knownScore <- 75
      missingScore <- Integer()
      safeScore <- knownScore >? missingScore
      stdout.println(`Safe score: ${safeScore}`)

      // Both unset — result is unset
      noScore1 <- Integer()
      noScore2 <- Integer()
      noResult <- noScore1 >? noScore2
      if noResult?
        stdout.println("Should not reach here")
      else
        stdout.println("Both unset, result is unset")

      // <=? coalescing less-or-equal minimum
      minimum <- minOrEqual(10, 10)
      stdout.println(`<=? with equal: ${minimum}`)

      // >=? coalescing greater-or-equal maximum
      maximum <- maxOrEqual(10, 10)
      stdout.println(`>=? with equal: ${maximum}`)

      // Practical use: find peak temperature across readings
      peak <- peakTemperature(22.5, 31.8)
      stdout.println(`Peak temp: ${peak}`)

Java: Math.max(a, b) requires null checks. Python: max(a, b) throws on None. EK9's >? combines comparison + unset handling in one operator. No equivalent exists in any mainstream language.

coalescing
greater than
maximum
safe comparison
unset handling

Explanation: EK9 has no max() function so 'max' will not resolve (E50001); use the >? coalescing maximum operator instead. See ek9 -h E50001 for details.


Question: What are the three assignment operators in EK9 and when do I use each?

What is the difference between <- and := in EK9?

How do declaration and assignment differ in EK9?

Answer:
EK9 has assignment operators for declaration and reassignment. EK9 separates declaration from reassignment with distinct operators:

<- DECLARATION WITH TYPE INFERENCE: Creates a NEW variable, compiler infers the type.
  name <- "Steve"     creates variable 'name' (inferred as String)
  count <- 0           creates variable 'count' (inferred as Integer)
  stdout <- Stdout()   creates variable 'stdout' (inferred as Stdout)

:= or : or = ASSIGNMENT: All three are interchangeable for reassignment.
  name := "Alice"     changes existing 'name' to "Alice"
  name: "Alice"       same effect — : is shorthand for :=
  name = "Alice"      same effect — = also works

With EXPLICIT TYPE declaration, use : or := or = with a type:
  age as Integer: 30     declares 'age' as Integer with value 30
  age as Integer := 30   same effect
  age as Integer = 30    same effect

:=? GUARDED ASSIGNMENT: Only assigns if the target is currently UNSET.
  name :=? "Default"   assigns "Default" ONLY if name is unset
  name :=? "Other"     does nothing — name is already set

KEY RULES:
- <- is declaration with type inference (used once per variable)
- :=, :, = are interchangeable for reassignment and explicit type declaration
- :=? is safe for conditional initialization

EK9 makes this distinction at the operator level — <- always means 'new variable', := always means 'update existing'.

defines module qa.gettingstarted.assignmentops

  defines program

    AssignmentOpsDemo()
      stdout <- Stdout()

      // <- DECLARATION: creates new variable
      greeting <- "Hello"
      stdout.println(`After <-: ${greeting}`)

      // := ASSIGNMENT: updates existing variable
      greeting := "Hi there"
      stdout.println(`After :=: ${greeting}`)

      // := again on same variable — fine, it exists
      greeting := "Hey"
      stdout.println(`After := again: ${greeting}`)

      // :=? GUARDED: only assigns if unset
      nickname <- String()
      stdout.println(`Before :=?: nickname is ${nickname?}`)
      nickname :=? "Buddy"
      stdout.println(`After :=? on unset: ${nickname}`)
      nickname :=? "Pal"
      stdout.println(`After :=? on set: ${nickname}`)

      // Practical example: building a configuration
      host <- "localhost"
      port <- 8080
      host := "production.example.com"
      port := 443
      stdout.println(`Config: ${host}:${port}`)

Go separates declaration (:=) from assignment (=), EK9 does the same with <- and :=. Java and Python use = for both (ambiguous). Kotlin uses val/var keywords, EK9 uses operators.

assignment
declaration
guarded
reassignment
variable creation

<- operator
:= operator
:=? operator
assignment operators


Question: What does the #? hashcode operator do in EK9?

What does hash question mark mean in EK9?

Answer:
The #? operator is the HASHCODE operator in EK9. It calls the _hashcode() method on the object and returns an Integer.

Syntax: #? variableName (prefix operator)

IMPORTANT: #? is NOT a Python comment. It is NOT a Ruby method call. The # prefix in EK9 means 'introspection' — #? is hashcode, #^ is promote, #< is first, #> is last.

EK9 operator method names:
  #? calls _hashcode() — returns Integer
  #^ calls _promote() — returns a different type (type widening)
  $ calls _string() — returns String (string CONVERSION)
  ? calls _isSet() — returns Boolean

The 'default operator' keyword auto-generates #? from declared fields. You rarely need to implement it manually.

Bridge: Like Java's hashCode() but as a prefix operator. Like Python's hash() but with # prefix syntax.

defines module qa.operators.hashcodedetail

  defines record

    <?-
      Simple record with auto-generated #? operator via 'default operator'.
    -?>
    Coordinate
      latitude as Float: Float()
      longitude as Float: Float()

      Coordinate()
        ->
          latitude as Float
          longitude as Float
        this.latitude :=: latitude
        this.longitude :=: longitude

      default operator

  defines program

    HashcodeDemo()
      stdout <- Stdout()

      // #? on built-in types
      greeting <- "Hello"
      nameHash <- #? greeting
      stdout.println(`String hash: ${nameHash}`)

      age <- 42
      ageHash <- #? age
      stdout.println(`Integer hash: ${ageHash}`)

      // #? on custom record (auto-generated by 'default operator')
      location <- Coordinate(51.5074, -0.1278)
      locationHash <- #? location
      stdout.println(`Coordinate hash: ${locationHash}`)

      // Comparing hashcodes
      sameLocation <- Coordinate(51.5074, -0.1278)
      if #? location == #? sameLocation
        stdout.println("Same hashcodes for equal coordinates")

Java: obj.hashCode() — EK9: #? obj. Python: hash(obj) — EK9: #? obj. Kotlin: obj.hashCode() — EK9: #? obj. The # prefix is EK9's introspection family, NOT a comment character.

hash
hashcode
introspection
prefix operator

Explanation: EK9 has no Java-style '.hashCode()' method; use the '#?' prefix operator, as 'hashCode' resolves on no EK9 type. See ek9 -h E50060 for details.


Question: Does EK9 have ++ and -- operators? What are the rules?

Are ++ and -- expressions or statements in EK9?

Answer:
YES, EK9 has ++ and -- operators. But they are STATEMENT-ONLY — they cannot be used in expressions.

VALID (statement-only):
  count++         increments count by 1
  count--         decrements count by 1
  temperature++   increments temperature

INVALID (not an expression):
  y <- x++        COMPILE ERROR — ++ cannot be used in an expression
  total <- count++ + 5    COMPILE ERROR
  if x++ > 10     COMPILE ERROR

WHY STATEMENT-ONLY?
Mutating operators (++, --, +=, -=) return 'this' — the SAME object, not a copy. This means y <- x++ would make y and x point to the SAME object (aliasing). This is EK9's most dangerous gotcha and is prevented by the compiler.

Bridge: Like C++/Java's ++ but restricted to statements. Similar to how Python chose not to have ++ at all — EK9 allows it but prevents the aliasing danger.

OTHER MUTATING OPERATORS:
  x += 5    statement-only, adds 5 to x
  x -= 3    statement-only, subtracts 3 from x
  x *= 2    statement-only, multiplies x by 2

All mutating operators follow the same rule: statement-only, no expression use.

See Q905 for mutating operator aliasing details. See Q241 for the full mutation operator list.

defines module qa.operators.incrementrules

  defines program

    IncrementDemo()
      stdout <- Stdout()

      // ++ as statement — VALID
      counter <- 0
      counter++
      stdout.println(`After ++: ${counter}`)

      counter++
      counter++
      stdout.println(`After two more ++: ${counter}`)

      // -- as statement — VALID
      counter--
      stdout.println(`After --: ${counter}`)

      // += as statement — VALID
      counter += 10
      stdout.println(`After += 10: ${counter}`)

      // -= as statement — VALID
      counter -= 3
      stdout.println(`After -= 3: ${counter}`)

      // The correct pattern when you need the value:
      // Increment first, then use the variable
      score <- 100
      score++
      displayScore <- score
      stdout.println(`Score after increment: ${displayScore}`)

      // NOT: displayScore <- score++  (COMPILE ERROR)

Java: i++ works in expressions (causes bugs). C: i++ is expression (undefined behaviour in some cases). Python: no ++ at all. EK9: ++ exists but is statement-only — the safest of all approaches.

aliasing
decrement
increment
mutation
statement only

++ operator
-- operator
statement-only operators
mutating operators

Explanation: The '++' operator is statement-only in EK9 — it cannot be used inside an expression like 'displayScore <- score++'; increment on its own line first, then use the variable. See ek9 -h E07950 for details.


Question: Why does EK9 separate variable declaration from assignment?

Why can't I just use = for everything in EK9?

What's the point of having <- and := as separate operators?

Why does EK9 force me to use <- the first time?

How is <- different from := in practice?

Answer:
EK9 separates declaration (<-) from assignment (:=) to prevent accidental variable creation and make variable lifetimes explicit.

THE PROBLEM IN OTHER LANGUAGES:
In Python/JavaScript, x = 5 either creates or reassigns — you can't tell which. This leads to:
- Accidental shadowing (creating a new variable when you meant to update)
- Typo bugs (misspelling a variable name creates a new one silently)
- Unclear lifetime (when was this variable created?)

EK9'S SOLUTION:
  <- means 'I am creating something new'
  := means 'I am updating something that exists'

This makes every variable's birth point explicit in the code.

Many languages (Go, Rust, Ada) also separate declaration from assignment. EK9 uses <- for new variables and := for updates — making every variable's birth point visible in the code.

See Q965 for the full assignment operator guide.

defines module qa.gettingstarted.declvsassign

  defines function

    <?-
      Shows the lifecycle: declare with <-, update with :=.
      EK9 separates declaration (<-) from assignment (:=).
    -?>
    buildGreeting()
      ->
        firstName as String
        lastName as String
      <-
        rtn as String: String()

      // <- creates the variable (declaration)
      fullName <- `${firstName} ${lastName}`

      // := updates the variable (assignment to existing variable)
      fullName := `${fullName}!`

      rtn := fullName

  defines program

    DeclVsAssignDemo()
      stdout <- Stdout()

      // <- declares new variables
      message <- buildGreeting("Steve", "Limb")
      stdout.println(message)

      // := updates existing variable
      message := buildGreeting("Alice", "Smith")
      stdout.println(message)

      // :=? only assigns if currently unset
      optionalName <- String()
      optionalName :=? "Fallback"
      stdout.println(`Optional: ${optionalName}`)

      // :=? has no effect when already set
      optionalName :=? "Ignored"
      stdout.println(`Still: ${optionalName}`)

Go separates declaration from reassignment with different operators, just like EK9. Rust uses 'let' keyword for declaration. Python and Java use = for everything. EK9 makes the distinction at the operator level: <- for new, := for update.

assignment
declaration
lifetime
shadowing
variable creation

<- vs :=
variable declaration
assignment separation

message := buildGreeting("Steve", "Limb")

Explanation: The first use of a variable must declare it with '<-'; using ':=' on 'message' before it exists means the name does not resolve. See ek9 -h E50001 for details.


Question: What does the #^ promote operator do in EK9?

How do I convert types in EK9?
What is #^ in EK9?
How does type promotion work in EK9?
What does hash caret mean in EK9?

Answer:
The #^ operator is the PROMOTE operator in EK9. It calls the _promote() method on the object and returns a DIFFERENT type (type widening/conversion).

Syntax: #^ variableName (prefix operator)

IMPORTANT: #^ is NOT a Python comprehension. It is NOT a Ruby hash. The # prefix in EK9 means 'introspection' — #^ is promote, #? is hashcode, #< is first, #> is last.

EK9 introspection operators:
  #^ calls _promote() — type CONVERSION to a wider type
  #? calls _hashcode() — returns Integer hashcode
  #< calls _first() — returns first value (enumerations)
  #> calls _last() — returns last value (enumerations)

Typical use: converting a Character to a String, or a narrower numeric type to a wider one.

The 'default operator' keyword auto-generates #^ from declared fields. You rarely implement it manually.

Bridge: Like Java's widening conversion (int to long) but as an explicit operator. Like Rust's From/Into traits but with operator syntax.

defines module qa.operators.promotedetail

  defines program

    PromoteDemo()
      stdout <- Stdout()

      // #^ on Character — promotes to String
      letter <- 'A'
      letterAsString <- #^ letter
      stdout.println(`Character promoted to String: ${letterAsString}`)

      // #^ on Integer — may promote to Float depending on type
      wholeNumber <- 42
      promoted <- #^ wholeNumber
      stdout.println(`Integer promoted: ${promoted}`)

      // The promote operator returns a DIFFERENT type
      // This is what distinguishes it from $ (string conversion)
      // $ always returns String, #^ returns whatever _promote() is defined to return

Java: implicit widening (int -> long) or explicit casting. Python: int(x), str(x) conversion functions. EK9: #^ prefix operator calls _promote(). The # prefix is EK9's introspection family — NOT a comment.

introspection
prefix operator
promote
type conversion
widening

#^ operator
promote
_promote method
type conversion


Question: How do I output stream results to stdout or a collection in EK9?

How do stream terminals work in EK9?
What does > mean in EK9 streams?
How do I redirect stream output in EK9?
How do I collect stream results in EK9?

Answer:
EK9 stream pipelines use Unix-style > for terminal output. The > operator sends stream results to a sink (like Stdout, a collection, or a file).

TERMINAL OPERATORS:
  > stdout          sends each item to stdout (like Unix > redirect)
  > myList          sends items into a collection
  >> myList         appends items to existing collection
  collect as Type   materializes stream into new collection

IMPORTANT: Use > like Unix redirect — NOT stdout <- cat items.

CORRECT:
  cat items | filter by isValid > stdout
  cat numbers | sort by ascending | head 5 > stdout
  cat names | map with toUpperCase | collect as List of String

WRONG:
  stdout <- cat items | filter by isValid   WRONG — do not use <- for stream output
  result = cat items | collect               WRONG — use 'collect as Type'

Bridge: Like Unix pipes: ls | grep foo | head 5 > output.txt. The > in EK9 streams works exactly like > in shell — it redirects the pipeline output to a destination.

See Q235 for full stream operations reference. See Q237 for streams vs loops comparison.

defines module qa.streams.outputpatterns

  defines function

    <?-
      Check if a number is even.
    -?>
    isEven() as pure
      -> numberToCheck as Integer
      <- rtn as Boolean: numberToCheck mod 2 == 0

  defines program

    StreamOutputDemo()
      stdout <- Stdout()

      // > stdout — sends each item to standard output
      stdout.println("Even numbers:")
      cat [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        | filter by isEven
        > stdout

      // collect as — materializes into a new collection
      stdout.println("Collected evens:")
      evens <- cat [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        | filter by isEven
        | collect as List of Integer
      stdout.println($evens)

      // head N — take first N then > to sink
      stdout.println("First 3:")
      cat [10, 20, 30, 40, 50]
        | head 3
        > stdout

Unix shell: command | filter | head > output — same concept in EK9. Java Streams: .collect(Collectors.toList()) — EK9: collect as List of T. Python: list comprehension — EK9: cat source | filter | collect.

collect
redirect
sink
stdout
stream output
terminal

stream terminals
> operator in streams
collect as
stream output


Question: How do guard expressions work in EK9 if statements?

How do I combine declaration with null checking in EK9?

What does the <- guard do inside an if statement?

Answer:
Guard expressions combine variable handling with isSet checking in a single statement. The block only executes if the value is SET.

THREE GUARD FORMS:

1. DECLARATION GUARD (<-):
  if name <- getName()
    stdout.println(name)   declares name, only runs if SET

2. ASSIGNMENT GUARD (:=):
  selectedItem <- String()
  if selectedItem := findItem("key")
    stdout.println(selectedItem)   assigns to existing var, only runs if SET

3. GUARDED ASSIGNMENT (:=?):
  config <- loadDefaults()
  if config :=? loadOverrides()
    stdout.println(config)   only assigns if loadOverrides() returned SET

The := guard always assigns, then checks isSet. The :=? guard only assigns when the right side is SET — the existing value is preserved if the right side is unset.

WITH ADDITIONAL CONDITION:
  if name <- getName() then name.length() > 3
    stdout.println(name)   runs if SET AND length > 3

All three guard forms work IDENTICALLY across all control flow:
  if v <- expr()         declaration guard
  switch v := expr()     assignment guard
  while v :=? expr()     guarded assignment
  try v <- expr()        declaration guard in try
  for v <- iterator      declaration guard in for

This eliminates 90-95% of null pointer exceptions through compile-time enforcement.

Bridge: Like Go's 'if err := doSomething(); err != nil'. Like Rust's 'if let Some(v) = expr()'. Like Swift's 'if let v = optionalExpr'.

See Q74 for more guard examples. See Q75 for guard switch.

defines module qa.controlflow.guardif

  defines function

    <?-
      Returns a greeting if the name has content, or unset if empty.
    -?>
    findGreeting() as pure
      -> personName as String
      <- rtn as String: String()

      if personName?
        rtn: "Hello, " + personName

  defines program

    GuardIfDemo()
      stdout <- Stdout()

      // BASIC GUARD: only enters if findGreeting returns SET
      if greeting <- findGreeting("Steve")
        stdout.println(greeting)

      // GUARD WITH UNSET: block is skipped
      if greeting <- findGreeting(String())
        stdout.println("This won't print")
      else
        stdout.println("Greeting was unset - skipped")

      // GUARD WITH CONDITION: SET and passes additional test
      minLength <- 3
      if greeting <- findGreeting("Alice") then greeting.length() > minLength
        stdout.println(`Long greeting: ${greeting}`)

      // GUARD IN SWITCH: declares and checks in one step
      switch greet <- findGreeting("Charlie") with greet
        case == "Hello, Charlie"
          stdout.println("Found Charlie's greeting")
        default
          stdout.println(`Other greeting: ${greet}`)

Go: if err := f(); err != nil { } — same concept. Rust: if let Some(v) = expr { } — same concept. Swift: if let v = optional { } — same concept. Java: requires separate null check. EK9 guards are universal across all control flow.

control flow
declaration guard
guard
isSet check
null safety

guard expressions
if guard
declaration guard
isSet checking

greeting.length() > minLength and greeting <> null

Explanation: 'null' does not exist in EK9 — use tri-state semantics (unset/set) with guard expressions such as 'if name <- expr()' instead of comparing against null. See ek9 -h E01073 for details.


Question: My variable already exists but <- creates a new one — help!

When do I use <- vs := in EK9?
I keep mixing up <- and := in EK9
What is the rule for <- vs := in EK9?

Answer:
Simple rule: '<-' is for the FIRST TIME. ':=' is for EVERY TIME AFTER.

PATTERN:
  name <- "Steve"     FIRST TIME: <- creates name
  name := "Alice"     AFTER: := updates name
  name := "Bob"       AFTER: := updates name again

The compiler ENFORCES this:
- Using := before <- is an error (variable does not exist yet)
- Using <- twice on the same name is an error (already declared)

Also: ':=' and ':' and '=' are all interchangeable for reassignment.
  name := "Alice"   same as
  name: "Alice"     same as
  name = "Alice"

defines module qa.gettingstarted.declarethenassign

  defines program

    DeclareThenAssignDemo()
      stdout <- Stdout()

      // <- creates the variable (FIRST TIME)
      greeting <- "Hello"
      stdout.println(greeting)

      // := updates it (EVERY TIME AFTER)
      greeting := "Hi there"
      stdout.println(greeting)

      // := again — still updating
      greeting := "Hey"
      stdout.println(greeting)

      // : is shorthand for :=
      greeting: "Yo"
      stdout.println(greeting)

      // Another variable: <- first, then :=
      counter <- 0
      counter := counter + 1
      counter := counter + 1
      stdout.println(`Counter: ${counter}`)

Unlike Java and Python where = does both declaration and reassignment, EK9 separates them: <- declares a new variable, := updates an existing one. This prevents accidental shadowing.

arrow
assign
create
declare
first time
update

Explanation: The first use of a variable must be '<-' to create it; using ':=' on a name that does not exist yet is not resolved. See ek9 -h E50001 for details.


Question: How do I declare a variable inside an if condition in EK9?

What is the if v <- expr pattern in EK9?

How do guards combine declaration and null checking in EK9?

How do I skip a block when a value is unset in EK9?

Answer:
Use the guard pattern: 'if v <- expression'. This declares v AND checks if it is SET. The block only runs if the value is set.

SYNTAX:
  if variableName <- someExpression()
    // only runs if someExpression() returned a SET value
    // variableName is available here

The <- inside an if is a GUARD DECLARATION. It combines:
1. Declare a new variable
2. Call the expression
3. Check if the result is SET (not unset/absent)
4. Only enter the block if SET

WITH ADDITIONAL CONDITION:
  if v <- getValue() then v > 0
    // runs if getValue() is SET AND v > 0

WITH ELSE:
  if result <- compute()
    stdout.println(result)
  else
    stdout.println("compute returned unset")

This replaces null checks in other languages:
  Java:   if (x != null) { use(x); }
  Kotlin: x?.let { use(it) }
  Rust:   if let Some(x) = expr { use(x); }
  EK9:    if x <- expr()
            use(x)

See Q74 for more guard patterns. See Q971 for guard details.

defines module qa.controlflow.guarddeclaration

  defines function

    <?-
      May return a set or unset String.
    -?>
    lookupUser() as pure
      -> userId as Integer
      <- rtn as String: String()

      if userId > 0
        rtn: `User-${userId}`

  defines program

    GuardDeclarationDemo()
      stdout <- Stdout()

      // Guard: declares 'user' and only enters if SET
      if user <- lookupUser(1)
        stdout.println(`Found: ${user}`)

      // Guard with unset result: else branch runs
      if user <- lookupUser(0)
        stdout.println("Should not print")
      else
        stdout.println("User not found - guard skipped block")

      // Guard with additional condition
      minId <- 1
      if user <- lookupUser(5) then user.length() > minId
        stdout.println(`Valid user: ${user}`)

Go: if err := f(); err != nil. Rust: if let Some(v) = expr. Swift: if let v = optional. Kotlin: val v = expr; if (v != null). EK9: if v <- expr() — one line, compile-time enforced.

control flow
guard
if declaration
isSet check
null safety

Explanation: 'null' does not exist in EK9 — return the tri-state unset value String() instead of a null literal. See ek9 -h E01073 for details.


Question: Show me a practical EK9 example using both <- and := together.

How do <- and := work together in a real EK9 program?

Show me EK9 code where I declare with <- then update with :=

Can you demonstrate the <- then := pattern in EK9?

What does a typical EK9 function look like with <- and :=?

Answer:
Here is a practical example showing <- for first declaration and := for subsequent updates. The pattern is always: <- once to create, then := (or : or =) to update.

The function declares 'total' with <- then updates it with := inside the loop. The program declares variables with <- and passes them to the function.

defines module qa.gettingstarted.arrowequals

  defines function

    <?-
      Sums a list of integers.
      Uses <- to declare, := to update.
    -?>
    sumList()
      -> numbers as List of Integer
      <- rtn as Integer: 0

      for number in numbers
        rtn := rtn + number

  defines program

    ArrowEqualsDemo()
      stdout <- Stdout()

      // <- declares new variables
      scores <- [85, 92, 78, 95, 88]
      total <- sumList(scores)
      stdout.println(`Total: ${total}`)

      // := updates existing variable
      label <- "Scores"
      label := label + " (5 items)"
      stdout.println(label)

      // :=? only assigns if unset
      fallback <- String()
      fallback :=? "No scores available"
      stdout.println(`Fallback: ${fallback}`)

Go: total := 0 then total = total + item. Python: total = 0 then total = total + item (ambiguous). EK9: total <- 0 then total := total + item (explicit separation).

<- and := together
declaration then assignment

Explanation: The first use of 'total' must declare it with <- ; using := on a variable that does not yet exist leaves it unresolved. See ek9 -h E50001 for details.


Question: Can guard expressions be used in switch and while, not just if?

Where can I use the <- guard pattern in EK9?

Does the guard declaration work in switch statements?

Is the if v <- expr guard universal across EK9 control flow?

Answer:
Yes. Guard declarations work IDENTICALLY in if, switch, while, do-while, and try. The same 'v <- expression' pattern means 'declare v, check if SET, only proceed if SET' everywhere.

IN IF:
  if name <- findUser(id)
    stdout.println(name)

IN SWITCH:
  switch record <- database.get(id) with record
    case .type == "USER"
      processUser(record)
    default
      processOther(record)

IN WHILE:
  while item <- iterator.next()
    process(item)

IN TRY:
  try connection <- openDatabase()
    query(connection)
  catch
    -> ex as Exception
    logError(ex)

The guard pattern is universal — one syntax to learn, works everywhere. This eliminates 90-95% of null pointer exceptions across ALL control flow.

See Q971 for if guard details. See Q75 for switch guard details.

defines module qa.controlflow.guardcontexts

  defines function

    <?-
      Returns a set or unset value based on input.
    -?>
    fetchValue() as pure
      -> key as String
      <- rtn as String: String()

      if key?
        rtn: "Value for " + key

  defines program

    GuardContextsDemo()
      stdout <- Stdout()

      // Guard in IF
      if greeting <- fetchValue("hello")
        stdout.println(`IF guard: ${greeting}`)

      // Guard in IF with else
      if missing <- fetchValue(String())
        stdout.println("Should not print")
      else
        stdout.println("IF guard: skipped because unset")

      // Guard in SWITCH
      switch found <- fetchValue("test") with found
        case == "Value for test"
          stdout.println(`SWITCH guard: exact match`)
        default
          stdout.println(`SWITCH guard: ${found}`)

Go: if err := f(); err != nil — only works in if. Rust: if let — only works in if/while. Swift: if let — only in if. EK9: guards work in if, switch, while, do-while, and try — truly universal.

guard
switch guard
try guard
universal
while guard

universal guard pattern
guards in all control flow

Explanation: 'null' does not exist in EK9; check a value with the '?' isSet operator, not '!= null'. See ek9 -h E01073 for details.


Question: Are record fields public or private in EK9?

Can I access record fields directly in EK9?

Why do class fields need accessor methods but record fields don't?

How do I access data in a record vs a class in EK9?

What is the visibility difference between records and classes?

Answer:
Record fields are PUBLIC — accessed directly by name.
Class fields are PRIVATE — accessed only through methods or operators.

RECORD (public fields):
  defines record
    Coordinate
      latitude as Float: 0.0
      longitude as Float: 0.0

  // Access directly:
  point <- Coordinate(51.5, -0.1)
  stdout.println(`Lat: ${point.latitude}`)    direct field access

CLASS (private fields):
  defines class
    Location
      latitude as Float: 0.0

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

  // Must use accessor method:
  loc <- Location(51.5)
  stdout.println(`Lat: ${loc.latitude()}`)    method call, not field

Records are for DATA — fields visible, auto-generated operators.
Classes are for BEHAVIOUR — fields hidden, explicit methods.

Records can have constructors and operators but NOT methods.
Classes can have constructors, methods, AND operators.

See Q900 for full visibility comparison. See Q876 for record basics.

defines module qa.classesandoop.recordpublicclass

  defines record

    <?-
      Record fields are PUBLIC — direct access.
    -?>
    Point
      x as Float: 0.0
      y as Float: 0.0

      Point()
        ->
          x as Float
          y as Float
        this.x :=: x
        this.y :=: y

      default operator

  defines class

    <?-
      Class fields are PRIVATE — accessor methods required.
    -?>
    Circle
      radius as Float: 0.0

      Circle()
        ->
          radius as Float
        this.radius :=: radius

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

      default operator

  defines program

    VisibilityDemo()
      stdout <- Stdout()

      // Record: direct field access (PUBLIC)
      point <- Point(3.0, 4.0)
      stdout.println(`Point x: ${point.x}, y: ${point.y}`)

      // Class: must use accessor method (PRIVATE)
      circle <- Circle(5.0)
      stdout.println(`Radius: ${circle.radius()}`)

Java records: fields are private, accessed via generated accessors. Kotlin data classes: properties are public by default. Python dataclasses: attributes are public. EK9 records: fields are PUBLIC (direct access). EK9 classes: fields are PRIVATE (method access only).

accessor class field private public record visibility

record field visibility
class field visibility

Explanation: Class fields are private in EK9, so reading circle.radius directly is not accessible from this context — use the accessor method circle.radius(). See ek9 -h E06180 for details.


Question: How do I filter a list and print the results to stdout in EK9?

Show me a stream pipeline that filters and outputs to stdout

How do I use cat, filter, and > stdout together?

What is the basic stream-to-stdout pattern in EK9?

Answer:
Use a stream pipeline: cat source | filter by predicate > stdout

The > operator sends stream results to stdout, like Unix pipe redirect.

PATTERN:
  cat collection | filter by checkFunction > stdout

The pipeline reads items from the collection, keeps only those passing the filter, and sends each to stdout.

defines module qa.streams.filtertostdout

  defines function

    isHighScore() as pure
      -> score as Integer
      <- rtn as Boolean?

      threshold <- 80
      rtn: score > threshold

  defines program

    FilterToStdoutDemo()
      stdout <- Stdout()

      scores <- [45, 92, 78, 95, 63, 88, 71, 99]

      stdout.println("High scores:")
      cat scores | filter by isHighScore > stdout

Java Streams: list.stream().filter(x -> x > 5).forEach(System.out::println). EK9: cat scores | filter by isHigh > stdout — shorter, no lambda syntax.

stream filter to stdout
cat filter redirect

Explanation: EK9 streams use Unix pipe syntax: 'cat source | filter by fn > stdout'. There is no .stream() method or Java-style method chaining. See ek9 -h stream.


Question: How do I transform items and collect the results in EK9?

Show me cat with map and collect as in EK9

How do I apply a function to every item in a list using streams?

Answer:
Use map with to transform each item, then collect as to gather results:

  cat names | map with toUpper | collect as List of String

The map operation applies a function to each element. The collect terminal materializes the stream into a collection.

defines module qa.streams.mapcollect

  defines function

    formatTemperature() as pure
      -> celsius as Float
      <- rtn as String: `${celsius}C`

  defines program

    MapCollectDemo()
      stdout <- Stdout()

      readings <- [18.5, 22.3, 15.0, 28.7, 20.1]

      labels <- cat readings
        | map with formatTemperature
        | collect as List of String

      stdout.println($labels)

Java: stream.map(String::toUpperCase).collect(Collectors.toList()). EK9: cat names | map with toUpper | collect as List of String.

stream map collect
stream transformation

Explanation: EK9 streams use Unix pipe syntax, not Java method chaining. 'stream()' and 'map()' are not methods on List. Write 'cat items | map with fn | collect as Type'.


Question: How do I sort a stream and take the top N items in EK9?

Show me sort by and head in a stream pipeline

How do I get the first 3 items after sorting in EK9?

What replaces break in EK9 stream loops?

Answer:
Use sort by to order items, then head N to take the first N:

  cat prices | sort by ascending | head 3 > stdout

head N replaces the break-after-N pattern from other languages. The pipeline stops after emitting N items.

Use > stdout to print results, or collect as to gather into a collection.

defines module qa.streams.sorthead

  defines function

    compareByAmount() as pure
      ->
        left as Float
        right as Float
      <- rtn as Integer: left <=> right

  defines program

    SortHeadDemo()
      stdout <- Stdout()

      expenses <- [45.99, 12.50, 89.00, 23.75, 67.30, 8.99, 155.00]

      stdout.println("Top 3 expenses:")
      cat expenses
        | sort by compareByAmount
        | head 3
        > stdout

Java: stream.sorted().limit(3). Python: sorted(items)[:3]. EK9: cat items | sort by comparator | head 3.

break replacement
head
pipeline
sort
stream
top N

      for expense in expenses
        if count > 3
          break

Explanation: EK9 has no break statement (E01070). Use 'head N' in a stream pipeline to take the first N items. See ek9 -h E01070.


Question: How do reject and tee work in EK9 stream pipelines?

What is the opposite of filter in an EK9 stream?

How do I copy stream items to a side collection while continuing the pipeline?

Answer:
reject is the inverse of filter — it removes matching items.
tee copies items to a side collection while continuing the pipeline.

  cat items | reject by isBlank | tee in backup | head 5 > stdout

reject by predicate: removes items where predicate returns true (opposite of filter by).
tee in collection: copies each item into the collection AND continues the pipeline.

defines module qa.streams.rejecttee

  defines function

    isBelowThreshold() as pure
      -> measurement as Float
      <- rtn as Boolean?

      minimumReading <- 10.0
      rtn: measurement < minimumReading

  defines program

    RejectTeeDemo()
      stdout <- Stdout()

      readings <- [5.2, 15.8, 3.1, 22.4, 9.7, 18.3, 7.0]

      validReadings <- List() of Float

      stdout.println("Valid readings (above threshold):")
      cat readings
        | reject by isBelowThreshold
        | tee in validReadings
        > stdout

      stdout.println(`Saved ${validReadings.length()} valid readings`)

Java: no reject (use filter with negation). No tee (use peek with side-effect). EK9: reject is first-class, tee is explicit side-copy.

      cat readings
        | filter by not isBelowThreshold
        > stdout

Explanation: Use 'reject by' to remove matching items. 'filter by not' is not valid syntax. reject is the inverse of filter.