Namespaces and imports
One file needs none of this chapter. The moment a second file joins in, three questions appear, and WCL answers each with its own keyword. Which files take part? import. What are the declarations in them called? namespace. How do I write those names here? use, plus the :: qualifier for block kinds.
Keeping the three apart is the whole design. An import decides membership and nothing else — it does not rename anything, and it does not re-export. A namespace decides names and nothing else — it has no idea which files will import it. Everything left over is spelling, and that is what use and :: are.
Every file and every command below was run before it was written down. Type them and compare.
§ 1A project in three files
Make a folder with a lib/ subfolder inside it, and save these three files.
# lib/base.wcl — the shared vocabulary.
namespace acme.base
type Endpoint { host: utf8 port: u32 }
symbol_set Tier { gold silver }
# lib/net.wcl — the network library. It imports its own neighbour.
namespace acme.net
import "./base.wcl"
@block("service")
type Service {
@inline(0) name: identifier
listen: acme.base.Endpoint
tier: acme.base.Tier
}
# platform.wcl — the root document.
import "./lib/net.wcl"
@document
type Platform {
owner: utf8
@children("service") services: list<acme.net.Service>
}
owner = "platform"
service web {
listen = { host: "0.0.0.0", port: 8080u32 }
tier = :gold
}
service db {
listen = { host: "10.0.0.4", port: 5432u32 }
tier = :silver
}
$ wcl check platform.wcl
OK
$ wcl get platform.wcl owner
"platform"
$ wcl get platform.wcl services.db.tier
:silver
Three files, one document. Read what platform.wcl got for its single import line. It got acme.net.Service, from the file it named. It got acme.base.Endpoint and acme.base.Tier as well, because lib/net.wcl imported lib/base.wcl in turn — imports are transitive. It got the service block kind, because a @block decorator on an imported type declares that kind for the whole document. And it wrote ./lib/net.wcl while lib/net.wcl wrote ./base.wcl, because each path is resolved from the file it is written in, not from the root.
The rest of this chapter takes that paragraph apart.
§ 2Declaring a namespace
A namespace declaration takes a dotted path and files every declaration in its file under it. In lib/base.wcl above, type Endpoint is really acme.base.Endpoint, and that longer name is what the rest of the document sees. A file with no namespace line declares into the root namespace, where the name you write is the whole name.
Two rules. The declaration must be the first item in the file, and there may be at most one. Comments and blank lines above it are trivia rather than items, so a file may still open with its header comment. Both rules are checked on the file you open:
$ wcl check late.wcl
wcl::parse
× namespace declaration must be the first item in the file
╭─[late.wcl:4:1]
3 │
4 │ namespace app
· ──────┬──────
· ╰── must be first item
5 │
╰────
$ wcl check twice.wcl
wcl::parse
× duplicate namespace declaration
╭─[twice.wcl:3:1]
2 │
3 │ namespace app.web
· ────────┬────────
· ╰── duplicate namespace
4 │
╰────
A namespace is a property of the file, not of the folder it sits in. Two files in one folder may declare different namespaces. Two files in different folders may declare the same one and pool their declarations. Nothing about the directory layout leaks into the names.
Both rules are checked on the entry file only
Placement and uniqueness are checked on the file you name on the command line, not on the files it imports. An imported file that declares its namespace late, or twice, passes today. Its first namespace still decides what the file's declarations are called, so the second is not applied — it is ignored. Write the declaration first and write it once. The tool will not catch a library that does not.
§ 2.1Declaration names nest
The other half of a name is the declaration's own. A dotted declaration name nests under the file namespace, so one file can carry several groups without one namespace line each. Inside the namespace, a relative path resolves; the fully-qualified path always resolves too.
namespace acme.graphics
type color.RGB { r: u8 g: u8 b: u8 }
type theme.Swatch {
fill: color.RGB # relative to acme.graphics
stroke: acme.graphics.color.RGB # the same type, written in full
}
@document
type Cfg { swatch: theme.Swatch }
swatch = {
fill: { r: 12u8, g: 34u8, b: 56u8 },
stroke: { r: 0u8, g: 0u8, b: 0u8 },
}
color.RGB and acme.graphics.color.RGB name one type here, and wcl check prints OK. Both spellings are covered by How a name resolves below — the first is step 1, the second is step 5.
§ 3Imports
An import pulls another file's items into the current document. Two forms, told apart by how the path is written. A quoted path names a file on disk. An angle-bracket path names a file the host program supplies.
import "./lib/net.wcl" # disk
import "../shared/db.wcl" # disk, one folder up
import <wdoc.wcl> # system — served by the host, not the filesystem
§ 3.1Disk imports
A quoted path is resolved against the directory of the file the import is written in, then canonicalised. A path that names nothing fails at open time, with the path it tried:
$ wcl check stray.wcl
wcl::parse
× failed to import './base.wcl': failed to resolve './base.wcl': No such
│ file or directory (os error 2)
╭─[stray.wcl:1:8]
1 │ import "./base.wcl"
· ──────┬─────
· ╰── cannot resolve import
2 │
╰────
§ 3.2Importer-relative resolution
That failure is the rule worth stating on its own, because it is the one that surprises people. Every path resolves from the file it is written in. The library in the worked example writes import "./base.wcl", and it works, because base.wcl sits beside net.wcl. Write the same line in platform.wcl one level up and it fails, exactly as above. The root's ./ is the root's folder.
So a library folder is movable. Every path inside lib/ is written from lib/'s point of view. Move the folder and one line changes: the one in the file that imports it. The rule holds for system imports too — see the next section.
One directory, and no fallbacks
A disk import resolves against exactly one directory: the importing file's own. Nothing else is tried — no list of roots, no environment variable, no project file that adds one. A relative path either names a file from where it is written, or it fails. An absolute path is legal and resolves as written. That makes it portable to exactly one machine.
§ 3.3System imports and the registry
An angle-bracket path names a system import: a file the host program registered under that name before it opened the document. Nothing is read from disk, and the path is not a filename. This is how a tool ships a standard library — wdoc's whole schema arrives on one line:
import <wdoc.wcl>
site handbook { title = "Handbook" }
page index {
title = "Handbook"
h1 "Handbook"
p "One import, and every wdoc block is in scope."
}
On the host side a registry is a table of names to source text. The host turns it into a loader, which serves system imports out of the table and passes every other path through to disk:
use PathBuf;
use ;
let mut reg = new;
reg.register;
reg.register;
// System imports come out of the registry; every other path still
// reads from disk.
let loader = reg.loader;
let doc = open_at_with_loader source,
"platform.wcl",
Some,
&new,
loader,
)?;
A later registration under the same name wins. Registry::extend folds one registry into another by the same rule. That is how a tool built on wdoc layers its own library over wdoc's, with neither side knowing about the other.
Registry names are importer-relative in their own namespace, exactly as disk paths are on disk. wdoc.wcl writes import <wdoc/prelude.wcl>, and that file — registered as wdoc/prelude.wcl — writes import <core.wcl> for its sibling, which resolves to wdoc/core.wcl. .. works and means what it looks like: a part registered as mylib/component/common.wcl reaches the other library with import <../../wdoc.wcl>.
A system import naming something unregistered fails at open time, and says so in the registry's own vocabulary rather than the filesystem's:
$ wcl check nosuch.wcl
wcl::parse
× failed to read '<wcl-system>/acme/net.wcl': no system import registered
│ for <acme/net.wcl>
╭─[nosuch.wcl:1:9]
1 │ import <acme/net.wcl>
· ──────┬─────
· ╰── io error
2 │
╰────
§ 3.4What an import brings in
An imported file's items join the document as full members. Its types, interfaces, unions, symbol sets, connection declarations, let bindings and functions are all usable from the importing file. Its @block, @table and @document decorators declare kinds and top-level fields for the document as a whole.
What an import does not do is rename anything. Declarations keep the namespace of the file that declared them, and that namespace is what you write. lib/net.wcl imported acme.base.Endpoint, but Endpoint did not become an acme.net name by passing through:
$ wcl check badtarget.wcl
wcl::parse
× unknown use target 'acme.net.Endpoint'
╭─[badtarget.wcl:3:1]
2 │
3 │ use acme.net.Endpoint
· ──────────┬──────────
· ╰── not declared
4 │
╰────
There is one convenience on top of that. Importing a namespaced file adds its namespace to this file's bare-name search path, so Service resolves without a use line once lib/net.wcl is in. use is what you reach for when that is not enough — see use declarations.
§ 3.5Loaded once
Each file is loaded at most once per document, keyed by its resolved path. A diamond — two libraries that both import a third — loads the shared file once and splices it in once. So does a repeated import <wdoc.wcl> in several page files. Neither is an error, and neither duplicates a declaration.
That is why a library may import whatever it needs without knowing what else the document imports. Redundancy is free; only a genuine loop is not.
§ 3.6Import cycles
A cycle is an import that re-enters a file already open further up the current chain. It is reported at open time, at the line that closed the loop, naming the file it would have re-entered:
$ wcl check platform.wcl
wcl::parse
× import cycle detected at '/home/you/platform/lib/net.wcl'
╭─[/home/you/platform/lib/base.wcl:8:8]
7 │
8 │ import "./net.wcl"
· ─────┬─────
· ╰── cycle
╰────
Read the two paths together and the loop is in front of you. The arrow points at lib/base.wcl line 8; the message names lib/net.wcl. So net imports base imports net. A file that imports itself reports the same way, with one path in both slots.
A cycle is not the same as a diamond
The two look alike and are handled oppositely. A file already loaded somewhere else in the document is skipped — that is the diamond, and it is fine. A file still being loaded, an ancestor of the chain the loader is in right now, is a cycle and an error. The difference is not how many files point at it; it is whether it has finished.
Cycles are almost always a sign that a shared name wants a file of its own. Split the part both files need into a third file and have each import that.
§ 3.7Imports inside a block
An import may also sit inside a block. Its items are spliced into the enclosing block as if they had been written there, which lets a long subtree live in its own file. Fields and child blocks both come across, and the children are checked against the parent's @child / @children slots like any literal child.
# tags.wcl — items spliced into whichever block imports it.
tag { key = "env" value = "prod" }
tag { key = "team" value = "platform" }
@block("tag")
type Tag { key: utf8 value: utf8 }
@block("service")
type Service {
@inline(0) name: identifier
@children("tag") tags: list<Tag>
}
@document
type Cfg { @children("service") services: list<Service> }
service web {
import "./tags.wcl"
}
$ wcl parse tagged.wcl
...
service web {
tag {
key = "env"
value = "prod"
}
tag {
key = "team"
value = "platform"
}
}
A top-level import is eager: the document loads it when it opens. A block-scoped import is lazy — it is loaded the first time evaluation reaches into that block. How a document evaluates covers what that means for a file that turns out never to be needed.
§ 4Order does not matter
Nothing in this chapter is order-sensitive except the one rule that says so — namespace must come first. A use may stand above the import that brings its target in. An import may be the last line in the file. Resolution runs over the whole document once every file is loaded, so nothing here is a forward reference:
use acme.base.Tier as Level # the `use` before the `import`
@document
type Cfg { level: Level }
level = :gold
import "./lib/net.wcl" # and the import last of all
$ wcl check backwards.wcl
OK
$ wcl get backwards.wcl level
:gold
Note what that file reached: acme.base.Tier, two imports deep, through a use written above the only import in the file. Order is for the reader. Put your imports at the top anyway.
Order-independent is not preference-free
One thing is decided by source order: the tie-break in How a name resolves. When two imported namespaces both declare a name and nothing disambiguates it, the earlier import wins. Read that as a reason to disambiguate, not a reason to sort your imports.
§ 5use declarations
A use declaration binds a name. Write it at the top level of the document you open — inside a block it is a parse error, and use belongs to the root document covers why an imported file should not carry one. Every form below is checked when the document opens, so an unknown target or a clashing alias fails before anything evaluates:
use acme.base.Endpoint # bind the leaf: write `Endpoint`
use acme.base.Tier as Level # bind it under another name
use acme.billing # add a namespace to the search path
use acme.net as N # namespace alias: `N.Service`, `N::service`
use acme.base.{Endpoint as Addr, Tier} # several members at once
| Form | Binds | Reach for it when |
|---|---|---|
| use ns.Name | Name | The name is long and you write it often |
| use ns.Name as Alias | Alias | Two namespaces offer the same name |
| use ns | Every member of ns, bare | One namespace should win the whole file |
| use ns as Alias | Alias.Name — and Alias::kind for block kinds | You want the namespace visible at each use |
| use ns.{A, B as C} | A and C | Several names from one namespace |
The one form to read twice is the third. use ns binds no single name; it adds a whole namespace to the bare-name search path. The last two rows are the pair that trips people: use acme.net lets you write Service, while use acme.net as N does not — it gives you N.Service.
§ 5.1When you need one
Add a second library to the worked example. acme.billing declares a Tier of its own, and now nothing says which one a bare Tier means:
# lib/billing.wcl — a second library, with a name that collides.
namespace acme.billing
symbol_set Tier { monthly annual }
import "./lib/net.wcl"
import "./lib/billing.wcl"
@document
type Plans { plan: Tier }
plan = :monthly
$ wcl check plans.wcl
wcl::eval::schema_violation
× field 'plan' declared as symbol_set 'Tier' but ':monthly' is not one of
│ its members
plans.wcl: 1 schema violation
The document is not confused. It is decided, and it decided against you. lib/net.wcl is imported first, so its transitive acme.base outranks acme.billing. Bare Tier is therefore acme.base.Tier, whose members are gold and silver. Three ways to fix it, and any one will do:
- use acme.billing.Tier — bind the leaf you meant. Says the most, in one place.
- use acme.billing — put that whole namespace ahead of the imports' own order. Right when one library should win the file.
- plan: acme.billing.Tier — write the name in full at the point of use. No declaration needed at all.
All three print OK, and wcl get plans.wcl plan answers :monthly. What none of them do is remove the other Tier: the document still holds both, and both are still reachable in full. A use is a local spelling, not an edit to the model.
§ 5.2How a name resolves
A written name is tried against five candidates, in this order, and the first one that names a real declaration wins:
- The file's own namespace, plus the name as written. Inside namespace acme.graphics, color.RGB is tried as acme.graphics.color.RGB first.
- An item alias, for a single-segment name — anything a use ns.Name or a use ns.{…} bound.
- A namespace alias, on the first segment of a dotted name — N.Service where use acme.net as N.
- Each namespace on the search path, in order: the use ns declarations first, in source order, then the imported files' namespaces, in import order.
- The name as already fully qualified. This is why acme.graphics.color.RGB works from inside acme.graphics, even though step 1 tried it as acme.graphics.acme.graphics.color.RGB and found nothing.
Steps 2 and 3 are what a use adds, and both sit above every import. That is the mechanism behind the fix above. Stated plainly: anything you declare explicitly outranks anything an import brought along.
§ 5.3use belongs to the root document
Only step 1 of that list is per-file. A name is always tried in its own declaring file's namespace first, which is why an imported library's bare references resolve at home. Steps 2, 3 and 4 are the document's, and today they come from the root document alone.
Two consequences, and they point opposite ways. A use in the file you open reaches into every imported file: write use acme.base.Tier as Level in the root and a library declaring level: Level resolves. A use in an imported file reaches nowhere at all — it binds nothing, and its own unknown targets and duplicate aliases go unchecked.
A library should not write use
A use line inside an imported file is inert today, and it fails quietly rather than loudly. The alias resolves to nothing, so a field declared through it is never checked at all. A library should write the names it means in full — acme.base.Tier, not an alias — or rely on step 1 and its own namespace. Keep use for the document you open, where it is checked and where it works.
§ 5.4What a use refuses
Three checks run at open time. An unknown target fails — including the case in What an import brings in, where the namespace exists but does not own that name. Two bindings of one local name fail, whichever forms produced them:
$ wcl check dupalias.wcl
wcl::parse
× duplicate use alias 'Tier'
╭─[dupalias.wcl:5:1]
4 │ use acme.base.Tier
5 │ use acme.billing.Tier
· ──────────┬──────────
· ╰── duplicate alias
6 │
╰────
And the brace-list form insists on a namespace to its left, so reaching into a type for its fields is refused rather than quietly ignored:
$ wcl check listtype.wcl
wcl::parse
× expected namespace, but 'acme.base.Endpoint' names a type
╭─[listtype.wcl:3:1]
3 │ use acme.base.Endpoint.{host}
· ──────────────┬──────────────
· ╰── not a namespace
╰────
§ 6Qualified kind names
Everything so far has been about type names. Block kinds — the word before the brace — are namespaced too. They need their own spelling, because a kind is not a type reference: it is written where a type name cannot go. So a kind takes a :: qualifier instead of a dot. acme.net::service names the service kind in the acme.net namespace, and N::service does the same through a namespace alias.
The case that needs it is a local kind with the same name as a library's. Both stay usable in one file:
import "./lib/net.wcl"
# A local `service`, for the things that are not built yet.
@block("service")
type Stub {
@inline(0) name: identifier
note: utf8
}
@document
type Staging {
@children("service") services: list<Stub>
}
service search { # the local Stub — a bare kind prefers home
note = "not built yet"
}
acme.net::service web { # the library's Service, named explicitly
listen = { host: "0.0.0.0", port: 8080u32 }
tier = :gold
}
wcl check prints OK. Now give the bare service a field only the library's schema declares — service search { tier = :gold } — and it does not. That is the proof that the qualifier selected a different schema rather than annotating the same one:
$ wcl check staging-bad.wcl
wcl::eval::schema_violation
× field 'tier' is not declared by schema 'Stub'
staging-bad.wcl: 1 schema violation
A bare kind prefers a declaration in the referencing file's own namespace, then one the root document authored, then the first candidate found. The practical reading is the first clause: your own @block shadows a library's, deterministically. A library cannot capture a kind name out from under you by being imported.
§ 6.1One kind per namespace
A namespace is what makes the qualifier able to choose, so a kind must be unique within one. Two @block("service") types in the same namespace are refused, and the message names the way out:
$ wcl check twokinds.wcl
wcl::eval::schema_violation
× type 'External' redeclares @block("service") already declared in this
│ namespace (kinds must be unique per namespace; qualify across namespaces
│ with `::`)
╭─[twokinds.wcl:5:1]
4 │ @block("service")
5 │ type External { @inline(0) name: identifier url: utf8 }
· ────────────────────────────┬───────────────────────────
· ╰── schema violation
6 │
╰────
twokinds.wcl: 1 schema violation
The same kind name in two different namespaces is fine — that is staging.wcl above. Schemas covers @block and @table themselves; Writing your own blocks covers declaring a kind that a wdoc document can use.
§ 7Four keywords, four questions
This is the comparison the four mechanisms exist to make. Each answers a question the others cannot see, which is why they stayed four things rather than one.
| Keyword | Answers | Written in | Affects | Wrong tool when |
|---|---|---|---|---|
| import | Which files are in the document? | Any file, top level or in a block | The whole document | You wanted to rename something — it never does |
| namespace | What are my declarations called? | The first line of a file | Every declaration in that file, everywhere | You wanted a shorthand for one file — that is use |
| use | How do I write those names? | The top level of the file you open | The whole document, imported files included | You wanted the declaration itself renamed |
| :: | Which namespace's block kind? | One block instance | That instance only | The name is a type — types take a dot |
One worked consequence to close on. In staging.wcl the local @block("service") and the library's are both live. The import decided both files are here. The namespaces decided the two schemas have different owners. The bare kind picked the local one by the home-first rule, and the :: qualifier reached past it for the one instance that wanted the other. No line of that overrode another: each answered its own question, and the answers composed.
§ 8Where to go next
- Types — what a type reference may name, and how &T references and interfaces resolve.
- Schemas — @document, @block, @table, and how several @document types in one namespace merge.
- How a document evaluates — laziness, caching and cycles, including what a block-scoped import defers.
- Documents, fields and blocks — the thirteen item forms, three of which this chapter covered.
- Documents, pages and sites — what import <wdoc.wcl> puts in scope.