RFC 049: if let and while let pattern control flow¶
- Status: Implemented
- Created: 2026-04-02
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 000 (core language surface)
- RFC 018 (testing)
- Issue: https://github.com/dannys-code-corner/incan/issues/333
- RFC PR: —
- Written against: v0.3.0-dev.1
- Shipped in: v0.3.0-dev.1
Summary¶
This RFC introduces if let and while let as first-class Incan control-flow constructs for pattern-oriented destructuring. if let is intended for cases where authors care about exactly one successful pattern and want the non-match case to do nothing, such as replacing boilerplate like Some(x) => ... paired with None => pass with if let Some(child) = filter.input:. while let covers the looping counterpart: continue iterating while a pattern keeps matching, then stop when it no longer does. This RFC does not replace full match; full match remains the canonical construct for multi-arm branching, exhaustive reasoning, and cases where the non-match path is semantically meaningful. The design is intentionally closer to Rust than Python because the motivating cases in Incan are primarily explicit Option, Result, and enum destructuring, so Rust-style pattern control flow is a better fit than Python-style truthiness.
Core model¶
if let PATTERN = VALUE:attempts to matchVALUEagainstPATTERNonce.- If that match succeeds, the body executes and any names bound by the pattern are available inside that body.
- If that match fails, the body is skipped and no names are bound.
while let PATTERN = VALUE:attempts the same match on each loop condition check and continues iterating while it succeeds.- Full
matchremains the preferred construct when more than one arm matters.
Motivation¶
In Incan today, it is common to inspect an Option[T], Result[T, E], or enum payload, perform a small action when one shape is present, and otherwise do nothing.
That often produces code like:
match filter.input:
Some(child) => return [child]
None => pass
or:
match result:
Err(err) => log(err)
Ok(_) => pass
These are explicit, but they are also repetitive. The unmatched arm often adds no meaning beyond "do nothing."
This RFC introduces surfaces that say exactly that: perform one pattern match and execute one body on success, or keep looping while one pattern keeps matching.
That choice is deliberate. Incan's problem here is not "how do we make conditionals feel more Pythonic." It is "how do we reduce boilerplate around explicit Option and Result handling without weakening the language's pattern-matching model." Rust has already established that if let is an effective answer for that specific problem shape, and this RFC follows that direction.
Goals¶
- Add concise, explicit pattern-oriented control-flow forms for one-arm extraction and loop-while-match cases.
- Reuse existing pattern semantics rather than introducing truthiness-based control flow.
- Allow successful matches to bind names with normal lexical scope inside the success branch or loop body.
- Keep full
matchas the primary construct for multi-arm or exhaustive branching. - Align the surface with a familiar and proven construct where that improves readability.
Non-Goals¶
- Replacing full
match. - Introducing Python-style truthiness such as
if child:. - Adding multi-arm shorthand, match guards, or expression-level pattern-match sugar in this RFC.
- Defining raw Rust passthrough syntax as a language feature.
- Changing existing constructor syntax in value position.
Guide-level explanation¶
Basic form¶
def first_child(filter: Filter) -> List[Rel]:
if let Some(child) = filter.input:
return [child]
return []
This means:
def first_child(filter: Filter) -> List[Rel]:
match filter.input:
Some(child) => return [child]
None => pass
return []
More examples¶
def log_failure(result: Result[int, str]) -> None:
if let Err(err) = result:
log(err)
def first_join_child(rel: Rel) -> List[Rel]:
if let RelType.Join(join) = rel.rel_type:
if let Some(left) = join.left:
return [left]
return []
def drain(queue: Queue[Option[int]]) -> List[int]:
values = []
while let Some(value) = queue.pop_front():
values.append(value)
return values
What this is for¶
Use if let when:
- exactly one pattern matters;
- the non-match case should do nothing;
- the code reads more clearly as opportunistic extraction than as branching.
Use while let when:
- each iteration should continue only while one pattern keeps matching;
- the loop naturally ends on first non-match;
- writing the same destructuring
matchorwhile true+breakpattern would be noisier.
What this is not for¶
When both outcomes matter, use full match:
match result:
Ok(value) => cache.store(value)
Err(err) => logger.error(err)
This RFC also does not introduce truthiness:
# Not part of this RFC
if child:
return [child]
Reference-level explanation¶
Syntax¶
This RFC introduces if let and while let statement forms:
if_stmt ::= "if" if_test ":" block
while_stmt ::= "while" while_test ":" block
if_test ::= expr | if_let_test
while_test ::= expr | while_let_test
if_let_test ::= "let" pattern "=" expr
while_let_test ::= "let" pattern "=" expr
The pattern grammar is the same pattern grammar already used by match arms.
This RFC introduces if let and while let in statement position. It does not introduce let patterns in arbitrary boolean expression positions.
Semantics¶
VALUEmust be evaluated exactly once.- The pattern match must use the same matching rules as a
matcharm. - If the pattern matches, the
if letbody executes. - If the pattern does not match, the body is skipped.
- A failed match must not bind any names.
- In
while let, the condition expression must be re-evaluated on each iteration, just as an ordinarywhilecondition is re-checked on each iteration. - In
while let, the loop body executes only for iterations whose condition pattern matched successfully. - In
while let, the first non-match exits the loop without binding names for that failed attempt.
The following:
if let PATTERN = VALUE:
BODY
is semantically equivalent to:
match VALUE:
PATTERN => BODY
_ => pass
The following:
while let PATTERN = VALUE:
BODY
is semantically equivalent to:
while true:
match VALUE:
PATTERN => BODY
_ => break
Scope and binding¶
- Names bound by the pattern are in scope only within the
if letsuccess branch. - Names bound by a
while letcondition are in scope only within the loop body for the successful iteration that produced them. - Those names are not in scope after the
if letorwhile letcompletes. - Shadowing behavior follows the same rules as bindings introduced by
matcharms. - In v1,
if letremains single-arm only: it does not accepteliforelsebranches. When the non-match path matters, usematch.
Typing¶
VALUEmust be type-checkable againstPATTERNunder the same rules as amatcharm.- Impossible patterns must produce the same kind of type errors as
match. - Bound names receive the same types they would receive in the equivalent
matcharm.
Errors and diagnostics¶
- Diagnostics should describe this construct as pattern matching, not assignment.
- Unused pattern bindings should follow normal lint behavior.
- Tooling should explain
if letin terms of its equivalent single-armmatchwhen helpful.
Design details¶
Why if let and while let¶
This RFC deliberately chooses if let and while let as the primary surfaces instead of PATTERN match VALUE, value is Pattern, or walrus-style syntax.
The reasons are:
- it is immediately recognizable to users familiar with Rust-style destructuring control flow;
- it clearly communicates that the construct is pattern-matching-oriented;
- it reads naturally in single-arm extraction cases;
- it scales cleanly from
OptionandResultto enum payloads and other destructuring patterns; - it covers both the one-shot and looping variants of the same pattern-control-flow idea instead of standardizing an asymmetrical surface.
Most importantly, it does not require inventing a new control-flow spelling for a problem already well served by an established shape.
It is also a better fit for Incan than a Python-flavored shorthand such as if child:. Incan's motivating examples are about matching structured values like Some(...), Ok(...), and Err(...), not about truthiness. Choosing a Rust-aligned surface keeps the semantics explicit and keeps the feature centered on shape-based control flow.
Why this is not "raw Rust passthrough"¶
The syntax is Rust-aligned, but this RFC does not define if let / while let as "whatever Rust accepts."
Incan owns the construct. That means:
- the grammar is specified in Incan terms;
- the semantics are specified in Incan terms;
- lowering to Rust
if let/while letis an implementation strategy, not the language definition.
That distinction matters because Incan should remain free to evolve its own pattern grammar, diagnostics, and lowering strategy without accidentally turning backend quirks into language law.
Why not general let inside any if¶
This RFC does not propose a general rule like "if the parser sees let in an if or while condition, forward it to Rust."
That approach is too broad for a young language because it:
- blurs the boundary between Incan syntax and backend syntax;
- risks surprising edge cases if Rust accepts shapes Incan does not want to standardize;
- makes future non-Rust lowering harder.
This RFC instead standardizes two narrow constructs: if let PATTERN = VALUE: and while let PATTERN = VALUE:.
Supported usage¶
The intended sweet spot is shallow, single-arm extraction:
if let Some(child) = filter.input:
return [child]
if let Ok(value) = result:
return value
if let RelType.Cross(cross) = rel.rel_type:
process(cross)
while let Some(token) = stream.next():
process(token)
Interaction with full match¶
Use full match when:
- more than one arm is meaningful;
- the unmatched path matters to the reader;
- exhaustiveness matters;
- nesting would make
if letchains harder to read than a singlematch.
This RFC therefore reinforces style rules:
- use
if letfor opportunistic extraction with implicit no-op on failure; - use
while letfor repeated extraction that should stop on first non-match; - use
matchfor true branching.
Interaction with Option and Result¶
if let and while let are especially useful for:
Option[T]viaSome(...);Result[T, E]viaOk(...)andErr(...).
This RFC does not change the meaning of ?. The ? operator remains the preferred construct for propagation. if let is for side effects, local extraction, and control flow that intentionally continues after non-match, while while let is for repeated extraction that naturally stops on first non-match.
Interaction with RFC 018 is¶
RFC 018 already uses is in pattern-oriented assertions. That remains valid and useful in assertion contexts.
This RFC does not extend is into destructuring conditional or loop control flow. The reason is conceptual clarity:
isreads as a boolean pattern test;if let/while letread as destructuring control-flow constructs.
For this RFC's narrow goal, if let / while let are the better fit.
Alternatives considered¶
-
Keep using full
matcheverywhere. This preserves one construct but keeps the repetitiveNone => pass/Err(_) => passboilerplate that motivated this RFC. -
PATTERN match VALUE. This is explicit, but it introduces a new dedicated control-flow spelling where a well-understood construct already exists. -
Extend
is, as inif value is Some(child):. This is plausible, especially given RFC 018, but it frames the feature more as a boolean pattern test than as a single-arm destructuring branch. -
Walrus-style binding. This is awkward for pattern-matching constructs, especially around
Some(...),Ok(...), and other constructors. It obscures the fact that the operation is a pattern match rather than assignment. -
General Rust passthrough for
letinsideif/while. This was rejected because it weakens Incan's ownership of its own syntax and semantics. -
Ship only
if letnow and deferwhile let. This was rejected because the two constructs share the same mental model, pattern semantics, and implementation machinery. Deferringwhile letwould force users back intowhile trueplusmatch/breakboilerplate for the looping form of the exact same problem.
Drawbacks¶
- The language gains additional control-flow surface area.
- Users must learn when to prefer
if let/while letover fullmatch. - Formatter and linter guidance will matter to prevent overly dense nested
if letorwhile letchains.
Implementation architecture¶
The preferred implementation strategy is to express if let and while let through the same semantic core already used by full match.
That can be done either by:
- interpreting
if letas a single-armmatchplus implicit_ => pass, or - interpreting
while letas repeated single-arm matching plus implicit loop exit on non-match, or - representing them separately while preserving the same pattern-matching semantics.
This section is non-normative. Any implementation strategy is acceptable if it preserves the semantics above.
Layers affected¶
- Language surface:
if let PATTERN = VALUE:must be accepted inifstatement position, andwhile let PATTERN = VALUE:must be accepted inwhilestatement position. - Type system: the pattern must type-check exactly like a
matcharm, and bindings must stay scoped to the success branch or successful loop iteration body. - Execution handoff: implementations may realize
if let/while letthrough the existing pattern-match machinery as long as the observable semantics match this RFC. - Formatter:
if let/while letshould format predictably and avoid unreadable nested chains. - LSP / tooling: hover, completion, and diagnostics should respect branch-local and loop-body-local pattern bindings.
Implementation Plan¶
Phase 1: Parser, AST, and formatter¶
- Extend the statement grammar so
ifandwhileconditions can carrylet PATTERN = VALUEtests in statement position. - Represent
if letandwhile letexplicitly in the frontend AST, preserving spans for the pattern, value, and body. - Teach the formatter to print both constructs predictably and keep nested pattern-control-flow readable.
Phase 2: Typechecker and scope¶
- Validate
if let/while letpatterns under the same rules asmatcharms. - Bind names only within the success branch or successful loop iteration body.
- Emit span-precise diagnostics for impossible or otherwise invalid patterns in these positions.
Phase 3: Lowering and emission¶
- Lower
if letto the existing conditional/match machinery while preserving single-evaluation semantics. - Lower
while letto repeated pattern checking with loop exit on first non-match. - Emit correct Rust for both constructs without broadening the accepted Incan surface beyond this RFC.
Phase 4: Tooling, tests, and docs¶
- Preserve hover, completion, and diagnostics behavior for pattern bindings inside
if letandwhile let. - Add parser, typechecker, codegen snapshot, formatter, and integration coverage for both constructs.
- Update user-facing docs and release notes for the new control-flow surface.
Implementation log¶
Spec / design¶
- Settle RFC 049 as
if letpluswhile let, both in statement position only. - Record the binding, scope, and non-goal rules in the RFC body.
Parser / AST¶
- Parser: accept
let PATTERN = VALUEtests inifstatements. - Parser: accept
let PATTERN = VALUEtests inwhilestatements. - AST: represent
if letwith span-precise structure. - AST: represent
while letwith span-precise structure. - Formatter: round-trip both constructs stably.
Typechecker¶
- Validate
if letpatterns withmatch-equivalent checking. - Validate
while letpatterns withmatch-equivalent checking. - Scope bound names to the success branch or successful loop iteration body only.
- Emit clear diagnostics for invalid pattern usage in these positions.
Lowering / emission¶
- Lower
if letto the existing control-flow core. - Lower
while letto the existing control-flow core. - Emit correct Rust for both constructs.
Tooling¶
- Keep diagnostics wording aligned with pattern-matching semantics rather than assignment.
- Preserve LSP behavior for pattern bindings inside
if letandwhile let.
Tests¶
- Parser unit tests for
if let. - Parser unit tests for
while let. - Typechecker unit tests for valid and invalid
if let. - Typechecker unit tests for valid and invalid
while let. - Codegen snapshot tests for
if let. - Codegen snapshot tests for
while let. - Integration coverage for end-to-end behavior.
Docs¶
- Update docs-site pages that describe control flow or pattern matching.
- Add a release-notes entry for RFC 049 / issue #333.
Design Decisions¶
- RFC 049 includes both
if letandwhile let;while letis not deferred to a follow-up RFC. - Both constructs are statement-position control-flow forms only in v1. This RFC does not introduce general
letpatterns in arbitrary boolean expressions. - Both constructs inherit the same pattern semantics and diagnostics contract as
matcharms rather than creating a separate matching model.