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:
- generation —
[derive(..)],[service(..)],[client_service(..)], a macro attribute (among themselves, as written); - the labels —
[deprecated(..)], then[internal(..)], then[hint(..)]; - the binding —
[extern(..)]; - the checks —
[must_use], then[track_caller], then[rpc], then[trait_only]; - the fence —
[platform(..)]; - 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 includesexportandmacroahead 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:
| Level | Operators | Notes |
|---|---|---|
| 1 | :: paths, calls, . [] ! ?. ?, as T | §3.6; as ascribes (§5.8) |
| 2 | prefix ! - 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) |
| 10 | is pattern | at most one per operand (no chaining) |
| 11 | && | |
| 12 | || | |
| 13 | then … else | B459; 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 { … } == qcompares;Point { x = 1, y = 2 }.length()folds the member chain), except in condition positions: anif/forcondition, afor … initerable, and amatchsubject parsecondition-expr, whose operands exclude struct initializers, so the{afterif Foois the block. Parenthesize a literal to use it in a condition (if p == (Point { x = 1 }) { … }). The head is atype-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. Acssblock (§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() != "" { … }. constcaptures 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 leavesPair::Of(Align::End)missing however few variantsPairhas; - 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.