<!-- Sema documentation — Control Flow
     Sema is a new AI-native, neurosymbolic programming language most models
     have not been trained on. Full corpus: https://sema.49.12.246.95.sslip.io/llms-full.txt
     Install language support: https://sema.49.12.246.95.sslip.io/install-skill/ -->

# Control Flow

> Sema's control constructs — if/elif/else, for, while, scoped with, and the declarative loop … until for bounded agent loops.

Sema's imperative control flow is Python-shaped: `if`/`elif`/`else`, `for … in`,
`while`, `break`, `continue`, `return`, `pass`. Two constructs are Sema-specific
and matter a lot: **`with`** for scoped resources (there are no destructors), and
**`loop … until`** — a declarative bounded do-until loop built for agents.

`match`/`case` also lives here in spirit, but it is large enough to have its own
page — see [Pattern Matching](/language/pattern-matching/).

:::note[No `assert`, no `try`/`raise`]
`assert` is a reserved, rejected token with a fix-it to `ensure`/`check`. There is
no `try`/`raise`/`finally` — failures are typed values handled with `expect`/
`except` and `?` (see [Error Handling](/language/error-handling/)). And blocks
introduce no new scope (Python rules), except the `with … as x:` binding — see
below.
:::

## `if` / `elif` / `else`

```sema
def tier(score: f32) -> str !{}:
    if score >= 0.9:
        return "hot"
    elif score >= 0.5:
        return "warm"
    else:
        return "cold"
```

A binding made inside an `if` arm is visible after the block — blocks do not open a
new scope. For the concise expression form (only the taken branch is evaluated),
use the conditional expression `x if cond else y`, which chains:

```sema
label = "hot" if score > 0.9 else ("warm" if score > 0.5 else "cold")
```

## `for … in`

`for` iterates any `Iterable` — lists, dicts, sets, ranges, and `Stream[T]`:

```sema
mut best_score = 0.0
for resource in resources:
    if resource.capacity <= 0:
        continue
    score = fit(incident, resource)
    if score > best_score:
        best_score = score

for incident in stream:          # Stream[T] is Iterable, with backpressure
    handle(incident)
```

Comprehensions are the expression form of the same protocol:

```sema
kept = [x for x in xs if p(x)]
by_id = {u.id: u for u in users}
```

## `while`

```sema
mut i = 0
while i < len(rows) and not done:
    process(rows[i])
    i = i + 1
```

`while` runs under a runaway guard. For agent-style "keep going until a condition
holds" loops, prefer `loop … until` (below) — it is the idiomatic replacement for
the hand-rolled `while i < max: … if stop: break` shape.

## `loop … until` — bounded do-until

Alongside `while`/`for`, `loop … until` is the declarative surface for a **bounded
agentic loop**. The body runs, *then* the condition is checked — it is a do-until,
so it runs **at least once**:

```sema
loop until decision.confidence >= 0.9 max_iters 8:
    analysis  = breakdown(query, state)
    state.facts += fact_extract(search(query_gen(analysis)))
    decision   = decide(query, state)
```

- `max_iters <n>` bounds the iteration count. Omit it and the loop runs until the
  condition holds, under the same runaway guard as `while`.
- `break` and `continue` work inside.
- It replaces the hand-rolled counter-and-break pattern with something that states
  its intent and its bound.

A real, runnable use from the [`research-agent`
example](/reference/examples-api/research-agent/) — iterate until a Bayesian
belief crosses a confidence threshold, bounded by the evidence available:

```sema
mut belief = Belief(alpha=1.0, beta=1.0, history=[0.5])
mut iters = 0
loop until belief.confidence() >= 0.7 max_iters len(decisions):
    belief.update(decisions[iters])
    iters = iters + 1
```

:::tip[The value-returning form]
`loop … until` is a statement. When you want a loop that *returns a value*, use
the functional form `std.agent_loop.loop_until`. See
[/stdlib/agent_loop/](/stdlib/agent_loop/) and the [agent-loops
guide](/guides/agent-loops/).
:::

## `with` — scoped resources

`with <expr> as x:` is Sema's **single** scoped-binding construct. It acquires a
resource (an effect), binds it for the block, and **deterministically releases** it
at scope exit — on success, failure, *and* cancellation, in reverse acquisition
order. There are **no user destructors or finalizers**, because nondeterministic
finalization would break replay.

```sema
with db.connect(cfg.ledger_dsn) as conn:      # acquire is an effect; release is deterministic
    rows = conn.query(query)
# `conn` is released here and cannot be referenced afterwards
```

The same construct expresses three things — policy scoping, model rebinding, and
resource lifetimes are all one mechanism:

```sema
with policy(NoExecFromGen):                    # scope a governance policy
    run_pipeline(inputs)

with models.writer = local_small:              # scoped model rebinding
    draft = summarize(article)

with meter as u:                               # ambient usage metering
    answer = write_report(facts)
```

Key rules:

- The `as`-binding is **affine and block-scoped** — the handle is released at scope
  exit and **cannot escape** the block. Referencing it afterwards is a compile
  error (capture checking).
- Release is journaled; a release failure is a typed `ReleaseError` routed to the
  owning scope, never masking the body's result.
- Double-release is impossible by construction.

Because `with` is what replaces `finally`, cleanup never lives in a handler — it is
owned by the scope. See the [error-flow ergonomics
table](/language/error-handling/#error-flow-ergonomics--no-cascade-tax).

## `match` — a first look

`match`/`case` is the structural branching construct — it destructures structs,
enums, tuples, and regex captures, with exhaustiveness checking and guards:

```sema
match choose_assignment(incident, resources):
    case Some(assignment): commit(assignment)
    case None:             defer(incident)
```

This is how you consume `Option`/`Result` and pattern-match enum payloads. The full
treatment — refutable vs irrefutable patterns, guards, or-patterns, exhaustiveness,
and interpolated literals — is on the [Pattern
Matching](/language/pattern-matching/) page.

## What's next

- **Destructuring in depth** → [Pattern Matching](/language/pattern-matching/)
- **Typed failures and `?`** → [Error Handling](/language/error-handling/)
- **Building real agent loops** → [Agent Loops guide](/guides/agent-loops/)
