Question: How do guards work in a do/while loop, in both statement and expression form?

Can a do-while loop use a guard variable?

How do I use ?= or :=? in a do/while loop?

Answer:
The do/while loop accepts a guard in its header (after 'do'), and — like the other loops — it has an expression form that yields a value. The guard is checked once at entry; if it leaves the guard variable unset the body is skipped, even though a do/while normally runs its body at least once.

DO/WHILE STATEMENT WITH A ?= GUARD
  first as String?
  more <- true
  do first ?= fetchRecord()
    stdout.println(`got ${first}`)
    more: false
  while more
The guarded assignment (?=) assigns fetchRecord() to 'first' and checks it is SET before the body runs.

DO/WHILE EXPRESSION WITH A :=? GUARD
The expression form declares a returning variable as the first body line and yields its final value:
  cached as String?
  loopOnce <- true
  summary <- do cached :=? fetchRecord()
    <- rtn as String: "none"
    rtn: cached
    loopOnce: false
  while loopOnce
Assign-if-unset (:=?) only assigns when 'cached' is currently unset. If the guard leaves it unset, the body is skipped and 'summary' is the initial value "none".

GUARD POSITION
The guard is the pre-flow, written directly after 'do'; the loop control stays where it belongs, after the body in the trailing 'while' clause.

See Q67 for the plain do/while. See Q77 for while guards. See Q1354 for the while expression form with a guard. See Q1361 for guards used uniformly across all expression-form constructs.

defines module qa.controlflow.guarddowhile

  defines function

    fetchRecord()
      <- rtn as String: "record-A"

  defines program

    GuardDoWhileDemo()
      stdout <- Stdout()

      // do/while STATEMENT with a ?= guard (guard checked once, after 'do')
      first as String?
      more <- true
      do first ?= fetchRecord()
        stdout.println(`got ${first}`)
        more: false
      while more

      // do/while EXPRESSION with a :=? assign-if-unset guard
      cached as String?
      loopOnce <- true
      summary <- do cached :=? fetchRecord()
        <- rtn as String: "none"
        rtn: cached
        loopOnce: false
      while loopOnce
      stdout.println(`summary is ${summary}`)

Java: do-while is a statement only; it always runs the body once and cannot gate the first iteration on a guarded, loop-scoped variable, nor produce a value. You would use a nullable variable plus a manual check. Kotlin/Swift/Rust: do-while (or repeat-while) is likewise a statement with no guard binding and no value form. EK9: do/while accepts <-, ?= and :=? guards that can skip even the first iteration when unset, and has an expression form that yields the returning variable.

:=? ?= control do do-while expression flow guard guarded isset loop returning while

do-while loop
guard variable
guarded assignment

do
        first ?= fetchRecord()
        stdout.println(`got ${first}`)
      while more

Explanation: The guard belongs in the header, directly after 'do' (do first ?= fetchRecord()), not as the first statement of the body. As a guard it can skip the whole body when unset. See ek9 -h E02001.


Question: How do I use a guard in a for-range loop, including the expression form?

Can a for i in 1 ... n loop use a guard variable?

for-range as an expression with a ?= guard

guarded assignment in a numeric for loop

How do I put a guard before the range in a for loop?

Answer:
A for-range loop ('for i in start ... end') accepts a guard in its header, written BEFORE the loop variable and separated by 'with' (or 'then'). The for loop also has an expression form that yields a value via a returning variable.

FOR-RANGE EXPRESSION WITH A ?= GUARD
The guard comes first, then 'with', then the loop variable and range:
  step as Integer?
  total <- for step ?= getStep() with i in 1 ... 5
    <- rtn as Integer: 0
    rtn: rtn + (i * step)
If getStep() is unset the guarded assignment leaves 'step' unset, the loop body is skipped, and 'total' is the initial value 0.

FOR-RANGE STATEMENT WITH A :=? GUARD
  factor as Integer?
  sum <- 0
  for factor :=? getStep() with i in 1 ... 3
    sum: sum + (i * factor)

HEADER ORDER
The grammar is 'for <guard> with <var> in <range>'. The guard variable ('step', 'factor') is SEPARATE from the loop variable ('i'). Putting the guard after the range ('for i in 1 ... 5 with step ?= ...') is NOT valid.

BY STEP
A for-range may add 'by <step>' for non-unit or descending ranges; the guard still comes first: 'for g <- getG() with i in 10 ... 1 by -1'.

See Q64 for the plain for-range. See Q1149 for 'by'. See Q1357 for for-in guards. See Q1361 for guards across all expression-form constructs.

defines module qa.controlflow.guardforrangeexpression

  defines function

    getStep()
      <- rtn as Integer: 2

  defines program

    GuardForRangeExpressionDemo()
      stdout <- Stdout()

      // for-range EXPRESSION with a ?= guard (guard FIRST, then 'with i in range')
      step as Integer?
      total <- for step ?= getStep() with i in 1 ... 5
        <- rtn as Integer: 0
        rtn: rtn + (i * step)
      stdout.println(`weighted total is ${total}`)

      // for-range STATEMENT with a :=? guard
      factor as Integer?
      sum <- 0
      for factor :=? getStep() with i in 1 ... 3
        sum: sum + (i * factor)
      stdout.println(`sum is ${sum}`)

Java: a for loop is a statement and cannot bind a guarded, isSet-checked variable in its header nor yield a value. You would compute a nullable outside the loop and check it manually. Python/Kotlin: for-comprehensions/folds produce values but have no isSet-guard concept. EK9: the for-range header takes a guard (<-, ?=, :=?) before the loop variable, and its expression form yields the returning variable — with the guard able to skip the whole loop when unset.

:=? ?= by expression for for-range guard guarded isset loop range returning with

for range loop
guard variable
for expression

total <- for i in 1 ... 5 with step ?= getStep()
        <- rtn as Integer: 0
        rtn: rtn + (i * step)

Explanation: The guard is the pre-flow and must come BEFORE the loop variable: 'for <guard> with <var> in <range>'. The guard variable is separate from the loop variable. See ek9 -h E02001.


Question: How do I use a guard in a for-in loop, including the expression form?

Can a for item in collection loop use a guard variable?

guarded assignment when iterating a collection

How do I guard a for-each loop that returns a value?

Answer:
A for-in loop ('for item in collection') accepts a guard in its header, written BEFORE the loop variable and separated by 'with' (or 'then'). The for-in loop also has an expression form yielding a value via a returning variable.

