Optional Bindings¶
Accepted
Accepted for V1 optional binding source forms and ownership behavior.
Optional values coerce to bool: null is false and a present value is true. This coercion is general, so it is valid in conditions and ordinary bool-valued contexts:
if maybe_value {
use(maybe_value)
}
var present: bool = maybe_value
The coercion tests only presence. For ?bool, Some(false) coerces to true; this is legal but should produce a lint because it is easy to confuse presence with payload truth.
In a plain if name { ... } where name is a local optional binding with a non-void payload, the true branch gets an implicit const payload shadow binding. It is sugar for if const name = name { ... }.
For ?bool, this means the branch tests presence and the shadow binding has type bool; the payload may still be false, so this form should lint strongly in favor of explicit binding. This sugar applies to bare local names only. Field accesses and arbitrary optional expressions such as if user.name { ... } or if make_optional() { ... } are presence tests without payload binding.
Payload Binding¶
Optional payload binding uses dedicated conditional declaration forms:
if const value = maybe_value {
use(value)
}
if const { name, age } = maybe_user {
use(name)
}
if const name = user.name {
use(name)
}
if var value = maybe_value {
value = transform(value)
}
The initializer may be omitted for a const binding when the binding name is also the optional being narrowed:
if const value {
use(value)
}
This is sugar for if const value = value. Same-name sugar is only for a bare local optional binding name. Destructuring requires an explicit initializer, such as if const { name } = maybe_user.
It is legal even when value: ?void, but should produce the same useless-binding lint as the explicit form. There is no if var value same-name sugar in V1 because assignment would affect the mutable payload shadow, not the outer optional. Use an explicit initializer for mutable payload bindings:
if var payload = value {
payload = transform(payload)
}
Inside the true branch, normal name resolution sees the payload binding. For the same-name if sugar, that payload binding is const by construction:
var value: ?String = get_name()
if value {
value = "x" // error: assigns to const payload shadow, not the outer optional
}
For if const name = optional_expr and if var name = optional_expr, the initializer must have optional type ?T. null skips the branch. A present value narrows to the payload and binds name: T for the branch. The optional type annotation, when present, annotates the payload type:
if const value: i32 = maybe_value {
}
An else branch receives no negative narrowing fact in V1. The payload binding is scoped only to the true branch, and the original optional remains its original optional type in else.
Conditional optional binding is valid anywhere if is valid, including expression position:
const label = if const name = maybe_name {
name
} else {
"unknown"
}
Same-name if sugar works in expression position too:
const label = if maybe_name {
maybe_name // String payload shadow
} else {
"unknown"
}
This unwraps one optional layer. For ??T, the binding has type ?T. ?void is allowed, though binding a void payload is usually useless and should be linted in favor of a plain presence check.
Because void has no useful payload, if marker { ... } for marker: ?void remains a presence test and does not introduce an implicit payload shadow binding.
Same-name optional payload sugar applies to if only. while value { ... } is a presence loop, not payload narrowing; use while const value = next_optional() for loop narrowing.
Ownership¶
Conditional optional binding is value binding, not hidden borrowing. Over an lvalue optional it copies the payload; if the payload type is non-copy, the binding is rejected in V1. Conditional optional binding over a fresh rvalue optional may move the payload because there is no source binding to preserve:
if const x = maybe_i32 {
} // ok when i32 is copyable; maybe_i32 is unchanged
Binding a non-copy payload from an lvalue optional is rejected:
if const file = maybe_file {
} // error when File is non-copy and maybe_file is an lvalue
A fresh rvalue optional may move its payload:
if const file = make_maybe_file() {
} // ok; payload moves out of the fresh optional rvalue
When an lvalue optional binding succeeds, the source optional is unchanged. A present source remains present; a null source remains null.
Semantically, the payload binding is created before entering the branch body. Implementations may elide or delay the physical copy when that preserves observable behavior, but diagnostics and name resolution treat the binding as existing at branch entry.
The lvalue non-copy rejection happens before branch-body analysis. It remains an error even if the branch would immediately dispose or move the payload.
V1 conditional optional binding does not move out of an lvalue optional and set it to null, even if the initializer uses move maybe_value. Destructive optional take requires future explicit optional APIs with clear post-state semantics.
When optional binding produces a resource payload, the binding owns that resource like any other local resource value. The compiler does not insert hidden cleanup at the end of the branch. The resource must be explicitly disposed or moved onward; missed cleanup is covered by optional cleanup lints such as ownership/missing-cleanup.
This is the V1 value-level narrowing/capture form for optionals. Broader pattern-oriented conditional syntax is deferred.
Conditional optional bindings follow normal local shadowing rules. They may reuse an outer name, and the payload binding shadows that name only inside the true branch or loop body.
Field Optionals¶
Field optional values are not flow-narrowed in V1. To narrow a field payload, bind it explicitly:
if user.name {
use(user.name) // still ?String; this was only a presence test
}
if const name = user.name {
use(name) // String
}