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 §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.

4.2 The three namespaces

A path’s first segment selects a namespace:

  • std::name: the standard library module name, resolved against the std package’s layers for the current platform (§11).
  • pkg::name: the module name of the importing file’s own package.
  • depname::name: the module name of the dependency declared as depname in 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::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 name that resolves both as name.vl and name/lib.vl is an ambiguity error.

A module name 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, so foo/lib.vl is reached by import foo only when the directory is spelled foo.

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 item print.
  • import std::reactive::{ Signal, SignalCell, combine }; binds each set member.
  • import std::option::Option::{ self, Some, None }; is a path into a TYPE: self binds the type itself; variant names bind the variants for unqualified use.

use path binds names from an already-visible type’s namespace without loading (variants, statics). export statement re-exports: importers of this module see the exported names as if declared here.

Platform gating is not checked at the import: a module outside the current platform’s layers (e.g. std::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.

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 declares fun print binds 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.

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 type has one namespace, and receiver position is not part of a name. Two impls of one type declaring the same name — two statics, two methods, or one of each — are a compile error at the declaration, since nothing ranks them and one would simply never be reachable.

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:

  1. An inherent method — one declared by an impl of the type whose with clause does not declare that name — always wins, whatever the text order.
  2. 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));
}

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.

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:

ValueAmbient names
omitted — the defaultstd’s base set: print, Option, Some, None, Result, Ok, Err
"std::web"the base set, plus Signal, view, View, and the modules style and ui
any module path (pkg::…, std::…, a dependency)that module’s exports
falsenone — 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.