FOR-IN EXPRESSION WITH A ?= GUARD
  prefix as String?
  joined <- for prefix ?= getPrefix() with item in items
    <- rtn as String: ""
    rtn: `${rtn}${prefix}${item}`
The guarded assignment (?=) assigns getPrefix() to 'prefix' and checks it is SET. If unset, the loop body is skipped and 'joined' is the initial "". (Note: build strings with interpolation `${...}`, not 3+ chained '+', which EK9 rejects with E11068.)

FOR-IN STATEMENT WITH A :=? GUARD
  marker as String?
  count <- 0
  for marker :=? getPrefix() with item in items
    if marker?
      count: count + 1

GUARD vs LOOP VARIABLE
The guard variable ('prefix', 'marker') is separate from the loop variable ('item'). Order is 'for <guard> with <var> in <collection>'; the guard cannot go after the collection.

See Q65 for the plain for-in. See Q1356 for for-range guards. See Q1361 for guards across all expression-form constructs. See Q76 for guards in for loops overview.

defines module qa.controlflow.guardforinexpression

  defines program

    GuardForInExpressionDemo()
      stdout <- Stdout()
      items <- ["alpha", "beta", "gamma"]

      // for-in EXPRESSION with a ?= guard (guard FIRST, then 'with item in collection')
      prefix as String?
      joined <- for prefix ?= getPrefix() with item in items
        <- rtn as String: ""
        rtn: `${rtn}${prefix}${item}`
      stdout.println(`joined: ${joined}`)

      // for-in STATEMENT with a :=? guard
      marker as String?
      count <- 0
      for marker :=? getPrefix() with item in items
        if marker?
          count: count + 1
      stdout.println(`count is ${count}`)

  defines function

    getPrefix()
      <- rtn as String: "> "

Java: enhanced-for is a statement; it cannot bind a guarded, isSet-checked header variable nor produce a value. Kotlin/Swift: for-each is a statement; value production needs map/fold. EK9: for-in takes a guard (<-, ?=, :=?) before the loop variable, and its expression form yields the returning variable, with the guard able to skip iteration entirely when unset.

:=? ?= collection expression for for-each for-in guard guarded isset iterate returning with

for in loop
guard variable
for expression

joined <- for item in items with prefix ?= getPrefix()
        <- rtn as String: ""
        rtn: `${rtn}${prefix}${item}`

Explanation: The guard is the pre-flow and must come BEFORE the loop variable: 'for <guard> with <var> in <collection>'. See ek9 -h E02001.


Question: How do I use a ?= or :=? guard in a try, in both statement and expression form?

Can a try/catch use a guarded assignment in its header?

try as an expression with a guard variable

How do I combine a guard with a try that returns a value?

Answer:
A try accepts a guard in its header (directly after 'try'), and has an expression form that yields a value via a returning variable. If the guard leaves its variable unset, the try body is skipped and the expression yields the returning variable's initial value.

TRY EXPRESSION WITH A ?= GUARD
  config as String?
  chosen <- try config ?= loadConfig()
    <- rtn as String: "default.yaml"
    rtn: config
  catch
    -> ex as Exception
    rtn: `error ${ex}`
The guarded assignment (?=) assigns loadConfig() to 'config' and checks it is SET. If unset, the body is skipped and 'chosen' is "default.yaml".

TRY STATEMENT WITH A :=? GUARD
  cached as String?
  try cached :=? loadConfig()
    stdout.println(`loaded ${cached}`)
  catch
    -> ex as Exception
    stdout.println(`failed: ${ex}`)
Assign-if-unset (:=?) only assigns when 'cached' is currently unset, then gates the body on the result.

GUARD POSITION
The guard is the pre-flow, written right after 'try' — 'try <guard>' — before the body, resource header ('-> r <- ...') and returning variable. catch and finally are unaffected.

See Q78 for try guards. See Q1157 for try guard expressions. See Q137 for try-with-resources. See Q1361 for guards across all expression-form constructs.

defines module qa.controlflow.guardtryexpression

  defines function

    loadConfig()
      <- rtn as String: "prod.yaml"

  defines program

    GuardTryExpressionDemo()
      stdout <- Stdout()

      // try EXPRESSION with a ?= guard (guard right after 'try')
      config as String?
      chosen <- try config ?= loadConfig()
        <- rtn as String: "default.yaml"
        rtn: config
      catch
        -> ex as Exception
        rtn: `error ${ex}`
      stdout.println(`chosen config: ${chosen}`)

      // try STATEMENT with a :=? assign-if-unset guard
      cached as String?
      try cached :=? loadConfig()
        stdout.println(`loaded ${cached}`)
      catch
        -> problem as Exception
        stdout.println(`failed: ${problem}`)

Java: try is a statement, cannot bind a guarded loop-scoped variable in its header, and cannot yield a value; finally cannot set a result. You would null-check before the try. EK9: try takes a guard (<-, ?=, :=?) right after 'try', has an expression form yielding the returning variable, and the guard can skip the body entirely when unset.

:=? ?= catch control exception expression flow guard guarded isset returning try

try expression
guard variable
guarded assignment

Explanation: The guard belongs in the header directly after 'try' (try config ?= loadConfig()), not as the first statement of the body. As a guard it gates the whole body on the guard variable being set. See ek9 -h E02001.


Question: How do I use a :=? assign-if-unset guard in a switch expression?

switch expression with a guarded assignment

Can a switch that returns a value lazily initialise its control variable?

How do I default the switch control with :=? before matching?

Answer:
The switch (or 'given') expression accepts a guard in its header. The guard runs once, before matching; if it leaves the guard variable unset the whole switch body is skipped and the expression yields the returning variable's initial value.

SWITCH EXPRESSION WITH A :=? GUARD
Assign-if-unset (:=?) sets the control variable only when it is currently unset, then the switch matches on it:
  tier as String?
  discount <- switch tier :=? lookupTier() with tier
    <- rtn as Integer: 0
    case "gold"
      rtn: 20
    case "silver"
      rtn: 10
    default
      rtn: 5
If lookupTier() is unset, 'tier' stays unset, the body is skipped, and 'discount' is the initial 0.

SWITCH STATEMENT WITH A :=? GUARD
The same guard works without a returning variable:
  level as String?
  switch level :=? lookupTier() with level
    case "gold"
      stdout.println("premium")
    default
      stdout.println("standard")

HEADER SHAPE
'switch <guard> with <control>' — the guard is the pre-flow; the value being matched comes after 'with'. Contrast ?= (guarded assignment, always assigns then checks) with :=? (assigns only when unset).

