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.wclwcl
# lib/base.wcl — the shared vocabulary.
namespace acme.base

type Endpoint { host: utf8  port: u32 }

symbol_set Tier { gold  silver }
lib/net.wclwcl
# 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.wclwcl
# 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
}
text
$ 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:

text
$ 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.

swatch.wclwcl
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.

wcl
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:

text
$ 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:

site.wclwcl
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:

rust
use std::path::PathBuf;
use wcl_lang::{disk_loader, Document, Environment, Registry};

let mut reg = Registry::new();
reg.register("acme/base.wcl", include_str!("../lib/base.wcl"));
reg.register("acme.wcl", "namespace acme\nimport <acme/base.wcl>\n");

// System imports come out of the registry; every other path still
// reads from disk.
let loader = reg.loader(disk_loader());

let doc = Document::open_at_with_loader(
    source,
    "platform.wcl",
    Some(PathBuf::from("/srv/platform")),
    &Environment::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:

text
$ 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:

text
$ 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:

text
$ 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.wclwcl
# tags.wcl — items spliced into whichever block imports it.
tag { key = "env"   value = "prod" }
tag { key = "team"  value = "platform" }
tagged.wclwcl
@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"
}
text
$ 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:

backwards.wclwcl
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
text
$ 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:

wcl
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
FormBindsReach for it when
use ns.NameNameThe name is long and you write it often
use ns.Name as AliasAliasTwo namespaces offer the same name
use nsEvery member of ns, bareOne namespace should win the whole file
use ns as AliasAlias.Name — and Alias::kind for block kindsYou want the namespace visible at each use
use ns.{A, B as C}A and CSeveral 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.wclwcl
# lib/billing.wcl — a second library, with a name that collides.
namespace acme.billing

symbol_set Tier { monthly  annual }
plans.wclwcl
import "./lib/net.wcl"
import "./lib/billing.wcl"

@document
type Plans { plan: Tier }

plan = :monthly
text
$ 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:

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:

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:

text
$ 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:

text
$ 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:

staging.wclwcl
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:

text
$ 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:

text
$ 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.

KeywordAnswersWritten inAffectsWrong tool when
importWhich files are in the document?Any file, top level or in a blockThe whole documentYou wanted to rename something — it never does
namespaceWhat are my declarations called?The first line of a fileEvery declaration in that file, everywhereYou wanted a shorthand for one file — that is use
useHow do I write those names?The top level of the file you openThe whole document, imported files includedYou wanted the declaration itself renamed
::Which namespace's block kind?One block instanceThat instance onlyThe 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