Narrowing#

Narrowing is how a union becomes one of its members inside a branch.

What narrows#

 Construct                        Example
 ───────────────────────────────  ────────────────────────────────────
 Truthiness of a name             if s then
 Truthiness of a dotted path      if config.name then
 not <cond>                       if not s then ... else ... end
 == nil / ~= nil                  if s ~= nil then
 A discriminant field             if shape.kind == "circle" then
 The is operator                  if v is Point then
 ffi.istype<T>(v)                 if ffi.istype<Point>(v) then
 A predicate function             if isPoint(v) then
 and / or                         if a and a.b then
 if / elseif chains               else-branches accumulate facts
 Ternary arms                     v is Point ? v.x : 0
 while cond do                    the body sees the condition
 Guard clauses                    if not s then return end
 never-returning helper calls     bail() narrows like an inline error

Discriminant narrowing also follows a copied local, so binding the value to a new name first does not lose the fact.

What does not narrow#

type(x) == "string" does not narrow. This is the one people expect most:

type is an ordinary function returning an ordinary string, and nothing ties its result back to s. Write s is string.

assert(x) as a statement does not narrow x. It narrows through its return value, because its signature subtracts nil:

Only names and dotted paths narrow. An index like a[i], a call, or any computed expression has no stable key to attach a fact to.

any never narrows. It is already compatible with everything.

Two smaller limits: the falsy side of and proves nothing, and subtracting every member of a union leaves the union alone, since there is no bottom type.

Facts live in a scope#

A narrowed fact dies with the scope that proved it, and assigning to a name clears the facts for that name and everything beneath it:

Predicate functions#

When narrowing cannot see what you know, write a predicate. The return type v is T names a parameter and a type:

The body is trusted. The checker verifies that the name is a parameter (NUPP2109) and that the parameter could hold that type (NUPP2110), and takes the rest on faith.

Guard clauses#

A function that returns never narrows the code after a call to it, the way an inline error does:

The checker infers this for a body whose every path raises, so the never return type is only needed where it cannot see that — an imported C abort, a declaration file with no body, or a loop that never ends. See primitives.

Exhaustiveness#

When every branch of a dispatch over a closed set of literals returns, the checker reports the unhandled members as the exhaustiveness lint. See unions.

Exhaustiveness counts single literal types and unions of them. A union of records is narrowed by a discriminant field instead.