See Q75 for switch guards. See Q837 for the ?= guard in a switch expression. See Q68 for the switch expression. See Q1361 for guards across all expression-form constructs.

defines module qa.controlflow.guardswitchexpression

  defines function

    lookupTier()
      <- rtn as String: "gold"

  defines program

    GuardSwitchExpressionDemo()
      stdout <- Stdout()

      // switch EXPRESSION with a :=? assign-if-unset guard
      tier as String?
      discount <- switch tier :=? lookupTier() with tier
        <- rtn as Integer: 0
        case "gold"
          rtn: 20
        case "silver"
          rtn: 10
        default
          rtn: 5
      stdout.println(`discount is ${discount}`)

      // switch STATEMENT with a :=? guard
      level as String?
      switch level :=? lookupTier() with level
        case "silver"
          stdout.println("standard tier")
        default
          stdout.println("premium tier")

Java: switch is a statement (or, since 14, an expression) but has no header guard and no isSet/assign-if-unset concept; you would default the variable in a separate statement. EK9: the switch/given expression takes a guard (<-, ?=, :=?) that can lazily initialise the control and skip the whole switch when unset, while still yielding the returning variable.

:=? assign case control default expression given guard isset returning switch unset

switch expression
guarded assignment
guard variable

discount <- switch tier :=? lookupTier() with tier
        <- rtn as Integer: 0
        case "gold"
          rtn: 20
        default
          rtn: 5

discount <- switch tier with tier :=? lookupTier()
        <- rtn as Integer: 0
        case "gold"
          rtn: 20
        default
          rtn: 5

Explanation: The guard is the pre-flow and comes first; the matched control follows 'with'. Order is 'switch <guard> with <control>'. See ek9 -h E02001.


Question: What is the ternary operator in EK9 and how do its ':' and 'else' forms differ?

conditional value selection without a switch

difference between ternary ':' and 'else'

Answer:
EK9 has no if-EXPRESSION: 'if' is always a statement. When you want a value chosen by a condition, use the TERNARY operator (or a switch expression). The ternary is: 'condition <- valueIfTrue <separator> valueIfFalse', where the separator is either ':' or the keyword 'else' — the two forms are IDENTICAL in meaning.

TERNARY WITH ':'
  passMark <- 90
  grade <- score >= passMark <- "A" : "B"
Reads: if 'score >= passMark' then "A" else "B". (Comparison literals must be named — a bare 'score >= 90' is rejected with E11064 — so bind the threshold first.)

TERNARY WITH 'else'
  grade <- score >= passMark <- "A" else "B"
Identical result; choose whichever reads better. 'else' is often clearer in longer expressions; ':' is more compact.

ONLY THE SELECTED ARM IS PRODUCED
The condition, and each arm, are ordinary expressions; only the chosen arm's value becomes the result.

TERNARY IS NOT COALESCING
The ternary chooses on a Boolean condition. It is distinct from the coalescing operators, which choose on set-state: '?:' (isSet coalescing, 'a ?: b' yields a if a is set else b). Use a ternary for a genuine Boolean test; use '?:' when you mean 'a, or b if a is unset'.

WHY NOT if-as-expression
'if' stays a statement so that all value production is explicit (ternary or switch), keeping branch side-effects and value-selection visually separate.

See Q61 for the if statement. See Q68 for the switch expression (the other value-producing conditional). See Q243 for coalescing operators. See Q898 for isSet vs ternary.

defines module qa.controlflow.ternarycolonelse

  defines program

    TernaryColonElseDemo()
      stdout <- Stdout()
      score <- 85
      passMark <- 90

      // ternary with ':' — condition <- valueIfTrue : valueIfFalse
      grade <- score >= passMark <- "A" : "B"
      stdout.println(`grade (colon form): ${grade}`)

      // ternary with 'else' — identical meaning, different separator keyword
      band <- score >= passMark <- "high" else "normal"
      stdout.println(`band (else form): ${band}`)

Java: 'cond ? a : b' — EK9's ':' form is the direct analogue but the condition is separated by '<-' ('cond <- a : b'). Java has no 'else' keyword variant. Python: 'a if cond else b' mirrors EK9's 'else' form. Kotlin/Rust/Swift: 'if' is itself an expression; EK9 deliberately keeps 'if' a statement and offers the ternary (and switch expression) for value selection so branch effects and value selection stay separate.

coalescing colon conditional control else expression flow if-expression select ternary value

ternary operator
conditional expression
if expression

passMark <- 90
      grade <- score >= passMark <- "A" : "B"

grade <- if score >= 90 then "A" else "B"

Explanation: EK9 has no if-expression. Use the ternary 'condition <- valueIfTrue : valueIfFalse' (or 'else' instead of ':'), or a switch expression. 'if' is a statement only. See ek9 -h E02001.


Question: Do guard variables work the same way in every expression-form control-flow construct?

Is the guard syntax uniform across while, for, switch and try expressions?

Can I use the same ?= guard in a while, for, switch and try that all return values?

guards across all control flow expression forms

one guard pattern for every value-producing construct

Answer:
Yes. EK9 offers a UNIFORM guard mechanism across all six expression-form control-flow constructs: while, do/while, for-range, for-in, switch/given, and try. In every one, a guard in the header (<- declaration, ?= guarded assignment, or :=? assign-if-unset) is evaluated once at entry; if it leaves the guard variable unset the whole body is skipped and the expression yields the returning variable's initial value. The same three operators, the same skip-on-unset semantics, the same 'returning variable must be initialised' rule — everywhere.

THE SAME ?= GUARD, SIX CONSTRUCTS (each yields source() when set):
  while:     wResult <- while g ?= source() then loop ...
  do/while:  dResult <- do g ?= source() ... while loop
  for-range: rResult <- for g ?= source() with i in 1 ... 1 ...
  for-in:    fResult <- for g ?= source() with item in [2] ...
  switch:    sResult <- switch g ?= source() with g ...
  try:       tResult <- try g ?= source() ...

HEADER POSITION
The guard is always the pre-flow, written FIRST: after 'while'/'do'/'for'/'switch'/'try', before the construct's own control ('then'/'with' condition, range, collection, or trailing 'while'). It is never placed after the control.

IF IS THE EXCEPTION
'if' has no expression form — its value-producing counterpart is the ternary ('cond <- a : b'). Every OTHER construct has both statement and expression forms, and the guard behaves identically in both.

WHY UNIFORM
One rule to learn, no per-construct special cases, and the returning-variable-initialised requirement means a guard-skipped body can never yield an undefined value. This is enforced by the compiler (E08050).

