Modules and imports
This specification owns how a program is assembled from source files: files as
modules, use, qualified names, versions written on repositories, pinning,
one version per repository, std as the language version, the prelude, where
@lang may appear, import cycles, submitted bundles, and name collisions
between imports and local names.
It does not own the spelling of paths, names and keywords
(grammar.md), visibility and constructor resolution
(types.md), method syntax (semantics.md), the
@tool declarations a module may contain and how the host binds them
(tools.md), the contents of std and its @lang declarations
(stdlib.md), program identity and the stored module sources
(history.md), or the configuration a module repository carries
for its Tools (abi.md).
Examples are illustrative. A flow block is a fragment of source in the
decided syntax.
A file is a module
A module is one source file. There are no inline module blocks and no
other unit of code. A program is the file a run starts from together with
every module its imports reach, directly or through other modules.
A program needs no other file. There is no manifest, project file or build configuration beside a runnable program: its imports carry all of its dependency information. The same file means the same thing whether it is run standalone, embedded in an application, or submitted as source.
Any file may be the file a run starts from; which of its functions starts the run is chosen at run time (execution.md).
Before a run starts, the whole program is resolved: every import names one file at one version, and the program is checked as a whole. Evaluation never loads code, and no module adds a path to the outside world besides Tool calls.
Serves foundations: Flow owns a closed evaluation model.
use
use "path" is the only import form. It binds the last segment of the path as
a module name in the importing file:
use "std@v1/task"
use "./helpers"
use "github.com/acme/tools@v1.2/catalog"
pub flow main(input: Input) -> Report !tool = {
let views = task.map_all(input.refs, flow(r) = helpers.describe(catalog.read(r)))
summarize(views)
}Open in playground →These bind task, helpers and catalog.
Every imported name is used qualified. An item from an imported module is
written as the module name, ., and the item: list.map, catalog.Document.
A constructor is qualified by its type where its type isn't known from context:
catalog.Answer.Dismissed. There is one separator for everything. There is no
import of individual names and no glob import, so every use of an outside name
shows where it comes from.
Only an imported module's pub items can be reached through it;
types.md owns what pub exposes.
Renaming is for collisions. When two imports would bind the same name, one of them is given another name, written before the path:
use "github.com/acme/tools@v1.2/catalog"
use shop "github.com/other/store@v3/catalog"Open in playground →An import is renamed only to resolve such a collision.
Tools are imported like any module. A Tool is a @tool function declared
in a module (tools.md); importing the module brings its Tools with
it. There is no separate import form for Tools.
Paths and versions
A path takes one of three forms:
| Form | Example | Names |
|---|---|---|
| std | std@v1/list |
a module of the standard library |
| remote | github.com/acme/tools@v1.2/catalog |
a module in a repository, at a version |
| relative | ./helpers, ../shared/text |
a file beside the importing file |
A path names a file without its .flow extension.
A version is written on the repository. A repository is the unit that is
released, so its version is written once, after the repository and marked by
@, and the path inside the repository follows. A remote path always carries a
version. The version is a tag or a commit:
use "github.com/acme/tools@v1.2/catalog" // a tag
use "github.com/acme/tools@3f8a3a8/catalog" // a commitOpen in playground →A relative import belongs to the importing file's version. It names a file in the same repository or local directory as the importing file, at the same version, and carries no version of its own. A relative import inside a remote module resolves within that repository at the version the remote import chose.
Pinning
A tag can be moved; a commit cannot. The first time a tag is resolved, the
commit it names is pinned, and every later resolution of that tag must reach
the same commit. If the tag has since moved, the program is refused rather than
silently changed, until the new commit is accepted explicitly. The reference
toolchain keeps pins in a per-machine cache and accepts a moved tag through
flow update (execution.md).
Every run records the commits its program resolved to (history.md), so replay and resume never resolve a tag again.
One version per repository
A program uses one version of each repository. Two versions would give two incompatible copies of every type in it.
Imports resolve together, across the whole program, remote modules' own imports included:
- Requests for one repository within one major version resolve to the highest version requested.
- Requests for different major versions of one repository are an error.
- Two different commit pins of one repository are an error.
The standard library
std is a versioned repository like any other, imported by path:
use "std@v1/list". It is Flow source. Functions and types the compiler
implements are declared in it with @lang, so their signatures can be read and
navigated like any other (stdlib.md).
The std version is the language version. std's @lang declarations are the
compiler's, so std@v1 means Flow v1. The program's language version is read
from its std imports; there is no separate version string.
A program uses one std.
- Every std import in the program names the same major version; different majors are an error.
- A path may ask for a minimum minor version,
std@v1.4. The program uses the highest minor any import asks for. Asking for a minor newer than the toolchain provides is an error. Minor versions only add. - A program that imports nothing from std uses the newest std major the toolchain provides, and history records which (history.md). Importing any std module pins it.
@lang appears only in std. A program and its other modules may not declare
anything with @lang.
Method syntax on a built-in type needs no import
(semantics.md); calling the same function by its
qualified name, list.map(items, f), still takes the import.
The prelude
A small prelude is visible in every module without an import:
- the built-in types
Int,Decimal,Bool,Text,Bytes,List,Dict,Set,UnitandNever; Option,ResultandToolProblem, with their constructors.
Everything else is imported and used qualified, task.spawn(...) and
task.Task included. The list lives in std's @lang(prelude) module, so
changing it is a std change.
Names the prelude exports can't be declared again. This covers only type and
function names. A program's enum may have constructors named Failed or
Unknown; where a bare constructor is ambiguous, it is qualified by its
type, as in ToolProblem.Unknown (types.md).
Serves foundations: stable meaning.
Import cycles
Modules within one repository, or within one local directory tree, may import each other in cycles. Every top-level signature is written in full and every top-level constant is pure and acyclic (semantics.md), so a cycle poses no ordering problem for checking or for setting up constants.
A cycle across repositories is refused: no version of either repository could be released first.
Submitted source
A program can be submitted as source text, through the CLI, a Tool or another host surface, rather than read from a file. Submitted source has no directory, so a lone submitted file may not use relative imports; std and remote imports resolve as usual.
A submission may instead be a bundle: a set of files, each given by its path and its source. Relative imports resolve within the bundle, as they would within a directory, and a relative import that leaves the bundle is refused.
A submitted program is the same language as a program read from disk, with the same meaning.
Name collisions
- A local binding may not share a name with an import. A
letbinding, pattern binding or parameter whose name matches a module name the file imports is an error; the diagnostic suggests renaming the binding or the import. - Field names never collide with imports, since a field is always reached
through
.. - Two imports may not bind one name unless one is renamed (above).
- Prelude type and function names can't be declared again (above).
Refusals
A program is refused before it runs when:
- an import does not resolve to a file;
- a remote import carries no version;
- a lone submitted file uses a relative import, or a bundle's relative import leaves the bundle;
- one repository is requested at two major versions, or at two different commit pins;
- std is requested at two major versions, or at a minor version newer than the toolchain's;
- a pinned tag now names a different commit and has not been accepted;
- imports form a cycle across repositories;
- a name is reached through a module that does not declare it
pub; - a local binding shares a name with an import, or two imports bind one name;
- a module other than std declares something with
@lang.
checking.md owns how each refusal is reported.
No program reflection
A program cannot list its imports, read the versions or commits it resolved to, read its own program identity, or see which implementation is bound to any of its Tools. Hosts and tooling can read all of these. A program that needs one as data receives it as a declared input or as a Tool reply, recorded like any other value.
Serves foundations: visible outside influence.