Control Flow
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.
if / elif / else
Section titled “if / elif / else”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:
label = "hot" if score > 0.9 else ("warm" if score > 0.5 else "cold")for … in
Section titled “for … in”for iterates any Iterable — lists, dicts, sets, ranges, and Stream[T]:
mut best_score = 0.0for 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:
kept = [x for x in xs if p(x)]by_id = {u.id: u for u in users}mut i = 0while i < len(rows) and not done: process(rows[i]) i = i + 1while 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
Section titled “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:
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 aswhile.breakandcontinuework 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 — iterate until a Bayesian
belief crosses a confidence threshold, bounded by the evidence available:
mut belief = Belief(alpha=1.0, beta=1.0, history=[0.5])mut iters = 0loop until belief.confidence() >= 0.7 max_iters len(decisions): belief.update(decisions[iters]) iters = iters + 1with — scoped resources
Section titled “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.
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 afterwardsThe same construct expresses three things — policy scoping, model rebinding, and resource lifetimes are all one mechanism:
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
ReleaseErrorrouted 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.
match — a first look
Section titled “match — a first look”match/case is the structural branching construct — it destructures structs,
enums, tuples, and regex captures, with exhaustiveness checking and guards:
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 page.
What’s next
Section titled “What’s next”- Destructuring in depth → Pattern Matching
- Typed failures and
?→ Error Handling - Building real agent loops → Agent Loops guide