See Q1354 (while), Q1355 (do-while), Q1356 (for-range), Q1357 (for-in), Q1359 (switch), Q1358 (try) for each construct in detail. See Q74 for the three guard operators. See Q1360 for the ternary (the if-expression equivalent).

defines module qa.controlflow.guardsuniform

  defines function

    source()
      <- rtn as Integer: 7

  defines program

    GuardsUniformDemo()
      stdout <- Stdout()

      // The SAME ?= guard, in each of the six expression-form constructs.
      // source() is set, so every body runs once and the result is 7.

      // The SAME ?= guard, with the SAME skip-on-unset semantics and the SAME
      // 'returning variable must be initialised' rule, shown in three representative
      // expression forms. source() is set, so each body runs once and yields 7.
      // (do-while, for-in and switch behave identically — see Q1355/Q1357/Q1359.)

      // while expression: guard, then 'then <control>'
      loopFlag <- true
      whileGuard as Integer?
      whileValue <- while whileGuard ?= source() then loopFlag
        <- rtn as Integer: -1
        rtn: whileGuard
        loopFlag: false

      // for-range expression: guard, then 'with <var> in <range>'
      rangeGuard as Integer?
      rangeValue <- for rangeGuard ?= source() with i in 1 ... 1
        <- rtn as Integer: -2
        require i?
        rtn: rangeGuard

      // try expression: guard right after 'try'
      tryGuard as Integer?
      tryValue <- try tryGuard ?= source()
        <- rtn as Integer: -3
        rtn: tryGuard

      stdout.println(`while=${whileValue} forRange=${rangeValue} try=${tryValue}`)

No mainstream language offers a single guard mechanism that is uniform across all loop, switch and try constructs AND lets each yield a value. Rust has 'while let'/'if let' but not for switch/try and they are statements; Swift has 'if let'/'guard let' but not across loops as value-producers. EK9 unifies declaration/guarded-assignment/assign-if-unset guards over while, do-while, for-range, for-in, switch and try, each with a value-producing expression form and a compiler-guaranteed initialised result.

:=? ?= consistent control do-while expression flow for guard returning switch try uniform while

guard variable
expression form
control flow uniformity

wResult <- while g ?= source() then loop
        <- rtn as Integer: -1
        rtn: g
        loop: false

wResult <- while g ?= source() then loop
        <- rtn as Integer?
        rtn: g
        loop: false

Explanation: In every expression form the returning variable must be INITIALISED so a guard-skipped body still yields a defined value. Declaring it uninitialised ('<- rtn as Integer?') is rejected by E08050. See ek9 -h E08050.


Question: What is the input-variation (input-variety) metric in EK9?

What do the input-variety floor and ceiling mean?

How does EK9 measure test input variety?

What is the type-variety rollup shown in the complexity hover?

Why does my method show an input-variety floor and ceiling?

Answer:
The input-variation metric scores whether tests drive a reasonable VARIETY of input value-classes through each public construct. It is a floor detector, not a correctness certificate.

CODE COVERAGE IS NOT CORRECTNESS
EK9 also has code coverage built into the compiler and the -t test runner (the E83001 publish gate). But 100% code coverage only means every line and branch executed at least once during the tests - it does NOT prove the function is functionally correct, and it does NOT mean the inputs were varied. You can reach 100% coverage on multiply(Integer, Integer) with a single multiply(2, 2) call: every line runs, coverage is green, yet negatives, zero, overflow (min/max) and unset were never tried. Coverage answers 'did the code run?'; input-variety answers 'across how many meaningfully-different input classes did it run?'. The two are complementary, and neither certifies correctness - even full coverage AND full variety cannot prove that an assertion's expected value is actually right (that is what mutation testing, EK9's eventual stronger gate, targets). Input-variety exists to catch the common blind spot: green coverage masking a thin, low-variety test suite - the classic multiply(2, 2) case.

VALUE-CLASSES
Each parameter type admits a set of value-classes. Integer has 9 (unset, zero, negative, positive, typical, min, max, near-min, near-max), Boolean 3 (true/false/unset), String 7 (heuristic), a user enumeration = its members + unset. These are derived structurally from the type, no literals.

TWO FIGURES PER CALLABLE
FLOOR (the headline) = the 1-wise 'each-choice' count = the sum of each input's class-count. It asks: was each value-class of each input exercised at least once? For combine(Integer, Integer) the floor is 9 + 9 = 18 (reachable by about 9 well-chosen tests). This is the number the optional publish gate is built around.
CEILING (informational) = the pairwise opportunity = the sum over input pairs of classCount x classCount. For combine it is 9 x 9 = 81. It is an upper bound, never a test target - good 1-wise testing only reaches around 11% of it.

RECORDS DECOMPOSE TO FIELDS
A record is transparent, so it contributes its fields as separate leaf dimensions, never an atomic product. A Point{x, y} used by an operator is scored on its x and y leaves - so packaging fields into a record does not change the number, and Point.<=>(Point) is floor 36 / ceiling 486, not 81 x 81.

RECEIVER COUNTS
An instance method is scored against its RECEIVER's state too (the receiver is an implicit input), so a no-arg pop() or isSet() is measured against the states its object can be in - not treated as having no inputs.

PER-TYPE ROLLUP
A class/record/component/trait also shows a rollup summing its whole surface (constructors + methods), so you see how well the type as a whole is variety-tested, not just one member.

WHERE TO SEE IT
The MCP tool ek9_query_complexity in inputVariety mode lists every callable's floor and ceiling plus the per-type rollup; the IDE complexity hover shows the same figures per construct. A low floor means under-tested (or the type over-partitions its inputs); it never proves correctness. See Q811 for built-in code coverage, Q312 for the complexity metrics, and Q311 for the quality checks catalog.

defines module qa.codequality.inputvariety

  defines function

    <?-
      Two Integer parameters admit 9 value-classes each, so the input-variation
      floor is 9 + 9 = 18 (exercise each class of each input once) and the pairwise
      ceiling is 9 x 9 = 81 (the informational upper bound).
    -?>
    combine() as pure
      ->
        left as Integer
        right as Integer
      <- rtn as Integer: left + right

  defines record

    <?-
      A record decomposes into its field leaves (x, y), so it is scored on its
      fields rather than as an atomic product - packaging does not change the number.
    -?>
    Point
      x Integer?
      y Integer?

      default private Point()

      Point()
        ->
          initX as Integer
          initY as Integer
        this.x: Integer(initX)
        this.y: Integer(initY)

      operator $ as pure
        <- rtn as String: `${x},${y}`

      default operator ?

  defines program

    InputVarietyDemo()
      stdout <- Stdout()

      total <- combine(2, 3)
      stdout.println(`${total}`)

      p <- Point(1, 2)
      stdout.println(`${p}`)

