Why this fires
The ?. operator in Zolo is null-safe chaining: it skips the method call
and returns nil when the receiver is nil. A Result or Option is a
wrapper value, not a nilable reference — it is never nil, so the chain
always runs over the wrapper itself instead of its payload.
use std::Database
let db = Database.open("sqlite://:memory:")?
db.query("SELECT * FROM users")?.each(|u| print(u.name))
// ^^ error[TE827]: `?.` is null-safe chaining, but the receiver
// is a `Result` — a `Result` is never nil.
// Chain on the Ok value instead:
// `expr ?> .each(...)` (propagate),
// `let v = expr?` then `v.each(...)` (unwrap),
// or `expr!.each(...)` (panic on Err)
This looks like Rust's try-then-chain syntax, but the operators have different
semantics in Zolo. ?. checks for nil; it does not unwrap a Result.
Because a Result is never nil, the null-safe guard is a no-op and the
chain attempts to call .each directly on the wrapper. .each is not a
method of Result — historically this died with a cryptic
R0001: attempt to call method 39;each39; (a nil value); the runtime now
converts that into the friendly wrapper-guard message (via
__zolo_optchain_call), and TE827 catches it statically before it ever runs.
The same applies to Option:
fn find_user(id: int) -> Option<User> { ... }
find_user(42)?.name
// ^^ error[TE827]: `?.` is null-safe chaining, but the receiver is an `Option`
// — an `Option` is never nil. Chain on the Some value instead:
// `expr ?> .name(...)` (propagate), `let v = expr?` then `v.name(...)` (unwrap),
// or `.unwrap_or(...)`
?. is correct when the receiver is a nilable wrapper — i.e. the type
is Result<T, E>? or Option<T>?:
fn maybe_result(flag: bool) -> Result<int, str>? {
if flag { Result.Ok(1) } else { nil }
}
maybe_result(false)?.is_ok() // fine — receiver may be nil
Fix it
1. Fallible pipe ?> (propagate Err/None)
Replace ?. with ?> followed by a dot chain. The function must return a
compatible Result or Option:
db.query("SELECT * FROM users") ?> .each(|u| print(u.name))
2. Bind then use
Unwrap with the ? postfix operator (propagates Err/None) and call the
method on the bound value:
let rows = db.query("SELECT * FROM users")?
rows.each(|u| print(u.name))
3. Force chain !. (panic on Err/None)
If a failure is a programming error and a panic is acceptable:
db.query("SELECT * FROM users")!.each(|u| print(u.name))
For Option, you can also use .unwrap_or(default) to avoid a potential
panic:
find_user(42).unwrap_or(User.default()).name
Notes
?.(null-safe),?>(fallible pipe),?(unwrap/propagate), and!.(force chain) are four orthogonal operators. Seedocs/06-operators.mdfor their full semantics.- The check is by type name, so a user-declared enum literally named
ResultorOptionalso trips TE827. The prelude names are effectively reserved. - When the receiver type is not statically known (
Any), TE827 is not emitted. A runtime guard in the__zolo_optchain_callprelude helper still catches the mistake and emits a friendly message instead of the cryptic raw runtime error. - TE827 fires for all members accessed via
?.on a wrapper, not only collection methods. Evenresult?.unwrap()is flagged: a wrapper is never nil, so?.over one is always a mistake.
See also
TE826— collection methods called on un-unwrappedResult/Option.docs/06-operators.md—?.,?>,?,!.operator set.docs/18-error-handling.md— Result/Option patterns.