Why can't I call get() in a do-while body when the trailing while condition checks the Optional?

← Safe Value Access · Ref: Q1379

No. A do/while checks its condition AFTER the body has already run, so the trailing 'while' cannot make anything in the body safe. Writing 'do ... while maybeName?' gives E08030 on the get() inside the body, and that is correct - by the time the check runs, the unsafe call has already happened.

WHY THE TRAILING CONDITION CANNOT HELP

Safe access works by proving the check happens BEFORE the access. A leading 'while maybeName?' checks first, so the body is safe. A do/while inverts that order:

  do
    stdout.println(maybeName.get())   //E08030 - runs BEFORE the check
  while maybeName?

The condition still controls whether you iterate again; it just cannot retroactively guarantee the first pass.

THREE WAYS THAT DO WORK

1. Guard from outside with 'if':

  if maybeName?
    do
      stdout.println(maybeName.get())
      loopDone: true
    while not loopDone

2. Use a PRE-FLOW guard on the 'do' itself. It sits between 'do' and the body, so it is evaluated first:

  do maybeName <- lookupName()
    stdout.println(maybeName.get())
    loopDone: true
  while not loopDone

3. Use a leading 'while' instead, if the loop does not need to run at least once:

  while maybeName?
    stdout.println(maybeName.get())

A PRE-FLOW GUARD OVERRIDES THE AT-LEAST-ONCE RULE

This surprises people. A do/while normally runs its body at least once, but a pre-flow guard is a real guard - if the value is unset the body runs ZERO times:

  do maybeName <- lookupMissingName()
    stdout.println("never printed")
  while not loopDone

The guard is a precondition on entering the loop at all, not part of the continuation condition.

WHICH CONDITIONS THE COMPILER CAN ANALYSE

Only a simple is-set check, or several combined with 'and':

  while maybeName? and stillGoing        //analysed - body is safe
  while maybeName? and not loopDone      //NOT analysed - E08030
  while maybeName? or useDefault         //NOT analysed - E08030

'or' genuinely breaks the guarantee. 'not' is refused conservatively: the analysis bails on the whole expression rather than reason about negation. If you hit this, hoist the check into an enclosing 'if' or a pre-flow guard.

DO NOT REASSIGN THE GUARDED VARIABLE

Inside a scope made safe by a check, reassigning that variable is E08040. The guarantee was established once; letting you overwrite it would invalidate it.

See Q67 for do-while basics. See Q163 for unwrapping Optional. See Q165 for why there is no unchecked get. See Q166 for the consistent safe-access pattern.

Example

defines module qa.safeaccess.dowhiletrailing

  defines function

    <?-
      Returns a set Optional.
    -?>
    lookupName() as pure
      <- rtn <- Optional("Steve")

    <?-
      Returns an unset Optional, to show a pre-flow guard refusing to enter the loop.
    -?>
    lookupMissingName() as pure
      <- rtn as Optional of String: Optional() of String

  defines program

    DoWhileSafeAccessDemo()
      stdout <- Stdout()

      // === GUARD FROM OUTSIDE WITH 'if' ===
      // The check runs before the loop, so the body is safe.

      outerName <- lookupName()
      outerDone <- false
      if outerName?
        do
          stdout.println(`Outer if guard: ${outerName.get()}`)
          outerDone: true
        while not outerDone

      // === PRE-FLOW GUARD ON THE 'do' ITSELF ===
      // Sits between 'do' and the body, so it is evaluated first.

      preFlowDone <- false
      do preFlowName <- lookupName()
        stdout.println(`Pre-flow guard: ${preFlowName.get()}`)
        preFlowDone: true
      while not preFlowDone

      // === A PRE-FLOW GUARD OVERRIDES THE AT-LEAST-ONCE RULE ===
      // The Optional is unset, so the body runs ZERO times.

      skippedDone <- false
      do missingName <- lookupMissingName()
        stdout.println(`Never printed: ${missingName.get()}`)
        skippedDone: true
      while not skippedDone
      stdout.println("Unset pre-flow guard skipped the body entirely")

      // === A LEADING 'while' CAN MAKE THE BODY SAFE ===
      // Checked before each iteration, so the ordering requirement is met.
      // 'and' composes; 'not' or 'or' would make the expression unanalysable.

      leadingName <- lookupName()
      stillGoing <- true
      while leadingName? and stillGoing
        stdout.println(`Leading while: ${leadingName.get()}`)
        stillGoing: false

Common mistakes

E08030 — The trailing 'while' is evaluated after the body has already run, so it cannot make get() safe. Move the check in front of the body as a pre-flow guard on the 'do'. See ek9 -h E08030 for details.

Incorrect:

      preFlowName <- lookupName()
      do
        stdout.println(`Pre-flow guard: ${preFlowName.get()}`)

Correct:

      do preFlowName <- lookupName()
        stdout.println(`Pre-flow guard: ${preFlowName.get()}`)

E08030 — Safe access is only inferred from a simple is-set check, or several joined with 'and'. Putting the is-set check under 'or' (or under 'not') makes the expression one the analysis will not reason about, so the body is not marked safe. Keep the is-set check as a plain 'and' operand. See ek9 -h E08030 for details.

Incorrect:

while leadingName? or stillGoing

Correct:

while leadingName? and stillGoing

E08040 — Inside a scope that a check has made safe, the guarded variable cannot be reassigned - that would invalidate the guarantee the check established. End the loop with a separate flag instead. See ek9 -h E08040 for details.

Incorrect:

        stdout.println(`Leading while: ${leadingName.get()}`)
        leadingName: lookupName()
        stillGoing: false

Correct:

        stdout.println(`Leading while: ${leadingName.get()}`)
        stillGoing: false
Other ways to ask this
  • Does a do-while trailing condition make an Optional safe in EK9?
  • Why do I get E08030 inside a do-while loop in EK9?
  • How do I safely unwrap an Optional in a do-while loop?
  • do ... while maybeValue? still says has not been checked before access
  • Safe access in a post-condition loop in EK9
  • Why does while o? work but do ... while o? not?
  • How do I guard a do-while loop in EK9?

Coming from another language?

Java: Optional has no loop integration; you call isPresent() then get(), and a do-while gives no help either way - the compiler never checks. Kotlin: smart casts do not survive into a do-while body from the trailing condition, and null-safety is enforced by the type system rather than by flow position, so you use '?.let' or '!!'. Swift: 'repeat { } while' cannot bind an optional; 'guard let' before the loop is the idiom, which is closest to EK9's outer-if form. Rust: 'loop { }' with 'if let Some(v) = opt' inside re-matches every iteration; there is no trailing-condition binding. Go: no Optional; a nil check before the loop, unverified by the compiler. C#: nullable reference analysis does flow-sensitive checks but treats a do-while trailing condition as not guarding the body, the same conclusion EK9 reaches - though C# emits a warning where EK9 emits an error. EK9: the check must be positioned before the access, and the compiler proves it; a pre-flow guard on 'do' is the construct that puts a check before a post-condition loop body.

Keywords: condition, do, access, E08030, safe, postcondition, E08040, iterator, while, loop, optional, guard, trailing, preflow, result, get, unwrap