Combinatorial/pairwise testing (PICT, ACTS) and mutation testing (PIT for Java, Stryker for JS) all measure input adequacy but as separate, optional external tools run in CI. SonarQube reports coverage but not input-class variety. EK9 builds a cheap, always-on input-variety floor into the compiler alongside the other quality metrics, with mutation testing as the eventual stronger successor.

boundary ceiling correctness coverage each-choice equivalence floor input-variation input-variety metric mutation pairwise quality receiver testing value-class variety

input variety
input-variation metric
test variety


Question: Can I apply a mutating operator like += to a method-call result?

What does repository.items() += entry do?

Can I mutate the collection returned by a method?

Is assigning to a method-call result allowed in EK9?

Why does myObject.getList() += x compile but myObject.getList() := x not?

Answer:
Yes - a MUTATING operator can be applied to a method-call result, but a plain or guarded ASSIGNMENT cannot.

MUTATING OPERATORS ON A CALL RESULT (ALLOWED)
EK9 methods return object references. A mutating operator (+=, -=, *=, /=, :~:, :^:, :=:) applied to a call result mutates the object the call returns IN PLACE:
  repository.items() += entry
This is exactly equivalent to repository.items()._addAss(entry) - the operator spelling of calling a mutating method on the returned object. It is NOT 'reassign the result of items()'; EK9's += never stores back, it always mutates the receiver.

WHEN THE MUTATION PERSISTS (AND WHEN IT DOES NOT)
Whether the mutation is observable depends entirely on WHAT the method returns:
  - Returns the internal field BY REFERENCE  -> the mutation persists. This is a deliberate 'here is my collection, modify it' exposure.
  - Returns a fresh COPY                     -> the mutation is applied to the throwaway copy and is lost.
The caller cannot tell which from the call site, so this is a property of the accessor's contract. It is the same aliasing consideration as any mutating method call on a result (e.g. getListCopy().add(x)).

SIMPLE / GUARDED ASSIGNMENT ON A CALL RESULT (REJECTED - E07895)
A simple (:=, :) or guarded (:=?) assignment needs a storage location to store into. A method-call result is a temporary value, not a location, so there is nothing to assign to:
  repository.items() := List()    // E07895: not an assignable target
Note this differs from assigning to a FIELD reached through a call, which targets a real field slot and IS allowed:
  getRecord().name := "Steve"      // OK - .name is an assignable field

See Q241 for mutation vs pure operators. See ek9 -h E07895 for the assignment restriction.

defines module qa.operators.mutate.call.result

  defines class

    // Returns its internal collection BY REFERENCE: mutations through the accessor persist.
    Basket
      items <- List() of String

      items()
        <- rtn as List of String: items

      default operator ?

    // Returns a fresh COPY each call: mutations through the accessor are lost.
    SafeBasket
      contents <- List() of String

      items()
        <- rtn as List of String: cat contents | collect as List of String

      default operator ?

  defines program

    MutateCallResult()
      stdout <- Stdout()

      // Mutating operator on a call result that returns the internal field -> mutation PERSISTS
      basket <- Basket()
      basket.items() += "apple"
      basket.items() += "pear"
      stdout.println(`reference-return count: ${length basket.items()}`)

      // Same operator on a call result that returns a COPY -> mutation is LOST
      safe <- SafeBasket()
      safe.items() += "apple"
      stdout.println(`copy-return count: ${length safe.items()}`)

Java/Kotlin: obj.getList().add(x) mutates the returned list in place (same aliasing caveat); obj.getList() = x is a compile error (method call is not an lvalue). C++: obj.get() += x works if get() returns a reference (T&) and is meaningless/ill-formed otherwise. EK9: += on a call result is the operator form of a mutating method call and is always allowed; := / :=? on a call result is rejected at compile time (E07895) because there is no assignable target.

E07895 accessor aliasing assignable call collection getter in-place list lvalue method mutating operator reference result return

mutating operator on call result
E07895
returned reference mutation

Explanation: A mutating operator (+=, -=, :~:, :^:, :=:) on a method-call result mutates the returned object in place and is allowed. A simple (:=) or guarded (:=?) assignment has no location to store into - a call result is not an assignable target - so it triggers E07895. See ek9 -h E07895 for details.


Question: How do I read and write CSV in EK9?

How do I parse a comma separated values file?

How do I handle quoted CSV fields that contain commas?

How do I access a CSV cell by column name?

Answer:
CSV is a first-class built-in type in EK9 - a full RFC 4180 reader and writer - so you do not reach for split(',') (which breaks the moment a field is quoted) or an external library.

PARSING RETURNS A RESULT
CSV().parse(text) returns a Result of (CSV, String). A malformed document (for example an unterminated quoted field) becomes an error carrying its reason, and - as with Optional - the compiler REQUIRES you to check isOk() before you call ok():
  result <- CSV().parse(text)
  if result.isOk()
    table <- result.ok()
The separator is configurable via a constructor argument, so CSV(';') or CSV('\t') read semicolon- and tab-separated data.

READING ROWS AND CELLS
A CSV holds a header row and the data rows. Read rows total-style, exactly like List - getOrDefault(index, default) never goes out of range:
  table.rows()                        how many data rows
  table.header()                      the header CSVRow
  row <- table.getOrDefault(0, CSVRow())
A CSVRow is more than a List of String: a data row is header-aware, so a cell can be read by column NAME as well as by position:
  row.getOrDefault(1, "?")            by position
  row.getOrDefault("weight", "?")     by column name

GETTING A REAL TYPE OUT OF A CELL
Cells are text. To get a real type, hand the cell to that type's constructor, which parses it and returns unset if it is not that type - no throwing:
  weight <- BigDecimal(row.getOrDefault("weight", "0"))
  number <- Integer(row.getOrDefault("number", "0"))
This is uniform for every type, including your own.

ITERATING (AND STREAMS)
A CSV iterates its data rows, so a for-in loop - or a full stream pipeline (filter, sort, head ...) - works directly:
  for row in table
    stdout.println(row.getOrDefault("name", "?"))

WRITING
Build a CSV row-by-row and render it back to RFC 4180 text with $ (fields that contain the separator, a quote or a newline are quoted for you):
  out <- CSV()
  head <- CSVRow()
  head += "id"
  head += "label"
  built <- out.header(head)
  row <- CSVRow()
  row += "1"
  row += "has, comma"
  built += row
  csvText <- $built

