Browse docs

Start here

examplesGetting started with Flowdocumentation

Design

FoundationsLanguage architecturePhilosophy

Language specification

Program checkingConcurrencyDataEffectsResults, Tool problems, and faultsGrammarHistoryModules and importsLanguage specificationEvaluationStandard libraryToolsTypes

Runtime

Runtime architectureThe host boundaryDiagnosticsRunning a programThe history format

Guides

Writing programs that reach checkpointsImplementing Tools with a toolkitLoops that never returnRecursive delegationSharing types between Tool modulesHarnesses over tool registries

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 commit
Open 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, Unit and Never;
  • Option, Result and ToolProblem, 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 let binding, 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.