Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec §3 — Grammar

The full syntactic grammar, in the notation of §1.3. Token classes (IDENT, NUMBER, STRING, …) are defined in §2. The start symbol is module.

A quoted terminal that is a contextual keyword (§2.2 — with, borrows, own, lazy, dyn, jump, then, as, only, context, sync, self) matches an IDENT of that spelling, and only at the position its production puts it; everywhere else the same word is an ordinary IDENT (B414). Each is decided without backtracking. with (after an impl subject or a trait head) and borrows (after a return type) sit where no name can, so the position alone decides. own and lazy at a parameter’s head, lazy before let/mut, jump before its target and dyn at a type’s head are PREFIXES of a name, and the grammar never puts two names side by side, so the word is the keyword when a name follows it (for dyn, when anything but :: follows it; for own and lazy, also when a binder’s other heads do — mut, a destructure’s ( or [, a spread’s ... — but never a single .) and a name otherwise: fun f(own: Owner), |lazy| lazy.force(), jump.height, dyn::Registry. then follows a complete operand (§3.8), which is where no name can stand either.

3.1 Modules and statements

module    = [ module-self ] { statement } ;
module-self   = [ platform-attr ] "mod" "self" ";" ;  (* B415, §4.1, §11.3 *)
platform-attr   = "[" "platform" "(" STRING { "," STRING } [ "," ] ")" "]" ;

statement = derived-item
          | service-item
          | macro-attributed-item
          | macro-fun
          | macro-block [ ";" ]
          | macro-invocation [ ";" ]
          | export         (* the visibility marker, §3.2 *)
          | expression ";"
          | then-statement ";"   (* B459, §3.8 *)
          | if-expr        (* not before "}" — see below *)
          | for-expr       (* not before "}" *)
          | match-expr     (* not before "}" *)
          | function
          | struct
          | enum
          | impl
          | trait
          | labelled-let   (* §3.4 *)
          | "mod" IDENT "{" { statement } "}"
          | import ";"
          | use ";"
          | block          (* not before "}" *)
          ;

A module-self — mod self;, the file’s OWN module, with no body — is the host for attributes about the whole file (§4.1): [platform("browser")] mod self; declares the platform of the WHOLE FILE (§11.3). It is legal only as the file’s first statement; anywhere else it is refused and told to move above the first import. self is reserved for it: a nested "mod" IDENT block may not be named self. The same attribute with no mod self after it fences the file’s first function.

A block-like form (if/for/match/{…}) in statement position must not be the last thing in its enclosing block: in that position it is instead the block’s trailing expression and supplies the block’s value (§3.5).

3.2 Imports and exports

import  = "import" path-branch [ "only" ] ;   (* impls-free, §4.3 *)
use     = "use"    path-branch ;
path-branch = [ "#" ] NAME [ "::" ( path-branch | path-set )
                   | "as" NAME ] ;        (* alias, §4.3 *)
path-set    = "{" set-element { "," set-element } [ "," ] "}" ;
set-element = path-branch | impl-selector ;
impl-selector = "(" "impl" type [ "with" type ] ")"
                [ "::" ( MEMBER | "{" MEMBER { "," MEMBER } [ "," ] "}" ) ] ;
NAME        = IDENT | "true" | "false" ;   (* variant re-exports *)

export      = { lead-attribute }   (* the item's own prefix, in THE order below *)
              "export" [ "(" "in" path-branch ")" ]
              statement   (* §4.8 *)
            | "export" "*" ";" ;          (* the whole-module marker *)
lead-attribute = deprecated-label | internal-label | hint-label | platform-attr
               | resource-attr | extern-attr | "[" "must_use" "]"
               | "[" "track_caller" "]" | "[" "rpc" "]"
               | "[" "trait_only" "]" | derive-attr | service-attr
               | client-service-attr | macro-attr ;

import brings names from another module into scope; use brings names from a type’s namespace (e.g. variants) into scope. In a set, self names the item itself (Option::{ self, Some, None } imports the type and its variants), and self as name imports it under another name. vilan fmt and Organize Imports write a set’s self first, then self as name, then the members. Semantics: §4.

export is a statement WRAPPER, not a declaration kind: it takes the statement under it, which is what lets one production cover every declaration and the re-export alike. export statement marks a declaration as the module’s surface (or re-exports an import); export(in PATH) statement narrows that to a scope subtree (mod, pkg, or a module path); export *; marks every item of the module and is the one form carrying no inner statement. The wrapper does not change the statement’s own shape, so a wrapped let keeps its terminator — export let registry = …; — and a wrapped fun still takes none. A declaration carrying attributes is wrapped as a whole, attributes and all; an attribute it carries joins the item’s own prefix, whose admissions are the item production’s (§3.3).

A declaration head is written in ONE order (B485, B536): its attributes, by rank; then its keywords, in Q8’s order — export (with its (in PATH)), then const or lazy, then async, then external or macro; then the declaration word (fun, struct, enum, trait, impl, let, mut, mod, import, use). The productions in §3.3 each state their own slice of it.

The attributes’ ranks, which are the order vilan fmt prints them in, each on its own line:

  1. generation — [derive(..)], [service(..)], [client_service(..)], a macro attribute (among themselves, as written);
  2. the labels — [deprecated(..)], then [internal(..)], then [hint(..)];
  3. the binding — [extern(..)];
  4. the checks — [must_use], then [track_caller], then [rpc], then [trait_only];
  5. the fence — [platform(..)];
  6. the class — [resource].

So [derive(Wire)] ⏎ export struct Handle { … }, [platform("browser")] ⏎ export impl …, [deprecated("use g")] ⏎ [must_use] ⏎ export async fun f(), [deprecated("use b")] export import a::b as c;, and async macro fun (B524). A head written in another order is read exactly as if written in this one, and reported:

  • attributes out of rank, and nothing else out of order — [internal("why")] ⏎ [deprecated("use g")] ⏎ fun — an error that names the head in the order (write `[deprecated(..)] [internal(..)] fun`): a warning in v0.44.0, refused from v0.45.0 (B536);
  • a keyword ahead of an attribute, or two keywords inverted — async [platform("node")] fun, external async fun, lazy export let, macro async fun — an error, with the head respelled in the order. That includes export and macro ahead of the attributes: export [derive(Wire)] struct Handle, the spelling before B485, read by v0.43.0 and refused from v0.44.0 (B485 S3).

vilan fmt writes every such head in the order, the refused ones included, so it is the migration either diagnostic asks for; the editor’s quick fix does the same for one head. A keyword set no order makes legal (async const fun, lazy fun) is refused by its production instead. Only an attribute shape — [ then a name — leads a head; export *; takes none. The marker is written once: a second export on one declaration is refused (B492).

A # before a path element is the reach marker: import pkg::a::{ #hidden }; imports an item the module does not export, deliberately (§4.3, §4.8).

as renames the LEAF a branch ends at — import a::b::c as d; binds d, and import a::{ b as x, c } binds x and c. It is an alternative to the :: continuation, not something that may follow one (a::b as c::d does not parse), and it is contextual: as is an ordinary identifier everywhere else in the language, including as a module or item name, and reads as an alias only where a path segment has ended and a NAME follows it.

only is a trailing modifier on an import STATEMENT, not on a leaf: it says the statement brings its names and no implementations (§4.3). Like as it is contextual — an ordinary identifier everywhere else, read as the modifier only where the whole path has ended — and a use refuses it, because a use never brought an implementation along.

An impl-selector is a brace-set ELEMENT that binds no name: it says which of the module’s impl blocks this file admits, and its optional :: tail names the members it takes (§4.3). _ stands for any type at an argument position ((impl List<_>)) and (impl _) selects every implementation the module declares; a selector writes no type X binders and takes no as. with TRAIT names the block by the trait it implements, as its declaration does — (impl Box with One) takes impl Box with One and no other Box block — which is how a block with no declarations of its own (all defaults, or a marker) is named; the trait resolves in the importing file’s scope, as the subject does. The subject is the ordinary type production, and it resolves in the IMPORTING file’s scope, so impl S reaches an alias that file bound and impl item::Struct is the qualified spelling.

A :: path may not cross a line break: the segment after a :: must begin on the same line the :: is on. Without the rule a:: at the end of a line joins whatever the next line starts with — style:: then print(…) on the line below is the legal path style::print, and the next statement is silently swallowed. Import a long path under a shorter name instead. The rule binds the two productions that COMMIT to a separator, the expression path (§3.6) and the import path here; a type path and a struct-literal head read the :: and the name that follows it together and leave a trailing separator where it was.

3.3 Items

Functions

function = [ "[" "deprecated" "(" STRING ")" "]" ]
           [ "[" "internal" "(" STRING ")" "]" ]
           [ extern-attr ] [ "[" "must_use" "]" ] [ "[" "track_caller" "]" ]
           [ "[" "rpc" "]" ] [ "[" "trait_only" "]" ]
           [ "[" "platform" "(" STRING { "," STRING } [ "," ] ")" "]" ]
           [ "async" ] [ "external" ]
           "fun" MEMBER [ generic-params ]   (* IDENT unless an impl/trait member *)
           "(" [ parameter { "," parameter } [ "," ] ] ")"
           [ ":" type ] [ "borrows" IDENT ] [ context-clause ]
           ( block | ";" ) ;

parameter  = [ "lazy" ] [ "mut" | convention ] [ "..." ] binder [ ":" type ] ;
convention = "own" | "&" [ "mut" ] ;
binder     = IDENT
           | "(" binder "," binder { "," binder } [ "," ] ")"
           | "(" named-binder { "," named-binder } [ "," ] ")"  (* by name, B569 *)
           | "[" binder { "," binder } [ "," ] "]" ;
named-binder = MEMBER "=" binder ;

extern-attr = "[" "extern" "(" extern-args [ "," "retains" ] [ "," ] ")" "]" ;
extern-args = STRING [ "," STRING ]              (* global, or module and symbol *)
            | "method" [ "," STRING ]            (* method on the receiver *)
            | ( "get" | "set" ) "," STRING       (* property read / write *)
            | "new" "," STRING [ "," STRING ] ;  (* construction *)

The four arms are the four host bindings. "method" is the only form word that stands alone: [extern(method)] binds the receiver method of the same name as the vilan function, and [extern(method, "on")] names a different one. get and set always take their property name — a bare [extern(get)] is not an accessor, it is a malformed attribute, and lowers as one. new reads its strings exactly as the first arm does: one is a global class ([extern(new, "TextDecoder")]), two are a module and the class imported from it ([extern(new, "node:sqlite", "DatabaseSync")]), and the call sites emit new Symbol(…) because a host constructor rejects a plain call.

retains is a flag, not a binding form: it is recognized in trailing position only — the one place it cannot displace a form word — and so composes with every shape above rather than needing an arm per combination. It declares that the host keeps an argument past the call; §6.8’s Externs and retention gives the semantics. Elsewhere in the argument list it is an unknown argument, and the attribute lowers to the empty global symbol as any other malformed extern attribute does.

A ; body is a signature-only declaration: legal for external functions and required trait methods. A parameter’s convention may come from the prefix (own x, &mut self) or from a view type (x: &mut T); the prefix wins if both are present (§6.3). The borrows clause names the parameter the returned view projects (§6.5). A closure literal’s parameters take this same rule, conventions included, so a callback can receive a writable view: signal.update(|&mut list| { … }) against a sync |&mut T| void parameter.

A leading mut is binder mutability, not a convention: the body may rebind and field-write its by-value copy, invisibly to the caller — fun f(mut x: T) { … } is fun f(x': T) { mut x = x'; … }. It applies to a plain name binder (including self and closure parameters), never combines with a convention, is not part of the signature (trait conformance ignores it), and is rejected on an external fun (no body). A resource cannot be taken mut (a resource never copies; take it own).

A leading ... marks a spread parameter: a call convention over an ordinary tuple parameter, where the call site writes the pack’s elements out flat — fun f(...items: T) { … } is fun f(items: T) { … } with f(a, b) meaning f((a, b)) (§5.9). It must be the last parameter (so at most one per signature), must declare its type, and takes a plain name binder. Unlike mut it is part of the signature, and it is rejected outside a free fun: on a closure literal, on a trait declaration or any impl member, and on an external fun. It never combines with a convention — the argument is a tuple the call site builds, so there is nothing to transfer or alias — but mut may precede it (mut ...items: T).

Each attribute of the prefix is optional, and the prefix is written in any order (§3.2): the production lists the canonical one, which vilan fmt prints. [deprecated("use …")] leads it: the function is deprecated, and every use in code outside the standard library — a call, a method call, the function passed as a value — still compiles but raises the non-fatal warning `{name}` is deprecated; {steer}, anchored at the using name, once per use site. The one required argument is the replacement steer, carried into the warning verbatim; by convention it reads use … ([deprecated("use two()")] warns `one` is deprecated; use two()). Uses inside the standard library are silent — std migrates its own callers in the release that deprecates. The attribute is honored wherever it appears, in std and user code alike, and dies with its item: when the function is removed, so is the mark. When the item goes away is the CHANGELOG’s fact, not the source’s — the removal comes no earlier than the minor release after the warning first shipped.

The same steer labels a struct, an enum, a trait or a module binding (leading its prefix, as on a function), and a re-export: [deprecated("use pkg::inner::DeltaCursor")] export import pkg::inner::DeltaCursor as KeyedCursor; deprecates the name KeyedCursor the re-export publishes, while the item stays exactly the item — KeyedCursor is still a DeltaCursor. A use of a deprecated type in another module — an annotation, a literal’s head — warns with the function’s own warning. An import line alone does not warn, for a type exactly as for a function: the uses are the fix sites, and a dead import falls out with the last one. The one import that does warn is one that reaches a deprecated re-export — the renamed name is transparent at every other site, so the import is its only use — and importing the item from where it is declared does not. The declaring module’s own uses are silent, as std’s are. A [deprecated] import that is not exported publishes nothing and is refused, and so is a [deprecated] or [internal] on an impl block, which nobody names — label its members. The editor strikes a deprecated name through at its declaration and every use (the standard deprecated semantic-token modifier) and leads its hover with the steer.

[internal("reason")] follows it, and answers a different question. Visibility says whether a module may name an item; this says whether a reader should reach for one that is named — an item exported on purpose and dangerous on purpose, like std::web::ui’s Region::cut_row, which each and a hand-written Slot legitimately need and which corrupts the reconciler’s view when a row is moved through it without hold_rows. The one argument is the reason, and it is required: it is what the editor shows, and a label with no reason is how these rot.

It is not a diagnostic. Nothing warns, nothing refuses, and the item stays exported and callable — the attribute changes what the editor does. Completion omits the name unless what is already typed is an exact prefix of three characters or more, and what survives that is sorted last with the reason as its detail; every use of the name, and the declaration itself, carries a semantic-token internal modifier that a theme dims; hover leads with the reason. A field is the case declaration visibility cannot serve at all, since vilan has no per-field visibility, and it is the case the attribute was asked for.

Two reaches are refused, and both are std’s: a field std labels [internal(..)] is not a member outside std (A149 S4), and a struct literal outside std cannot write one (B568). It is std’s machinery — a store handle’s slot tree and path, a region’s end marker — and as a member it would shadow what a reader writes: field syntax on a store handle reads the handled struct’s fields (app.user.name), and a path there must be the struct’s path, not the handle’s. Reading one is refused with the reason; what a program legitimately needs is a method (Region::end), and a value of such a struct comes from std’s own constructors (Store::new(..)), never a literal. No pattern reaches one either: a pattern names no field (§3.10). A field a package labels in its own code stays a member, and writable by a literal, everywhere, as above.

The same label rides every other declaration a reader may be steered away from: a struct, an enum, one enum variant, a trait and a module binding ([internal("…")] let cache = …;, the labelled-let statement of §3.4). On a struct, an enum or a trait it leads the declaration, ahead of [resource]; on a variant it leads the variant, as on a field. A label on a local let is refused — nothing outside the body can name a local, so the label would have no reader. The editor treats each exactly as it treats a function: hidden from completion below an exact three-character prefix, dimmed at the declaration and at every use (a type position included), and leading its hover. A package that also wants the terminal to say so opts in with [lints] internal_use = "warn" in its vilan.toml: every use of an internal item outside the module that declares it then warns (an import line alone is not a use) `{name}` is internal: {reason}; the declaring module’s own uses, std’s and a dependency’s stay silent.

Structs and enums

struct = [ deprecated-label ] [ internal-label ] { hint-label } [ platform-attr ]
         [ resource-attr ]
         [ "external" ] "struct"
         (IDENT | "null") [ generic-params ]
         ( "{" [ field { "," field } [ "," ] ] "}" | ";" ) ;
field  = [ internal-label ]
         ( [ reactive-attr ] [ expose-attr ] | expose-attr reactive-attr ) MEMBER [ ":" type ] ;
expose-attr    = "[" "expose" [ "(" "keyed" [ "=" type ] ")" ] "]" ;
reactive-attr  = "[" "reactive" "(" reactive-arg { "," reactive-arg } ")" "]" ;   (* A142 S7 *)
reactive-arg   = "coarse" | "name" "=" STRING ;
internal-label = "[" "internal" "(" STRING ")" "]" ;
deprecated-label = "[" "deprecated" "(" STRING ")" "]" ;
resource-attr    = "[" "resource" "]" ;   (* B413 *)
hint-label       = "[" "hint" "(" type ")" "]" ;   (* E227 *)

enum          = [ deprecated-label ] [ internal-label ] { hint-label } [ platform-attr ]
                [ resource-attr ]
                "enum" IDENT
                [ generic-params ] "{" [ variant { "," variant } [ "," ] ] "}" ;
variant       = [ internal-label ] NAME [ "(" [ type { "," type } [ "," ] ] ")" ]
                [ "=" backing-value ] ;
backing-value = [ "-" ] INTEGER | STRING ;
INTEGER       = NUMBER without a fractional part and without a SUFFIX ;

A ;-bodied struct is legal only for external structs (host types). An explicit variant backing value — = 0, = -1, = "start" — fixes what the variant is at runtime. An enum whose variants carry one is a backed enum and lowers to that bare value; see §5.3 of the types chapter.

The two backing types are the integers and str, and nothing else. A float is rejected for the same reason its equality is: the lowering is ===, on which 0.1 + 0.2 is a footgun and NaN is not even equal to itself. bool is rejected because bool is itself an enum that already lowers to native true/false, so a two-variant bool-backed enum is bool with extra steps.

An integer backing value is an integer, not a general NUMBER: a fractional part (= 1.5) and a type suffix (= 1u32, and = 1_000, which lexes as 1 with the trailer _000) are both errors rather than being silently discarded. Hex is read as hex (= 0xFF is 255). The value must lie in -9007199254740991 ..= 9007199254740991 — i53, the widest integer a runtime number holds exactly — because a backed enum is that number at runtime, and a discriminant past the bound would reach the host as a different value than the source wrote. The bound is symmetric. The implicit continuation obeys it too: a variant with no backing value takes the previous variant’s plus one, starting at 0, and running past the bound is an error rather than a wrap.

A string backing must be written on every variant. There is no successor of "start" for the continuation rule to hand out, and the string is deliberately not derived from the variant name — the two are independent (AlignItems::Start is "flex-start", Display::Hidden is "none"), and a naming convention that is right most of the time would be silently wrong the rest.

One enum has one backing type. The type is fixed by the first explicit value in declaration order and every later value must agree: enum X { A = 1, B = "two" } is rejected. An enum has one runtime representation, and a value that is sometimes a number and sometimes a string is not a vilan type.

Two variants may not share a backing value, whether written or continued — enum Dup { A = 1, B = 1 }, enum Walked { A = 1, B = 0, C }, and enum Align { Start = "a", End = "a" } are all rejected. Sharing one would make two variants a single runtime value (see §5.3 of the types chapter), leaving the second match arm unreachable in an otherwise exhaustive match.

A backing value is only legal when every variant is data-less. A variant carrying a payload may not carry one, and neither may its data-less siblings: an enum with any payload variant uses the tagged representation, in which a bare backing value has nowhere to put a payload.

The [resource] attribute marks a type declaration as a resource: the owned-resource class, whose semantics are specified in §6.8. It closes the label prefix and precedes external, so the full order is [resource] external struct, and it is accepted only on struct, enum and trait declarations; [resource] on any other item, a field or a variant is a parse error. On a trait it marks the trait’s OBJECTS: dyn T of a [resource] trait T is move-only and may hold a resource (§5.12). resource is not a keyword (B413 dissolved it into the attribute): it is an ordinary name everywhere else, and the retired spelling resource struct is refused with a steer to [resource] struct.

[hint(Trait<..>)] (E227) names the trait application a struct or an enum is SHOWN as in an editor’s inlay hint, written in the declaration’s own parameters: [hint(Pipe<U>)] [resource] struct Derive<S, T, U> makes a Derive<..> binding hint as : ~Pipe<..>. It is the one built-in attribute whose argument is a type. It changes nothing a program means, and it is checked where it is written: the argument must be a trait application, it may name only the declaration’s parameters and concrete types, some impl of the type must provide that application, and a declaration carries at most one. The shared label prefix also reads it ahead of a trait, a labelled let and an impl, where it is refused — none of them is a type to abbreviate.

Impls and traits

impl  = [ internal-label ] [ platform-attr ] "impl" type [ "with" type { "+" type } ]
        "{" { statement } "}" ;
trait = [ deprecated-label ] [ internal-label ] [ platform-attr ]
        [ "[" "resource" "]" ] "trait" IDENT
        [ generic-params ]
        [ "with" type { "+" type } ] "{" { function } "}" ;

An impl’s subject is a type pattern: type X [: bounds] binders anywhere inside it (impl List<type T>, impl Option<(type T, type U)>, bare impl type T) declare the impl’s generic parameters (§5.6). A binder the head never mentions again is written _ instead — the pattern wildcard’s spelling, with the same optional bound (impl Source<Option<_: Source<type U>>>); each _ is a parameter of its own, so two of them in one head are two parameters, exactly as Some(_, _) binds nothing twice. type _ is accepted and vilan fmt prints it as _. A binder’s bound may be a tuple bound instead of a trait-bound list, exactly as a generic parameter’s may (§5.9): impl type T: (2..) with Tuple { … } is a blanket over every tuple of two or more elements, and impl selection admits a receiver the way a tuple-bounded parameter admits an argument — arity inside the range, every element satisfying the element bound. An array’s LENGTH binds the same way (types §5.1, Fixed arrays): _ or const N in a subject’s length position (impl [type T; _], impl [type T; const N]) is a length parameter, _ the one the head declines to name; at a const parameter’s argument position (impl Ring<type T, const N>) the binder is const N or _. A literal length outranks a length binder by the specificity order (impl [i32; 3] over impl [type T; _]). with lists the implemented trait(s). An impl without with provides inherent members. A trait’s with lists supertraits.

Generic parameters and arguments

generic-params = "<" generic-param { "," generic-param } [ "," ] ">" ;
generic-param  = [ "type" ] IDENT [ ":" ( bound-list | tuple-bound ) ]
                 [ "=" type ]
               | "const" IDENT ;                (* a length parameter *)
bound-list  = type { "+" type } ;
tuple-bound = "(" [ NUMBER ] ".." [ NUMBER ] [ ":" type ] ")" ;
generic-args = "<" generic-arg { "," generic-arg } [ "," ] ">" ;
generic-arg  = type | NUMBER | "const" IDENT ;  (* a length; a binder in an impl subject *)

A tuple bound constrains a variadic tuple parameter’s arity and, optionally, each element (T: (2..), T: (..: Display)); see §5.9.

const N declares a length parameter (types §5.6, Length parameters): fun total<const N>(values: [i32; N]), struct Ring<T, const N> { slots: [T; N] }. It takes no bound and no default. A NUMBER as a generic argument is the length a const parameter takes (zeros<4>(), Ring<i32, 4>); a name there is read as the type path it is spelled as, and the parameter’s kind decides what it must name — a length parameter at a const position, a type at any other.

Attributes and macro items

derived-item   = derive-attr ( struct | enum ) ;
derive-attr    = "[" "derive" "(" IDENT { "," IDENT } [ "," ] ")" "]" ;
service-item   = { service-attr | client-service-attr }- struct ;
service-attr   = "[" "service" [ "(" service-args ")" ] "]" ;
service-args   = service-arg { "," service-arg } ;   (* a client name leads or is absent; each other arg at most once *)
service-arg    = IDENT | "http" | "client" "=" IDENT ;
client-service-attr = "[" "client_service" "]" ;
macro-attributed-item = macro-attr ( struct | enum | function ) ;
macro-attr       = "[" IDENT [ "(" [ expr-span { "," expr-span } ] ")" ] "]" ;
macro-fun        = "macro" function ;   (* written in §3.2's order: attributes, [ "async" ] "macro", "fun" *)
macro-invocation = "macro" IDENT "(" [ expr-span { "," expr-span } ] ")" ;
macro-block      = "macro" block ;

A macro attribute’s arguments are captured as source spans: the macro receives their text, not their values (§10). The built-in attribute names (derive, service, client_service, extern, must_use, track_caller, rpc, trait_only, doc, expose, platform, deprecated, internal, resource, hint, reactive) are not available as user macro-attribute names.

[reactive(..)] is a field’s store knobs, read by [derive(Storable)] (std::reactive::store) and by nothing else: coarse makes the field one slot, compared whole, and name = "x" generates its projection as x(). The name must be an identifier. It may stand on either side of [expose].

[service(..)] and [client_service] may be written in either order on one struct; a struct carrying both is peer-to-peer and expands once (proposal/transport-rpc.md §9.3). http in [service(..)] is a marker, never a client name: [service(http)] keeps the default <Struct>Client (transport-rpc.md §9.7.5).

3.4 Bindings and assignment

let        = [ "lazy" ] ("let" | "mut") binder [ ":" type ]
             [ "=" expression ] ;
labelled-let = ( deprecated-label [ internal-label ] | internal-label )
               [ "lazy" ] ("let" | "mut") IDENT [ ":" type ]
               [ "=" expression ] ";" ;
assignment = [ "*" ] place ( "=" | "+=" | "-=" | "*=" | "/=" | "%=" )
             expression ;
place      = chain ;                 (* an assignable location, §3.6 *)
ret        = "ret" [ expression ] ;
jump       = "jump" IDENT ;          (* break | continue *)

let binds immutably, mut mutably; a tuple binder destructures (irrefutably: names and nested tuples only), one sub-pattern per element of the value’s own shape — let ((a, b), c) = ((1, 2), 3); — and a pattern of another arity, or over a value that is no tuple, is refused. Both the type and the initializer are syntactically optional. lazy is accepted only on a MODULE-LEVEL let binding one name to one initializer (§6.10): a lazy mut, a lazy destructure, a lazy binding with no initializer and a lazy local are each refused. A place is a chain expression (§3.6) denoting a location: a local, a field chain, an index, or a place reached through a call (a.write().count), a call that returns a &mut view (cell.write() += 1), or a tuple of places ((a, b) = (b, a)); the optional leading * assigns through a view. The grammar reads any chain there, so a left side that denotes no location — -x, !b, (x + 1), await p, a literal, a value-returning call, a variant constructor — is refused by the checker, once per assignment. jump break / jump continue control the innermost enclosing loop.

An assignment is a statement: its value is void, and it stands only where a value is discarded — an expression statement (x = 5;), a block’s trailing expression ({ x = 5 }), a match arm’s body (A => x = 5,), a closure’s expression body (|v| total += v), and a then/else branch of a conditional that itself stands in one of these (c then x = 5;). Anywhere its value would be USED — inside parentheses, as an argument, a tuple or list entry, a let initializer, the right of another =, an operand — it is refused, naming the statement to write before it (B569, named-tuple-fields.md §3.2). Inside parentheses name = value is a tuple’s label (§3.6), never an assignment.

3.5 Blocks and control expressions

block      = "{" { statement } [ expression ] "}" ;
if-expr    = "if" condition-expr block [ "else" ( block | if-expr ) ] ;
for-expr   = "for" binder "in" condition-expr block  (* iteration *)
           | "for" condition-expr block              (* while *)
           | "for" block ;                           (* infinite *)
match-expr = "match" condition-expr "{" { match-leg [ "," ] } "}" ;
match-leg  = pattern { "," pattern } [ "if" expression ] "=>" expression ;

A block’s value is its trailing expression, or void if none. Conditions and match/for subjects are condition expressions (§3.8): struct initializers and css blocks are excluded there, keeping if Foo { unambiguous. A match leg’s comma-separated patterns form an or-pattern; the optional if guard applies to the whole leg; the trailing comma after a leg is optional.

A for <binder> in subject must have one element type, which the binder takes: a List<T>, a HashSet<T>, a [T; n], a str (yielding characters), or any type providing the iterator protocol next(&mut self): Option<T> (§5.7 dispatches it; for e in &mut c drives next_mut and binds each element as a view, §6). A tuple is not iterable, and the loop over one is refused: a tuple is a fixed sequence of independently typed elements rather than a container of one element type, so there is no single type for the binder to take and one body cannot be checked for every element. Read the elements positionally (t.0, t.1) or destructure the tuple (§5.9). The header’s binder is let’s (binder, §3.3), so a tuple or array binder destructures each ELEMENT — for (index, item) in list.iter().enumerate() — with exactly the rules a destructuring let takes; any other pattern is refused by name, and the element is bound and destructured in the body instead. A tuple family is the exception (§5.9): a loop over a mapped tuple (U in T: F<U>), or over a value of a tuple-bounded parameter T: (2..) (read as (U in T: U)), binds the element template F<U> at a U fresh to the loop and checks the body ONCE, parametrically — the binder has a type, and the body may use only what U promises. A &mut walk over a family is still refused: a family has elements, not element views. Whether a loop over a CONCRETE tuple should instead be UNROLLED — the body checked and emitted once per element, at that element’s own type — is recorded future work; the refusal is forward-compatible with it.

3.6 Chain expressions (postfix)

The tightest expression tier, chain:

chain   = path { call-suffix | postfix } ;
path    = ( IDENT generic-args ␣"::"  (* generic static head *)
          | "[" type ";" array-length "]" ␣"::"   (* array static head *)
          | atom )
          { "::" MEMBER } ;
call-suffix = [ generic-args ] "(" [ entry { "," entry } [ "," ] ] ")" ;
                                          (* an argument: its `entry` may be NAMED *)
member  = NUMBER                          (* tuple index: .0 *)
        | MEMBER [ call-suffix ] ;        (* field / ONE fused method call *)
MEMBER  = IDENT | RESERVED ;              (* any word, B414 S4 (§2.2) *)
postfix = "." member                     (* span-adjacent: `.` then the member *)
        | "[" expression "]"             (* index *)
        | "!"                            (* try-assert, §5.10 *)
        | "(" [ entry { "," entry } [ "," ] ] ")"
                                          (* direct call on the chain result *)
        | "?." member                    (* lift link, §5.10 *)
        | "?"                            (* expression lift, §5.10 *)
        | "as" ascribed-type ;           (* ascription, §5.8 *)
ascribed-type = type ;   (* every generic list in it span-adjacent, below *)

atom    = literal | IDENT | IDENT generic-args | struct-init
        | "(" expression ")" | tuple | list
        | tuple-comprehension | macro-invocation | macro-block
        | element | css-block ;
literal = NUMBER | STRING | "true" | "false" | "null" | "void" ;
tuple   = "(" ( spread | entry "," entry { "," entry } [ "," ] ) ")"
        | "(" labelled-entry ")" ;        (* the one-slot labelled tuple, B569 *)
entry   = spread | labelled-entry | expression ;
labelled-entry = MEMBER "=" expression ;  (* a label / a named argument, B569 *)
spread  = ".." expression ;
list    = "[" [ expression { "," expression } [ "," ] ] "]"
        | "[" expression ";" array-length "]" ;   (* repeat: the value per slot *)
tuple-comprehension = "(" comprehension-binding { "," comprehension-binding }
                      "=>" expression ")" ;
comprehension-binding = IDENT "in" secondary-expr ;

element      = "<" element-name { head-item }
               ( "/>" | ">" { child } "</" element-name ">" )
             | fragment ;
fragment     = "<>" { child } "</>" ;              (* a List<View> literal *)
head-item    = "." member                          (* a chain link, verbatim *)
             | "on" ":" IDENT "(" expression ")"   (* event form *)
             | element-name [ "(" expression ")" ] ;
                                          (* attribute; bare name = boolean *)
element-name = NAME { "-" NAME } ;   (* NAME: an identifier or any keyword *)
child        = element | STRING | ISTRING | "{" expression "}" ;

css-block    = "css" css-body ;        (* atom position; excluded in conditions *)
css-body     = "{" { css-item } "}" ;
css-item     = css-declaration | css-rule | css-link ;
css-declaration = css-property "(" [ expression { "," expression } [ "," ] ]
               ")" ";" ;               (* a CALL: the property is the name *)
css-property = { "-" } element-name ;  (* span-adjacent, as an element name is *)
css-rule     = "." MEMBER [ "(" [ expression { "," expression } [ "," ] ] ")" ]
               css-body ;
css-link     = "." MEMBER [ "(" [ expression { "," expression } [ "," ] ] ")" ]
               ";" ;                   (* a chain link, verbatim *)

A member name is any word, a reserved one included (B414 S4, §2.2): event.type, bag.if(), found?.match(), Kit::if(). The positions that take one — the name after a member ./?., a head item’s or a css item’s dot, a segment after ::, a struct field, a literal field given with =, a method declared in an impl or trait, an impl selector’s member — are each entered after a token that commits to them, so no keyword reading can begin there and the admission needs no lookahead. A member dot and its name are span-adjacent: nothing, not a space and not a line break, stands between . (or ?.) and the member (R-k). Without the rule a half-typed list. at the end of a line would take whatever the next line starts with as its member — helper in helper();, and under the member tier even let — and swallow the next statement, the way E142’s rule keeps a :: path from doing (§3.2). A chain written over several lines breaks BEFORE each dot — value on one line, .name() opening the next — which the rule leaves exactly as it was. A space on the dot’s own line is refused with the member still read; a line break after the dot takes nothing from the next line.

Name<Args> is read as a generic path head only when :: immediately follows (List<str>::new()); otherwise < is a comparison. A fixed-array type heads a path the same way (A171): [i32; 3]::default(), [str; 2]::from_json(text) — [T; n] is read as the array TYPE only when :: immediately follows and its element parses as a type, and otherwise [ opens a list or repeat literal. A literal before :: ([1, 2]::len(), [0; 3]::default()) names no type and is refused. A member fuses at most ONE call; a further (args) is a direct call on the chain’s result, calling a closure-typed value (self.hook.read()(a, b)). A ?. link’s continuation extends through the following plain postfixes up to the next ?. or !: a?.b.c()! lifts b.c() into the container, then try-asserts the result (§5.10). A bare ? — a ? with no . after it — is the expression lift: it takes no member and lifts its whole enclosing SLOT rather than a continuation, so the slot receives the container (§5.10). It has been in the language since 2026-07-16 and had no production here until now.

EXP as T is a type ascription (§5.8): a postfix of this tier, so a + b as f64 is a + (b as f64), -x as T is -(x as T), await p as T ascribes the promise, and the chain goes on after the type — a() as A .b() as B is ((a() as A).b()) as B. A type never holds a ., so a . after it is the next link; a [ is an index, a ( a direct call, and ?/! the lift and the try-assert of the ascribed value. An ascription is not absorbed into a ?. link’s continuation: what stands left of as is the whole chain so far. as is a contextual word (§2.2): it is read here only after a complete operand and only when a type can begin after it, so let as = 5;, as(2) and import a::{b as c} keep their meanings.

The type after as ends where the type grammar (§3.9) ends it, with ONE rule of its own, the whitespace rule: inside an ascribed type a < opens a generic list only when it is span-adjacent to the name before it. So n as usize < limit is a comparison, xs as List<usize> an ascription to a generic type, and x as List<i32> > y a comparison after a complete list; as (T) is the escape, n as (usize) < limit. A spaced list no comparison could be — inside another generic list, or one whose > is followed by ;, ,, a closer or . (xs as List <i32>;) — is refused with the tight spelling, read as written tight, and vilan fmt writes it so. The rule binds in expression position too (E291): after a name — a callee, a member call, a :: path head, a struct literal’s head — a < opens a generic list only when it touches the name, so a < b > (c) is two comparisons and f<T>(x) a generic call; a spaced list no comparison could be, one whose > is followed by :: or { (List <i32>::new()), is refused with the tight spelling and read tight. A spaced CALL keeps the comparison reading — pick <f64>(1, 2) is (pick < f64) > (1, 2) — but when its name is a generic function or a generic type that reading cannot be what was meant, and the checker refuses it once, naming the spacing and the tight call (pick<f64>(1, 2)), in place of the comparisons’ own refusals (E309). In a type annotation a spaced generic list parses as before (and the formatter tightens it). A closure type after as is read greedily, as everywhere — f as |i32| i32 — and vilan fmt prints it parenthesized, f as (|i32| i32).

value as auto T (and the bare value as auto) is an ascription the toolchain writes and keeps (§5.8): it never directs the value, and vilan check --fix rewrites it when the stage’s type moves — a stage hint frozen into the file.

A match, if, for or { form is complete at its closing brace (the block-like rule, §3.8), with one continuation it admits: as. match k { .. } as dyn Flow<i32> ascribes the whole form, in a value position and at a statement’s head alike — where the as must stand on the brace’s own line, because on a later line it is a name beginning the next statement. After the type the form is an ordinary operand: the chain and the operators go on.

A leading .. marks a tuple-value spread (§5.9). It is recognized only where an entry begins — a tuple construction’s entry, or a call argument — so .. after an expression is unaffected and remains the member-access dots it has always been ((1..3, x) is not a spread). A tuple construction whose only entry is a spread is still a tuple, not a parenthesized group: (..a) is the concatenation of one, and (e) is a group as before. There is no type-level spread; (..T, U) does not parse.

A tuple entry may carry a label (B569, named-tuple-fields.md §2): (x = 5, y = 7), the struct literal’s name = value spelling, which inside parentheses is free because an assignment has left value position (§3.4). The label is a MEMBER, so (type = "a", if = true) is legal. A tuple labels every written entry or none, and names each label once; a spread brings its operand’s slots with their labels, so (..p, z = 3) over a labelled p is labelled and over an unlabelled one is refused. MEMBER "=" decides it at the second token — == and => are tokens of their own — and an i-string’s interpolation hole holds an expression, never an entry. (x = 5) is the one-slot labelled tuple (x: i32): the label is what makes it a tuple, where (5) stays a group. There is no shorthand: (x, y) is positional and never means (x = x, y = y). (x = 5); as a statement builds a tuple and discards it, and is refused with the assignment it was meant to be.

The same entry in an argument list is a named argument (B569 S4, named-tuple-fields.md §8): a spread parameter collects its arguments into one tuple literal, so draw(x = 1, y = 2) against fun draw(...at: (x: f64, y: f64)) collects (x = 1, y = 2) and matches it by name (types §5.9). The collected arguments are named or none; a name no spread parameter collects — a callee without one, a fixed parameter before it — is refused, naming the assignment the author may have meant. There are no default arguments.

An element appears only in atom position, where < begins no other expression; after an operand, < remains a comparison (x < <div/> is a comparison whose right operand is an element). />, the closing marker </, the on: joint, and the - joints of a hyphenated name are span-adjacent token pairs, the shift-operator discipline (lexical spec §2.4). The closing tag’s name must match the opening tag’s token for token. In a head item, an undotted name is an attribute (a bare name is a boolean attribute) and a leading . is an ordinary chain member — the grammar never consults any method list. Text children are quoted strings; bare text is a parse error. An element is an ordinary expression: it desugars before analysis to the std::web::ui view chain (view("tag") with one method call per head item and a .child(…) per child), and postfix suffixes apply to it (<div />.show(flag)).

A head’s chain link must hand the element back (B620): the method it calls returns the element’s own type — View, as every one of std’s attribute and binding methods does, or Self from a trait — so the head stays a description of the element it opens. Any other method is refused at the link’s name, naming the method and where it comes from: <div .len() /> is “len is a method of View, but an element head takes only methods that hand the element back (View); call it on the element instead: <div />.len()”. The rule is about the HEAD alone: the same call written after the element, <div />.len(), is an ordinary postfix call on a View value and takes any method, whatever it returns. A name no method answers keeps the ordinary member error. The lowering of an admitted link is unchanged.

A fragment <>…</> is the nameless head, and it is a different lowering rather than an element with no tag: it desugars to a LIST LITERAL of its children, so its type is List<View>. <> and </> are span-adjacent pairs like /> and </, and neither is a lexical token — < and > are the same control characters the element and generic-argument rules use, so nothing about a comparison changes. A fragment takes no head items and has no self-closing form. Its type is where its uses are: a child position and every position a list fills (A46); it is not a View, so it is not a fun …: View return, a when body, a swap render or an each row.

A css block is the same shape on the style side, and it appears in atom position too — but where an element occupies grammar space nothing else could want, a css block is brace-initial, so it is excluded from condition operands exactly as a struct initializer is (§3.8). css is a reserved word (lexical spec §2.2); the block is the keyword followed immediately by {.

Inside the body the dot decides, and decides alone: an undotted item is a declaration and a dotted one is a method call, so the grammar never consults any method list and a method added to Style cannot change what an existing block means. What FOLLOWS a dotted head splits it, exactly as it does in a head item’s element syntax: a { … } body makes a condition rule, and a ; makes a chain link — a verbatim method call spliced into the chain at its written position (.ghost();, .flex_row();, .custom(a, b);). A bare member and an empty argument list are the same call, so .ghost; and .ghost(); both mean .ghost(). A property name is a span-adjacent name---name run, the element-name rule (so flex-direction is three tokens and --color-ink is five, while data - id is arithmetic). A declaration is a call: the property is the name and the value is its ordinary vilan expression arguments (outline("none");, width(pct(100));, --brand-ink(gray(900));), so there is no value grammar and no {expression} hole — a typed value is simply an argument, checked where the type system already lives. SEVERAL arguments are ONE value joined by a single space, CSS’s own list separator (margin(px(4), px(8)) is margin:4px 8px). The ; is required after every declaration, the last one included: the formatter may never invent a token. A condition rule’s parenthesized arguments are ordinary expressions (.on(within(attribute("data-theme").eq("dark")) + hover()) { … }).

Like an element, a block is an ordinary expression that desugars before analysis — to the std::web::style chain: style(), then .raw(property, value) per declaration and .name(args…, style() … ) per condition rule, with the rule’s own chain appended as the final argument, in written order. @ does not lex at all, so there are no at-rules inside one (lexical spec §2.4); # lexes (it is the import reach marker) but begins no expression, so there are no hex literals either.

3.7 Operator precedence

From tightest to loosest; every binary level is left-associative:

LevelOperatorsNotes
1:: paths, calls, . [] ! ?. ?, as T§3.6; as ascribes (§5.8)
2prefix ! - await async & &mut *unary; async also takes a block
3* / %
4+ -
5<< >>the two control tokens must be span-adjacent
6&bitwise and
7^bitwise xor
8|bitwise or
9== != < <= > >=one level; a < b < c parses as (a < b) < c (ill-typed, §5.7)
10is patternat most one per operand (no chaining)
11&&
12||
13then … elseB459; right-associative, above assignment (§3.8)

Bitwise operators bind tighter than comparisons (a & b == c is (a & b) == c).

3.8 The expression tiers

expression     = "const" expression        (* weak prefix: captures to the end *)
               | secondary-expr ;
secondary-expr = closure | block | if-expr | for-expr | match-expr
               | jump | let | ret | assignment
               | conditional-expr ;        (* §3.7 levels 1–13 *)
conditional-expr = operator-expr [ "then" expression "else" expression ] ;
then-statement = operator-expr             (* at a statement's head only *)
                 ( "then" expression [ "else" expression ]
                 | "else" expression ) ;   (* the guard *)
condition-expr = secondary-expr ;    (* struct-init and css-block excluded *)

struct-init   = type-path                      (* §3.9; qualified heads too *)
                "{" [ init-field { "," init-field } [ "," ] ] "}" ;
init-field    = IDENT [ "=" expression ]      (* shorthand: name alone *)
              | MEMBER "=" expression ;     (* a reserved field name, B414 S4 *)
closure       = ( "||" | "|" [ closure-param { "," closure-param } [ "," ] ] "|" )
                [ ":" type ] expression ;
closure-param = parameter ;   (* the same rule as a function's, less "..." *)

operator-expr  = unary-expr { binary-op unary-expr } [ "is" pattern ] ;
                 (* SHAPE only: §3.7's table is normative for precedence and
                    associativity, and for the one-`is`-per-operand rule *)
unary-expr     = { "!" | "-" | "await" | "async" | "&" [ "mut" ] | "*" }
                 ( chain | block ) ;   (* "async" also takes a block *)
binary-op      = "*" | "/" | "%" | "+" | "-" | "<<" | ">>"
               | "&" | "^" | "|"
               | "==" | "!=" | "<" | "<=" | ">" | ">="
               | "&&" | "||" ;

Two consequences of the tier split are normative:

  • A struct initializer is an operand of the operator/postfix chain (Point { … } == q compares; Point { x = 1, y = 2 }.length() folds the member chain), except in condition positions: an if/for condition, a for … in iterable, and a match subject parse condition-expr, whose operands exclude struct initializers, so the { after if Foo is the block. Parenthesize a literal to use it in a condition (if p == (Point { x = 1 }) { … }). The head is a type-path, so a qualified literal (shapes::Dot { x = 1 }) is the same production and takes the same exclusion — if k == shapes::Kind::Flat { … } reads the { as the block. A css block (§3.6) is brace-initial for the same reason and is excluded in the same three places, with the same escape: if (css { … }).class_list() != "" { … }.
  • const captures weakly: everything to the end of the expression (up to the enclosing bracket or comma) folds; parenthesize to narrow (§9).

A closure’s body is one expression (commonly a block). || in operand position always begins a zero-parameter closure; logical-or is only recognized between two operands.

then / else (B459) is SUGAR over if, desugared before analysis: c then a else b is if c { a } else { b }, and typing, the scope of c’s is bindings (they reach a, not b), emission and diagnostics are the if’s. As a conditional-expr — a VALUE — the form needs both branches, exactly as an if used for its value does; a then without its else, or a bare else, in value position is refused. It binds looser than || and tighter than assignment: a || b then x else y tests a || b, and v = c then x else y assigns the form. Each branch is a whole expression, so the form is right-associative: a then x else b then y else z is an else if chain, and an else belongs to the nearest then that has none.

At a statement’s HEAD, a form ended by the statement’s ; is a then-statement, read as the statement forms c then S; (if c { S; }), c else S; (if c {} else { S; }, the guard) and c then S else S; (if c { S; } else { S; }): each branch is one statement whose value is discarded, so the branches need not unify, and a branch that is itself a form is read as a statement too. A branch has no block of its own, so a let as a branch is refused. The one rule that is not a rewrite (R15): when the else statement of a statement form DIVERGES — ret, jump, a panic(…), an endless loop — the statement can be left only along the condition’s TRUE path, so the is bindings that path makes reach the rest of the enclosing block, from the statement’s end (opt is Some(let v) else ret; binds v below it). A capture under ! or in one operand of || is not proven by the condition holding and is not extended; the keyword if keeps its own scoping. The same form parenthesized, or at a block’s end with no ;, is a value. then is a contextual keyword: it is read only after a complete operand, where no name can stand, so let then = 1; and promise.then(f) are unaffected.

3.9 Types

type = "&" [ "mut" ] type                       (* view type *)
     | ( "type" IDENT | "_" ) [ ":" ( bound-list | tuple-bound ) ]
                                                 (* impl-subject binder *)
     | [ "async" | "sync" ] closure-type [ context-clause ]
     | "dyn" type-path                           (* trait object, §5.12 *)
     | "auto" [ type ]                           (* toolchain-kept, §5.8 *)
     | type-path                                 (* nominal *)
     | "(" IDENT "in" type ":" type ")"          (* mapped tuple, §5.9 *)
     | "(" [ tuple-slot { "," tuple-slot } [ "," ] ] ")"  (* tuple type *)
     | "[" type ";" array-length "]"            (* fixed array, §5.1 *)
     ;
array-length   = NUMBER                         (* unsuffixed or `usize` *)
               | IDENT                          (* a length parameter, or a named length *)
               | "_" | "const" IDENT ;          (* impl-subject binders *)
tuple-slot     = [ MEMBER ":" ] type ;     (* labelled: every slot, or none (B569) *)
type-path      = IDENT { "::" IDENT } [ generic-args ] ;
closure-type   = ( "||" | "|" [ [IDENT ":"] type { "," [IDENT ":"] type } "|" )
                 [ type ] ;
context-clause = "context" ( IDENT | "(" IDENT { "," IDENT } [ "," ] ")" ) ;

context here is the contextual keyword (§2.2); in TYPE position the clause is only valid on closure types, checked semantically (§8.5), and it is part of the closure’s type wherever one may be written — a parameter, a let annotation, a struct field, a generic argument, a return type. The same production is the DECLARATION clause a fun may carry (§8.6), and the RETURN TYPE is where the two meet: the type rule above takes the clause first, and the declaration peels it back off only when the return type cannot carry one. So fun f(): i32 context settings binds its clause to the FUNCTION (i32 is the return type), while fun f(): (|| i32) context settings leaves it on the closure TYPE — the returned closure is injected (§8.5). The one shape this position cannot spell is an UN-PARENTHESIZED closure return type carrying a clause (fun f(): || i32 context settings): the greed takes the clause onto the closure’s own return type, which cannot carry one, so the form is refused with both parenthesized readings named. With no return type at all the clause is read by the function production above instead. Written after the return type it precedes a borrows clause, and written without one it follows it; the formatter prints it where it was written.

A fixed array [T; n] holds n values of T, the length part of the type (types §5.1). Its length is a whole number (unsuffixed or usize), the name of a length parameter in scope ([i32; N] under <const N>), a NAMED length — an immutable binding, let or const let, at any scope the name resolves in (imports included), whose initializer is such a number or another named length (const let SIZE = 16;, [u8; SIZE]) — or, in an impl subject only, a length binder, _ or const N. A binder written anywhere else is refused, as _ is in an annotation. The same array-length follows the ; of a repeat literal [value; n] (§3.6). A length that needs evaluating (2 * K, a call, a mut, a parameter) is refused, and the message says why: lengths are known before the program is typed, and const evaluation runs after.

A tuple type’s slots may carry labels (B569): (x: f64, y: f64). MEMBER ":" at a slot’s head is a label (decided at the second token, so a::B stays a path and (U in T: …) a mapped tuple); every slot is labelled or none, each label once. A labelled one-slot type (x: i32) is a tuple, where (T) stays a group. Labels name positions and are no part of the type’s identity (types §5.9).

auto is contextual by dyn’s rule — the marker at a type’s head, except auto::, a path into a module named auto. It stands at a function’s return, a let or mut annotation and after as, and is refused everywhere else (a parameter, a field, a generic argument, a trait member’s return); the type after it is optional, and with none the annotation ends at the =, the {, the ; or a context/borrows clause — the bare auto, which vilan check --fix fills (§5.8).

dyn takes a type-path and nothing else: the keyword erases a TRAIT’s implementation, so a closure type, a tuple, an array or a view after it names nothing that could be erased and is a parse error. A dyn type stands wherever a type stands, nesting included (List<dyn Source<i32>>), and the trait it names must be object-safe (§5.12).

sync is likewise contextual (§7.4: the synchronous contract; parameters only). A closure type’s parameters may carry documentation names (|value: T| U); only the types are significant.

A type-path names a type, optionally through the modules that declare it: Style, List<T>, style::Style, std::reactive::SignalCell<i32>. Every segment but the last selects a namespace, and the resolution is the one an expression path uses (§4.2, §4.6) — so a module in scope reaches its types exactly as it reaches its values. Generic arguments belong to the LAST segment, the only one that names a type; there is no List<str>::Item in type position (a generic path head, §3.6, is an expression form).

Every type position takes a type-path: a return type, a let annotation, a parameter, a struct field, an impl subject, a trait bound, a generic argument, and any of these nested inside another type form.

3.10 Patterns (match)

pattern = ("let" | "mut") binder                (* binding *)
        | "(" pattern "," pattern { "," pattern } [ "," ] ")"
        | "(" named-pattern { "," named-pattern } [ "," ] ")"  (* by name *)
        | STRING | MULTILINE_STRING | NUMBER    (* equality literal *)
        | "_"                                   (* wildcard *)
        | NAME { "::" IDENT }
          [ "(" [ pattern { "," pattern } [ "," ] ] ")" ] ;  (* variant *)
named-pattern = MEMBER "=" pattern ;

A tuple pattern may be written by name (B569): let (y = top, x = left) = p;, (x = 0, y = let v) — the label, then what its slot meets, the literal’s spelling. It names exactly the matched value’s labels, in any order (the value must be a labelled tuple), and is placed slot by slot once the value’s type is known; every element is named or none, and a one-element by-name pattern (x = a) is a tuple pattern. A positional pattern ignores labels.

Bindings inside patterns are written explicitly (Some(let x)), so a bare name is always a variant reference, never a fresh binding: the classic mistyped-variant trap is a resolution error instead of a silent catch-all. bool and null literals match as variants of their enums.

let and mut are the two binding forms here exactly as they are at a declaration (§3.4): Some(let list) binds immutably, Some(mut list) binds mutably, and mut at a tuple or array binder stamps every name under it. A binder is a binding, so it takes rule 1’s copy like any other (§6.1): mutating list leaves the matched value alone, and an arm that means to change the subject assigns back through it. Writing both forms — Some(let mut list), in either order — is refused, and the refusal names Some(mut list). The let/parameter binder grammar (names and tuples, §3.3) is the irrefutable subset; refutable forms (literals, variants) are match-only. A tuple pattern is irrefutable only when its elements are: (let a, let b) is a destructure and matches everything, (1, 2) is a test.

A match must be exhaustive, and exhaustiveness is proven by unguarded legs only — a guard tests the value, which the check does not reason about, so a guarded leg proves nothing about what the match covers. What an unguarded leg proves is the value-space its whole pattern tree covers, not its root, and the check descends accordingly:

  • an enum position needs every variant named, and each named variant’s payload positions covered in turn, so Pair::Of(Align::Start) alone leaves Pair::Of(Align::End) missing however few variants Pair has;
  • a tuple position needs its elements covered — as a product, so (1, 2) and (3, 4) leave every other pair missing;
  • an open position (i32, str, a struct, a still-abstract type parameter) is covered only by a binder or _. No number of literals exhausts it: Wrapped::Of(1), Wrapped::Of(2), … is never total.

A subject whose type is not yet known, or is any, never, or a generic parameter, is exempt: the question is which values the subject can take, and there is no answer to give. The diagnostic names one uncovered value as a pattern that would cover it — “missing Pair::Of(Align::End)” — or asks for a catch-all where the hole is the whole domain. The consequence lands on the last leg, the one that answers for whatever the legs above it did not take: a match whose final leg is guarded must be exhaustive without it, so match a { A => …, B if c => … } is refused whatever the subject’s type (“match is not exhaustive”). Write the guard with a catch-all after it — B if c => …, _ => … — and the value the guard rejects has somewhere to go. A guard is tested wherever its leg stands, the last one included; no leg is silently promoted to a catch-all.