COMBINING
Stacking two CSVs vertically (+, :~:, +=) is guarded by header alignment - if the two headers are not the same columns the result is unset, so you cannot silently weld mismatched data together.

See Q188 for JSON as a first-class type. See Q23 for basic types. See Q129 for what happens when a key or index is absent (getOrDefault). Use 'ek9 -h CSV' and 'ek9 -h CSVRow' for the full API.

defines module qa.csv.readwrite

  defines program

    CsvReadWrite()
      stdout <- Stdout()

      //Read: a quoted field keeps its comma; parse returns a Result.
      result <- CSV().parse("name,weight\nHydrogen,1.008\nHelium,\"4.0026, approx\"")
      if result.isOk()
        table <- result.ok()

        stdout.println(`rows: ${table.rows()}`)

        //Read a data row and a cell by column name; convert with a constructor.
        helium <- table.getOrDefault(1, CSVRow())
        note <- helium.getOrDefault("weight", "?")
        stdout.println(`helium weight cell: ${note}`)

        firstWeight <- BigDecimal(table.getOrDefault(0, CSVRow()).getOrDefault("weight", "0"))
        stdout.println(`hydrogen weight typed: ${firstWeight}`)

        //Iterate the data rows.
        for row in table
          stdout.println(row.getOrDefault("name", "?"))

      //Write: build a CSV row-by-row and render to RFC 4180 text.
      out <- CSV()
      head <- CSVRow()
      head += "id"
      head += "label"
      built <- out.header(head)

      dataRow <- CSVRow()
      dataRow += "1"
      dataRow += "has, comma"
      built += dataRow

      stdout.print($built)

Java: CSV needs an external library (Apache Commons CSV, OpenCSV); split(',') is a common but broken shortcut. Python: csv.reader (positional) / csv.DictReader (by column name); cells are always str. Go: encoding/csv. Rust: the csv crate with serde. EK9: CSV and CSVRow are built in. parse returns Result of (CSV, String) with compiler-enforced isOk() before ok(); rows read by index or column name with getOrDefault; cells typed via the target constructor; writer via $. Full RFC 4180 (quoted fields, doubled quotes, embedded newlines), configurable separator.

column comma csv data field file getOrDefault header parse quoted read result rfc4180 row separated separator table tsv values write

Explanation: CSV.parse returns a Result of (CSV, String). Like Optional, the compiler enforces a check before access: ok() may only be called after isOk(), and error() only after isError(). Calling ok() (or error()) unguarded triggers E08030. Note the 'else' of an isOk() check does NOT count as an isError() check - a Result can also be empty - so guard error() with its own 'if result.isError()'. See ek9 -h E08030.


Question: How do I use a Set for unique values in EK9?

How do I get the union or difference of two collections?

Answer:
Set of type T is a built-in collection of UNIQUE values. Adding a value that is already present has no effect, so it is the natural tool for de-duplication and fast membership testing.

INSERTION-ORDERED
Unlike a hash set in some languages, an EK9 Set is INSERTION-ORDERED (backed by a linked hash set, just like Dict). Iterating a Set yields values in the order they were first added, which is deterministic and reproducible across runs and JVMs - so output and tests stay stable. Do not rely on any other order.

CREATING AND ADDING
  seen <- Set() of String
  seen += "apple"
  seen += "banana"
  seen += "apple"      // duplicate - no effect
Uniqueness is by VALUE, using the element type's == and #? (hashcode) operators - the same mechanism that makes a value a valid Dict key.

MEMBERSHIP, SIZE, ITERATION
  found <- seen contains "banana"
  count <- seen.length()               // reflects the unique count
  for item in seen                     // first-insertion order
    stdout.println(item)
A Set is always set when created; an empty Set is a valid value, not unset.

UNION AND DIFFERENCE
  combined <- setA + setB              // union
  removed  <- setA - setB              // difference
These also have in-place forms (+=, -=) and merge (:~:).

WHY NOT DICT OF (T, BOOLEAN)?
Before Set, membership was faked with Dict of (T, Boolean). Set says what you mean, iterates in insertion order, and its length is the unique count directly.

List and Dict are the other core collections. Use 'ek9 -h Set' for the full API.

defines module qa.set.unique

  defines program

    SetUnique()
      stdout <- Stdout()

      //A Set holds unique values; a duplicate add has no effect.
      seen <- Set() of String
      seen += "apple"
      seen += "banana"
      seen += "apple"

      count <- seen.length()
      stdout.println(`unique count: ${count}`)

      found <- seen contains "banana"
      stdout.println(`contains banana: ${found}`)

      //Iteration is in first-insertion order (deterministic).
      for item in seen
        stdout.println(item)

      //Union with another Set.
      other <- Set() of String
      other += "cherry"
      other += "date"
      combined <- seen + other
      stdout.println(`union count: ${combined.length()}`)

Java: HashSet (unordered) / LinkedHashSet (insertion order). Python: set (unordered) - EK9 differs by being insertion-ordered. Go: map[T]struct{} idiom. Rust: HashSet / IndexSet. EK9: Set of type T, insertion-ordered by design (like Dict), value-based uniqueness via == and #?, with +/-/+=/-=/contains/length and for-in iteration.

collection contains dedup difference distinct duplicate hashset insertion membership order set union unique

Explanation: Set (like the other built-in generic collections) is CLOSED - it cannot be extended (E05030: not open to be extended). Extending a collection mixes collection mechanics with application logic. Use composition/delegation instead: hold a 'Set of String' as a field and expose the operations you need.


Question: How do I work with very large integers (BigInteger) in EK9?

Is there arbitrary-precision integer arithmetic in EK9?

How do I compute large factorials or unbounded Fibonacci?

Answer:
Integer in EK9 is 64-bit, and on overflow it becomes UNSET rather than wrapping. When you need whole numbers larger than that, use BigInteger - an arbitrary-precision integer that never overflows.

CREATING
  big <- BigInteger("123456789012345678901234567890")
  fromInt <- BigInteger(1000)     // widen from a 64-bit Integer

ARITHMETIC (NEVER OVERFLOWS)
+, -, * and ^ (power) grow the value as needed:
  squared <- big * big
  cubed <- big ^ 3
Comparisons (<, <=, >, >=, ==, <>, <=>), abs and sqrt are available too.

MOD AND REM
EK9 requires the mod and rem operators to return an Integer, so BigInteger.mod / .rem return a 64-bit Integer; if the result would not fit 64 bits it is returned UNSET rather than losing data.

