Spec §4 — Names, modules, and packages
4.1 Modules
A module is one source file. Its top-level statements form its body; its
declarations (fun, struct, enum, trait, impl, module-level
let, mod blocks) are its items. There is no separate module
declaration: a file routes.vl in a package’s source root is the module
routes of that package.
The one statement that speaks about the file’s module itself is
mod self; — self is the file’s own module, the statement has no
body, and it must be the file’s first statement. It is the host for
attributes about the whole file: today [platform("browser")] mod self;
(§11.3). self is reserved for it, so a nested module cannot be named
self (mod self { … } is refused).
Modules form a tree, and a directory under the source root is a
module path. a.vl or a/lib.vl is the body of module a (both
present is an ambiguity error, §4.2), and a/b.vl is the module a::b
whether or not a has a body of its own; nesting is unbounded, so
a/b/c.vl is a::b::c. A directory with no body is a pure
namespace: it holds modules but is not one, and an import naming it
is a diagnostic listing the modules it does hold.
lib is a body’s file name, not a path segment: a/lib.vl is what
pkg::a resolves to, so pkg::a::lib would name one file under two
names and is a diagnostic steering to pkg::a (whose items are that
file’s). The rule reads the DIRECTORY: a lib.vl at the source root is
the ordinary module lib, because the root is nobody’s body, and a
directory literally named lib (a/lib/x.vl) is a path segment like any
other.
A module is reached by its own path. import pkg::a binds the items
of a’s body and nothing below it; a::b is reached by
import pkg::a::b. Item lookup through a module in expression position
(a::item) reads that module’s items only, so what a path means never
depends on which other files a build happens to load.
Loading is not lookup, and a child pulls its parent. Importing
pkg::a::b LOADS a’s body too, when a has one — a is a node on the
path to a::b, and the tree is built by loading each segment that names a
file (Rust’s model, where a::b is reached through a’s own mod b;).
Nothing of a’s becomes visible by it: the own-path rule above is
unchanged, and a::item after import pkg::a::b still does not resolve.
What is paid is the COMPILE: a.vl is parsed and analyzed, and its
diagnostics are the build’s. This is stated because it is payable — a
directory whose a.vl does real work is a cost every one of its children
imposes on any build that reaches one, so a heavy parent beside light
children is worth splitting. A directory with no body (the pure namespace
above) has nothing to load and costs nothing. Emission is unaffected: it
follows reachability, so a parent body nothing references contributes no
output.
What a parent DOES contribute is its impl blocks. Items are reached
by a module’s own path and nothing below it; implementations are not
items, and a plain import pkg::a::b; admits every block declared along
the path — a’s as well as a::b’s. So an extension impl written in a
directory’s body arrives in any file that imports a module beneath it,
without that file naming a, and a.vl is the place to put one that
every child’s importer should have. only declines them all and an impl
selector takes the blocks it names (§4.3); a block the declaring module
does not export is not admitted at all (§4.8).
4.2 The three namespaces
A path’s first segment selects a namespace:
std::name: the standard library modulename, resolved against the std package’s layers for the current platform (§11).pkg::name: the modulenameof the importing file’s own package.depname::name: the modulenameof the dependency declared asdepnamein the package manifest.
Within std itself, sibling modules are referenced as pkg::… (std is its
own package). The namespaces are disjoint: resolution is scoped by the
root segment, so a package is free to name a module ui or json even
though std has one: pkg::ui is always the package’s own module,
std::web::ui always std’s, and neither shadows the other. (Conversely,
pkg:: never reaches a std module.) The root names themselves are
reserved: a manifest may not declare a dependency — or a
[package] name — as std, pkg, or macro_std (§11.4), so no
package can shadow a root or vanish behind one; vilan, the language’s
own name, is reserved alongside them (§11.4). A module path that
resolves both as path.vl and path/lib.vl is an ambiguity error.
A path is resolved by its longest module prefix: the segments after
the root are matched against the tree longest-first, and what is left
over is looked up as items of the module that answered. So
pkg::a::b::c is the module a/b/c.vl when that file exists, and
otherwise the item c of the module a/b.vl, and otherwise the item
path b::c under a.vl.
A segment that names both — a.vl declares an item b and a/b.vl
exists — is an ambiguity error, the same answer a.vl beside
a/lib.vl gets (§4.1): one of the two has to be renamed. A module’s own
items are matched before its directory’s files, so the declaration is
what an import would land on and the file could not be reached at all;
a silent winner here is what makes a rename on either side change what
every existing pkg::a::b means. The refusal is raised at the
declaration, once, whether or not anything imports the path.
Every segment must match the on-disk directory entry byte for byte: a
case-insensitive filesystem that answers import foo with Foo.vl is a
diagnostic naming both spellings, not a resolution, so that a program
compiles identically on case-sensitive and case-insensitive filesystems
(design notes §5). Every component of the resolved path
carries the rule — directory components included — so foo/lib.vl is
reached by import foo only when the directory is spelled foo, and
a/b.vl by import pkg::a::b only when both are.
4.3 Imports
import path (§3.2) loads the target module (once per program: loading
is idempotent and cycle-tolerant) and binds the imported items in the
importing module’s scope:
-
import std::io::print;binds the itemprint. -
import std::reactive::{ Signal, SignalCell, combine };binds each set member. -
import std::option::Option::{ self, Some, None };is a path into a TYPE:selfbinds the type itself; variant names bind the variants for unqualified use. -
import std::web::style::Length::rem;is the same path into a type, reaching its statics — the functions itsimplblocks declare that take noself(§4.6). A static is reached through the module whose file writes the block, which is the type’s own module for a type whose impls sit beside it and the extending module for an extension impl; aselfmethod is not importable under either form, and says so. The set andasforms reach statics like any other leaf, which is what lets an app’s own prelude module re-export one bare (export import std::web::style::Length::rem as rem;). -
A type segment replaces the path’s namespace. What follows
Lengthis resolved inLength’s namespace and nowhere else — its variants, then its statics — so a module-levelremdeclared besideLengthdoes not shadowLength::rem, andstyle::Length::pxdoes not reach the module’spx. One rule for both kinds: an enum segment has always worked this way, and a struct segment does too. A path reaching a module’s item through a type-named prefix is a diagnostic naming the spelling that replaces it (style::px). -
import std::json::Json as Document;bindsDocument.asrenames the leaf and changes nothing else: the path resolves exactly as it would without one, and the imported item is the same item under a second spelling — go-to-definition onDocumentlands onJson, and a rename ofJsonrewrites the alias with every other reference to it. The form reaches a set member too (import a::{ b as x, c };), and a re-export publishes the alias (export import a::b as c;publishesc). -
import std::list only;binds the statement’s names and no implementations. A plainimportbrings everyimpldeclared in the files on the path to its leaf — that is how an extension impl arrives — andonlyis the spelling that declines them. -
import std::list::{ (impl List<i32>) };is an impl selector: a brace-set element that binds no name and admits exactly theimplblocks of the module whose subject unifies with the written type. A set naming one means “these implementations only”, soonlyis for statements without a selector._is the placeholder —(impl List<_>)admits everyListblock whatever its element, and(impl _)admits every block the module declares — and notype Xbinders are written in a selector.import std::list::{ (impl List<i32>)::first };takes ONE member into the type’s namespace for this file, and::{ first, last }takes several; a selector takes noas, because a method is called by name on a receiver. The subject resolves in the IMPORTING file’s scope:impl Safterimport item::Struct as S;reachesS, andimpl item::Structis the qualified spelling.
use path binds names from an already-visible type’s namespace without
loading (variants, statics) — the same two kinds import reaches by
path, and the difference is only that use loads nothing, the type being
in scope already. export statement re-exports: importers of this module
see the exported names as if declared here.
Visibility is §4.8. In an import path it shows up as one token: #
before a leaf is the reach marker, import pkg::a::{ #hidden };,
which takes an item the module does not export and says so.
Platform gating is not checked at the import: a module outside the
current platform’s layers (e.g. std::web::ui in a Node build) still loads,
so its items type-check. The error is reported where platform-colored
code becomes reachable from the build’s entry (§11).
4.4 Scopes and shadowing
Scopes nest: module → function/impl → block → closure. Name lookup walks
outward from the use site to the innermost binding. A let/mut binding
shadows any outer binding of the same name from its point of
declaration onward, including imports and items:
fun main() {
let print_count = 2;
mut label = "a";
{
let label = "inner"; // shadows the outer binding in this block
print(label);
}
print(label);
print(print_count);
}
Items within one module share the module scope and are visible
throughout the module regardless of declaration order (a function may
call one declared later). Local let bindings are visible only after
their declaration.
One module declares a name once, whatever the sorts: two struct N,
two fun n, a struct N beside an enum N or a trait N are all an
error at the second declaration, naming the first. Order is not a rule a
reader can see, and without this it decided which declaration a name
meant. Two modules may each declare N freely — a module is a
namespace, and shapes::N and colors::N are two types the program can
name apart.
The full ladder, weakest first: the prelude (§4.7) → module items and explicit imports → enclosing scopes → the innermost local binding. Two rungs deserve stating outright, because both are silent:
- An explicit import beats a same-file declaration.
import std::io::print;in a file that also declaresfun printbinds the import; the file’s own function is unreachable by that name. - Everything beats the prelude. It is the weakest rung by construction, so shadowing one of its names — by declaring it or by importing it — is never a diagnostic.
A let may also redeclare a name within the same scope: the later
binding shadows the earlier one from its own declaration point onward,
while uses before that point keep the earlier binding (parameters —
mut, spread, or plain — and loop/pattern bindings are shadowable the same way). A
spread parameter binds one name to the whole pack, like any other
parameter; it declares no per-element names. Visibility starts at
the end of the declaring statement, so an initializer never reads the
binding it declares: in let x = x + 1; the right-hand x is the
previous x (an enclosing or earlier same-scope binding) and an error
when none exists. Module-level bindings are the exception, as above: they
are order-independent, one declaration per name, and a genuine
initialization cycle is a compile error (§7 of the execution chapter).
4.5 Type position vs value position
A name is resolved differently by position:
- In type position (annotations, generic arguments, impl subjects), lookup prefers bindings that denote types; a value binding with the same name does not shadow a type there.
- In value position, lookup takes the nearest binding of any kind.
Consequently a local variable named Signal does not break let s: SignalCell<i32> annotations in the same scope, but relying on this is poor
style.
A type may also be qualified by the modules that declare it —
style::Style, std::reactive::SignalCell<i32> — in any type position
(grammar §3.9, types §5.1). The namespace segments resolve as §4.2’s
paths do, so an imported module reaches its types as it reaches its
values. The preference above does not apply to a qualified path: it
addresses exactly what that one namespace declares, so a member that is
not a type is an error there rather than a reason to keep looking.
4.6 Statics and members
Type::member (§3.6) resolves member in Type‘s namespace: enum
variants, the static functions of the type’s impls (those without self),
and the type’s own self-methods. Generic statics take their arguments at
the path head: List<str>::new(). A fixed array’s statics are its impls’
(impl [type T: Default; const N] with Default), reached through the array
type written whole at the head — [i32; 3]::default() binds the impl’s T
and N as List<str> binds List’s T.
A type has one namespace per importing file, and receiver position is
not part of a name. The namespace is the union of the impl blocks that
file’s own import statements admit (§4.3): a plain import a::b; admits
every block declared in a’s file and in every file on the path to it,
import a::b only; admits none, and
import a::{ (impl T) } admits exactly the blocks whose subject unifies
with T. A block’s members are what it declares AND the trait defaults it
inherits, so a default-only impl Box with One {} is a block (impl Box)
admits, and (impl Box)::describe takes the default it inherits.
(impl Box with One) takes that block alone among Box’s, naming it by the
trait as its declaration does; a whole-block selector admits a block that
declares and inherits nothing (a marker impl) as well. Method
lookup reads the file’s namespace: two blocks offering one name, one of
them declined by the file, are not ambiguous — the admitted block answers —
and a call only a declined block answers is refused, naming the import
that declined it.
Two impls declaring the same name for one subject — two statics, two methods, or one of each — are refused, and where depends on how far apart they are declared:
- In one module, at the declaration. Nothing ranks them and one would simply never be reachable, so the file that writes both is told so.
- In two modules, at the import, spanned on the second import
statement and naming both blocks. Each module stays importable on its
own; it is the file that takes both that has to choose. The fix is an
impl selector, which is why the refusal is reported where selectors are
written:
import pkg::z::{ (impl Thing)::tag };takes one member from one block and leaves the other module’s alone.
That is what lets two independent extension modules declare the same method for the same type and both ship: a file that never imports both never sees a conflict, and the refusal is a fact about the FILE’s imports rather than about the order modules happened to load in.
It is also the whole namespace of the path that names it: Type::
replaces what the rest of the path resolves against rather than adding to
it (§4.3), so the module the type is declared in does not answer through
it. A name that is not in the type’s namespace is not in the path’s.
That namespace is what import and use reach into (§4.3), and receiver
position IS the line they draw: a static binds to a bare name, and a
self-method does not — it is called on a value, so a bare binding would
name something uncallable. The refusal says so rather than reporting the
member as missing.
value.member resolves against the value’s type: fields first, then
methods, by a precedence rule — not by the order the impl blocks
happen to be written or the modules happen to load:
- An inherent method — one declared by an impl of the type whose
withclause does not declare that name — always wins, whatever the text order. - Otherwise, the method a trait provides, whether the impl declares it or inherits the trait’s default (§5.7).
Two declarations at the same level are an error rather than a silent
pick. Two inherent declarations of one name for one subject are rejected
at the definition site, before any call resolves them. Two traits
providing one name, with no inherent method above them, make each call an
ambiguity error — as does a T: A + B bound whose two arms supply it. Two
impls of one trait for one subject are rejected at the definition site
too, by the coherence rule of §5.4: rule 2 above says “the method a trait
provides”, and a trait provides one.
struct Bag { x: i32 }
trait Iter { fun pick(self): str; }
impl Bag with Iter {
fun pick(self): str { "the trait's" }
}
impl Bag {
fun pick(self): str { "the type's own" }
}
fun main() {
let bag = Bag { x = 1 };
// The inherent method wins, though the trait impl is written first.
print(bag.pick());
// Naming the trait reaches its version; naming the type means the
// inherent one, and never falls through to a trait's.
print(Iter::pick(bag));
print(Bag::pick(bag));
}
A trait’s method resolves only in a file that imports the trait — by
name, through a bound that writes it, or because the file declares the
trait or the impl itself (the base prelude brings Iterator, the web
prelude the pipe sealers). An impl registers when its module loads, and
loading is program-wide, so 42.to_string() would otherwise resolve in a
file that never imported Display just because another loaded module did.
Such a call is refused at the member’s name, naming the import that brings
the trait (Import it (`import std::display::Display;`)) — a warning in
v0.44.0, an error from v0.45.0 (B515, B535).
std’s own files import nothing they call, so they reach every trait std
declares and no package’s: a package’s trait — a blanket over every
type included — is never a candidate at a call inside std. A module’s
impl type T with Describe { fun describe(self): str { … } } leaves std’s
own describe calls to std’s Wire, as the same blanket written in the
entry always did. A package’s impl of a std trait (impl Point with Display) is an impl of a trait std names, and serves std like any other
(B583).
Trait::member(receiver, args…) is the disambiguator: it names which
provider to use, and works on a concrete receiver or a trait-bounded
generic one. Type::member(receiver, args…) means the type’s own member
or nothing.
The impl selector is the other disambiguator, and it answers a
different question: Trait::member picks between two providers both of
which the file can see, while a selector decides which blocks the file
sees at all. A file that would otherwise take one name from two modules
writes import pkg::z::{ (impl Thing)::tag }; and the rest of z’s
blocks stay out of its namespace. Both spellings are per file, and
neither changes what any other file resolves.
4.7 The prelude
Two sets of names are in scope without imports.
The built-in set, always: the primitive types (i32, str, bool,
…), List, void, and the boolean/null literals’ types. (The exact
set is the lang-item table, appendix §A.4.)
The package’s prelude, chosen by its manifest. A prelude is a
module, and the names it makes ambient are exactly that module’s
exports. [package] prelude (and [library] prelude) names it:
| Value | Ambient names |
|---|---|
| omitted — the default | std’s base set: print, Iterator, Option, Some, None, Result, Ok, Err |
"std::web::prelude" | the base set, plus Signal, SignalCell, the collection pipes’ sealers CollPipe and SetPipe, view, View, the six slot values (when, when_some, swap, each, each_values, each_by), and the modules style and ui |
any module path (pkg::…, std::…, a dependency) | that module’s exports |
false | none — only the built-in set above |
fun main() {
// `Option`, `Some`, `None` and `print` with no import: the language
// manufactures the `Option` here, so it can also name it.
match [10, 20, 30].get(1) {
Some(let n) => print("found one"),
None => print("empty"),
}
}
A prelude entry may name a module as well as a member. An ambient
module contributes exactly one name to the bare namespace — its own — and
its members are reached through it, so style::Display needs no import
while bare Display still means std::display::Display.
The prelude is the weakest binding in the language. A local
declaration or an explicit import of a prelude name wins, silently, with
no diagnostic (§4.4). A file that declares its own fun print gets its
own; a file that imports std::io::print gets that import, and the
import is simply redundant rather than an error.
A prelude is per package and never inherited. A dependency’s files
resolve under the prelude its manifest declares — not its consumer’s,
not a workspace root’s, and not per platform layer. Two packages that
disagree about what Signal means both keep compiling in one build.
A shadowed prelude name has no qualified spelling at the use site:
std::io::print(x) written inline is refused (§4.6). Recover by importing
the module and qualifying through it — import std::io; then
io::print(…).
The standard library itself declares prelude = false, so its own
resolution stays greppable.
4.8 Visibility
Every top-level item carries one bit: exported, or the module’s own. The default is the module’s own — a declaration with no marker is private.
The four forms.
| form | means |
|---|---|
export fun helper() | this item is the module’s surface. Any declaration takes it: fun, struct, enum, trait, impl, a module-level let, a macro, a mod block |
export *; | every item of this module is exported. One bare statement at the module’s top level; vilan fmt puts it just below the file’s leading import run |
export(in PATH) item | narrower than export. export(in mod) keeps one item private under an export *;; export(in pkg) publishes it to its own package and no further |
export import pkg::io::print; | a re-export: importers of this module see print as if it were declared here. This is how a prelude module is written (§4.7) |
export on an impl block means what it means on every other
declaration. A block a consumer cannot see contributes no methods to
that consumer — not even as an ambient impl arriving along a path — so a
curated module writes export impl Thing { … } for the blocks it means
to publish. A module with no marker anywhere is uncurated and offers
everything, which is what keeps a package that has never thought about
visibility compiling exactly as it did.
A name a std module re-exports is offered at its shortest public
path: the “import it first” steer, the trait-scope refusal and the
add-import fix all write import std::reactive::store::Store; — the module
that re-exports Store and that a program is meant to import — and not
the module that happens to declare it. Where two re-exports tie at the
shortest, the declaring module is named.
Visibility never blocks access. The bit is consulted by completion, by the add-import fix, by the “import it first” steer and by the per-importer method namespace (§4.6) — and by nothing in name resolution. An importer who needs a private item writes the reach:
import pkg::util::{ #helper }; // "not exported, and I want it anyway"
A plain import pkg::util::helper; of an unexported item still compiles
and warns, naming the marked spelling; so does a qualified reach
through the module (util::helper() after import pkg::util;), which
has no leaf to mark. The reach marker on an item that IS exported warns
the other way — it says something about the author’s belief that is not
true — and the fix is to delete one character.
Reaching a dependency’s unexported item is no diagnostic at all, at any release. Whether an item should be exported is its author’s judgement, and a consumer’s need is real evidence against it; the marker is available there too and says the same thing, but nothing asks for it.
The second warning is about signatures. An exported item whose signature names an unexported type is reported once at the declaration:
Sis returned here.my_funis exported, butSis not. A consumer can callmy_funbut cannot name the return type.
Its reach is the signature positions and only those — a module-level
let’s type, a parameter type, a return type, an exported struct’s field
types, an exported enum’s variant payloads, a declared bound, and a
generic argument in any of them. A private type used inside an exported
function’s body is exactly the encapsulation the bit exists to
permit, and is never reported. The fix is to export the type; when the
type belongs to a dependency there is no fix, and the message says so.
The standard library is curated: its modules export what they publish and
keep their own machinery private, which is why vilan check on a program
that reaches into std’s internals tells you so.