NO PROMOTION TO FLOAT
BigInteger deliberately has NO #^ promote operator. A BigInteger is WIDER than a Float, so an implicit conversion would silently lose precision - exactly what BigInteger exists to avoid. If you really need a floating value, do the conversion explicitly.

DISPLAY
$ gives the full decimal string:
  stdout.println($big)

See Q1367 for exact decimals (BigDecimal). Use 'ek9 -h BigInteger' for the full API.

defines module qa.biginteger.usage

  defines program

    BigIntegerUsage()
      stdout <- Stdout()

      //Arbitrary precision - multiplication never overflows.
      big <- BigInteger("123456789012345678901234567890")
      squared <- big * big
      stdout.println(`squared: ${squared}`)

      //Widen from a 64-bit Integer.
      fromInt <- BigInteger(1000)
      cubed <- fromInt ^ 3
      stdout.println(`cubed: ${cubed}`)

Java: java.math.BigInteger. Python: int is arbitrary precision by default. Go: math/big Int. Rust: num-bigint crate. EK9: BigInteger is built in; +,-,*,^ never overflow (contrast Integer which unsets on overflow); mod/rem return Integer (unset if out of 64-bit range); no #^ promote (a BigInteger is wider than Float).

arbitrary arithmetic big biginteger factorial fibonacci integer large number overflow power precision

BigInteger
arbitrary precision
large integers


Question: How do I get exact decimals (BigDecimal) in EK9?

How do I do high-precision decimal arithmetic?

What do I use for exact decimals that are not money?

Answer:
Float is 64-bit binary floating point, so it cannot represent values like 0.1 exactly (0.1 + 0.2 is not quite 0.3). When you need EXACT decimals, use BigDecimal - arbitrary-precision decimal arithmetic. (For currency specifically, use Money.)

EXACT ARITHMETIC
  sum <- BigDecimal("0.1") + BigDecimal("0.2")   // exactly 0.3
+, -, * and ^ are exact. Create from a String (exact), an Integer, or a Float.

EQUALITY IS BY VALUE, NOT SCALE
Unlike java.math.BigDecimal.equals, EK9 compares by value: 1.0 equals 1.00. ==, <=> and #? all use value comparison, so equal values also hash the same.

DIVISION AND SQRT
A non-terminating quotient like 1/3 needs a bound, so division and sqrt keep 34 significant digits (MathContext DECIMAL128). Division by zero is UNSET.

ROUNDING
  rounded <- BigDecimal("3.14159").round(2)      // 3.14 (HALF_UP)

DISPLAY
$ gives the plain-string form (no scientific notation).

Like BigInteger, BigDecimal has no #^ promote operator. See Q1366 for BigInteger and Q23 for the Money type. Use 'ek9 -h BigDecimal' for the full API.

defines module qa.bigdecimal.usage

  defines program

    BigDecimalUsage()
      stdout <- Stdout()

      //Exact: 0.1 + 0.2 is exactly 0.3 (Float cannot do this).
      sum <- BigDecimal("0.1") + BigDecimal("0.2")
      stdout.println(`sum: ${sum}`)

      //Equality is by value, not scale: 1.0 equals 1.00.
      a <- BigDecimal("1.0")
      b <- BigDecimal("1.00")
      equal <- a == b
      stdout.println(`1.0 == 1.00: ${equal}`)

      //Round to a number of decimal places.
      rounded <- BigDecimal("3.14159").round(2)
      stdout.println(`rounded: ${rounded}`)

Java: java.math.BigDecimal - but equals is scale-sensitive (1.0 != 1.00); EK9 compares by value. Python: decimal.Decimal. Go: shopspring/decimal or math/big Rat. Rust: rust_decimal. EK9: BigDecimal is built in; exact (0.1+0.2==0.3); value equality (1.0==1.00); DECIMAL128 division; round(places); no #^ promote. Use Money for currency.

0.1 0.3 arithmetic bigdecimal decimal equality exact float money precision rounding scale value

BigDecimal
exact decimals
decimal precision


Question: How do I generate random numbers, and make them reproducible, in EK9?

How do I seed a random number generator in EK9?

How do I make randomness deterministic in tests?

What is the difference between SystemRandom and SeededRandom?

How do I roll a dice or pick a random value?

Answer:
Randomness in EK9 is behind the Random trait, with two implementations - following the same injectable pattern as Clock / SystemClock.

THE RANDOM TRAIT
  next()                full-range Integer
  next(lower, upper)    Integer in the inclusive range [lower, upper]
  nextFloat()           Float in [0.0, 1.0)
  nextBoolean()         Boolean

TWO IMPLEMENTATIONS
  SystemRandom()        production - non-deterministic (a different sequence each run)
  SeededRandom(seed)    reproducible - a given seed yields a PINNED sequence, the same across runs and JVMs

WHY IT MATTERS
Because behaviour is behind a trait, code depends on Random and you inject the implementation. In production inject SystemRandom; in tests inject SeededRandom (or a mock) so anything using randomness becomes deterministic and its output can be asserted. Using SystemRandom in a test gives flaky, unrepeatable results - reach for SeededRandom there instead.

EXAMPLE (REPRODUCIBLE DICE)
  rnd <- SeededRandom(42)
  d1 <- rnd.next(1, 6)     // inclusive 1..6
  d2 <- rnd.next(1, 6)
The (lower, upper) bounds are inclusive and match OS().random(lower, upper).

Note there is no shuffle/choice primitive - write a small helper over next(l, u) if you need to reorder or pick from a List.

Clock / SystemClock is the sibling injectable pair. Use 'ek9 -h SeededRandom' for the full API.

defines module qa.random.usage

  defines program

    RandomUsage()
      stdout <- Stdout()

      //Reproducible: the same seed yields the same sequence every run.
      rnd <- SeededRandom(42)

      d1 <- rnd.next(1, 6)
      d2 <- rnd.next(1, 6)
      stdout.println(`dice: ${d1} ${d2}`)

      f <- rnd.nextFloat()
      stdout.println(`float in [0,1): ${f}`)

Java: java.util.Random(seed) / ThreadLocalRandom. Python: random.seed / random.Random(seed). Go: math/rand with a seeded Source. Rust: rand crate with StdRng::seed_from_u64. EK9: a Random trait with SystemRandom (non-deterministic) and SeededRandom(seed) (reproducible), modelled on the injectable Clock/SystemClock pair; next()/next(l,u inclusive)/nextFloat/nextBoolean.

deterministic dice inject next random reproducible seed seeded seededrandom shuffle systemrandom test trait

Random
SeededRandom
reproducible randomness


Question: How do I do trigonometry and use maths constants in EK9?

How do I compute a square root or power?

Answer:
The trigonometric and transcendental functions live on Float, and the mathematical constants come from the small stateless Maths type.

CONSTANTS (Maths)
  maths <- Maths()
  piValue <- maths.pi()
  eValue <- maths.e()

FUNCTIONS ON FLOAT (RADIANS)
All trig works in RADIANS:
  sin, cos, tan, asin, acos, atan, atan2
  log (natural), log10, exp
plus sqrt and abs (operators) and ^ (power). For example:
  halfPi <- maths.pi() / 2.0
  s <- halfPi.sin()               // ~ 1.0

UNSET ON OUT-OF-DOMAIN
A result that is not a finite number becomes UNSET rather than a NaN surprise - for example asin of a value outside -1..1, or log of a non-positive value.

ROUNDING
  rounded <- 3.14159.round(2)      // 3.14

Remember to convert degrees to radians yourself (degrees * pi / 180) before calling the trig functions.

See Q23 for the Float type basics. Use 'ek9 -h Float' and 'ek9 -h Maths' for the full API.

defines module qa.maths.usage

  defines program

    MathsUsage()
      stdout <- Stdout()

      maths <- Maths()

      //Trig works in radians: sin(pi/2) ~ 1.0
      halfPi <- maths.pi() / 2.0
      s <- halfPi.sin()
      stdout.println(`sin(pi/2): ${s}`)

      //Natural log of e is 1.0
      lnE <- maths.e().log()
      stdout.println(`ln(e): ${lnE}`)

Java: java.lang.Math.sin/cos/... and Math.PI/Math.E. Python: math module (math.sin, math.pi). Go: math package. Rust: f64 methods (x.sin()) and std::f64::consts::PI. EK9: trig/transcendental methods on Float (radians) - sin/cos/tan/asin/acos/atan/atan2/log/log10/exp - plus sqrt/abs/^; constants pi()/e() on the Maths type; out-of-domain results are unset.

cos e exp float log math maths pi power radians sin sqrt tan transcendental trigonometry


Question: Why does reading a file with TextFile.input() give me unset lines?

How do I read a file that contains SQL in EK9?

What is the difference between input() and unsanitizedInput() in EK9?

My file lines come back unset when I read them in EK9

How do I bypass sanitization when reading a file in EK9?

How do I find out why a line was rejected in EK9?

Answer:
TextFile.input() screens every line as it is read. A line that trips a threat detector is REJECTED and comes back as an unset String - it is not passed on. This is the secure default. Use unsanitizedInput() when the file legitimately contains content that looks dangerous.

SCREENED READ - THE DEFAULT
file.input() wraps the read so each line passes through InputSanitizer:
  try
    -> input <- file.input()
    while input.hasNext()
      line <- input.next()
      if line?
        stdout.println(line)
      else
        stdout.println("blocked")
Always test the line with line? before using it. A rejected line is unset.

UNSET MEANS REJECTED, NOTHING ELSE
While hasNext() is true, an unset line can only mean the line was rejected. A blank line or a whitespace-only line reads back SET (an empty or blank String is a set String), so there is never any confusion between "rejected" and "empty".

UNSCREENED READ - THE BYPASS
Some files legitimately contain what looks like an attack: SQL migration scripts, HTML fragments, shell scripts, log files containing --. Screening those would discard real data, so read them with unsanitizedInput():
  try
    -> rawInput <- file.unsanitizedInput()
    while rawInput.hasNext()
      line <- rawInput.next()
      stdout.println(line)
Every line passes through unchanged.

GETTING THE REASON, NOT JUST THE FACT
input() tells you a line was rejected but not why - the threat category goes to the sanitizer log. When the program itself needs the category, read raw and ask InputSanitizer directly:
  sanitizer <- InputSanitizer()
  threat <- sanitizer.detectThreat(line)
  if threat?
    stdout.println(threat)
detectThreat() returns a comma separated list such as "SQL_INJECTION, SQL_INJECTION_SIGNATURE" and, unlike sanitize(), does not write to the log. It returns an unset String when the line is safe.

SCREENING IS A JUDGEMENT, NOT A PROOF
Detection is heuristic in both directions. That is why the bypass exists, and why the real guarantee comes from the compiler-enforced sanitized modifier at the point a String enters a dangerous context - not from the file read.

See Q151 for reading text files. See Q1237 for sanitized function parameters. See Q1246 for sanitized constructor parameters. See Q1247 for sanitized method parameters.

defines module qa.security.sanitizedread

  defines program

    SanitizedVsUnsanitizedRead()
      stdout <- Stdout()

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

      // === SCREENED READ - THE DEFAULT ===

      // input() screens every line. A rejected line comes back UNSET,
      // so always test it with line? before using the value.
      stdout.println("Screened read")
      try
        -> input <- file.input()
        while input.hasNext()
          line <- input.next()
          if line?
            stdout.println(line)
          else
            stdout.println("blocked")

      // === UNSCREENED READ - THE BYPASS ===

      // Use unsanitizedInput() for files that legitimately hold content
      // resembling an attack: SQL scripts, HTML, shell scripts, logs with --
      stdout.println("Unscreened read")
      try
        -> rawInput <- file.unsanitizedInput()
        while rawInput.hasNext()
          line <- rawInput.next()
          stdout.println(line)

      // === THE REASON, NOT JUST THE FACT ===

      // input() reports THAT a line was rejected; detectThreat reports WHY.
      // It returns an unset String for safe content and does not write a log entry.
      sanitizer <- InputSanitizer()
      stdout.println("Threat categories")
      try
        -> rawInput <- file.unsanitizedInput()
        while rawInput.hasNext()
          line <- rawInput.next()
          threat <- sanitizer.detectThreat(line)
          if threat?
            stdout.println(threat)

Java/Python/Go: reading a file returns bytes or lines with no screening at all; any input validation is something you remember to add. EK9: TextFile.input() screens by default and reports a rejection as an unset String, so forgetting to check is visible rather than silent. unsanitizedInput() is the deliberate, named bypass, and InputSanitizer.detectThreat() supplies the threat category when the program needs to branch on it.

blocked bypass file injection input read rejected sanitize security textfile threat unsanitized unset

sanitized file read
unsanitizedInput
detectThreat

line <- input.next()
      stdout.println(line)

Explanation: input() screens each line, so a rejected line is unset. Test it with line? before use, or read via unsanitizedInput() if the content is trusted. See ek9 -h E50060 for details.