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

Data

This specification owns the meaning of Flow's built-in data types: Int, Decimal, Bool, Text, Bytes, List, Dict, Set, Option, Duration and Instant. For each it fixes the values, the meaning of the literals that build them, the operators std declares for them, the order values of the type take, and the cases that fault. Nothing here assigns a policy meaning to a value: money, limits, counts and deadlines are application data.

Literal and operator spelling, precedence and the text escapes belong to grammar.md. The data-type category, structural equality, ordering and text form of records, enums and tuples, and Unit, Never and tuples themselves belong to types.md. How each value crosses a Tool boundary is tools.md's, and its exact encoding is runtime/spec/abi.md's. Which std module declares each type and operator, and the rules every std function follows, are stdlib.md's. Faults and how they end a run are errors.md's.

Built-in types are declared in std

Every type below is declared in std source, without a body, in its home module, and marked as data there (stdlib.md). The compiler implements it; the declaration is what a reader navigates to. Each operator in this file is a function std declares for its operand type, and each literal form builds the type std marks for it. No rule here names a type the compiler knows by any other route.

All of these types are data types under types.md: they have equality, an order and a text form, they can cross a Tool boundary, and a generic function over them needs nothing written.

Values are immutable. An operation that "adds" to a list, a dictionary or a set returns a new value and leaves its input unchanged. No value of these types carries identity beyond its contents: two equal values behave equally in every operation, including iteration order.

Every rule below is exact. Pure computation is not recorded in history (history.md); replay computes it again, so two conforming implementations that disagreed on any result here would disagree on the run. No rule depends on the host machine's word size, locale, hash order or floating-point unit.

Int

Int is the integers: every whole number, with no upper or lower bound. There is one integer type. There is no unsigned, sized or platform-width integer, and no Nat; a value that must be non-negative is checked where that matters, for example where a Tool reply is decoded (tools.md).

Addition, subtraction, multiplication and negation are exact and never fault. Nothing wraps, saturates or overflows.

let a = 40 + 2                      // 42
let b = 9223372036854775807 + 1     // 9223372036854775808
let c = -a                          // -42
Open in playground →

An integer that grows large costs memory and time. Exhausting what the host provides is a host limit, which faults as errors.md states; it is not overflow, and the value it would have had is still the exact one.

The text of an Int is its decimal digits, with - before a negative one and no leading zero: -42.

A bound belongs where a boundary has one. Where a Tool or a protocol needs a bounded number, the bound is made explicit by a checked conversion on that side; inside a run, Int has none.

Integer division and remainder

Integer division is the std function int.div(a, b), also written a.div(b) by method syntax (semantics.md). It rounds toward negative infinity. % is the matching remainder. For a nonzero divisor b, the two satisfy, exactly:

a == a.div(b) * b + (a % b)

with a % b zero or carrying the sign of b, and smaller in magnitude than b. That fixes both results for every pair.

let q1 = 7.div(2)      //  3
let q2 = (-7).div(2)   // -4
let r1 = -7 % 2        //  1
let r2 = 7 % -2        // -1
Open in playground →

A zero divisor faults, for div and % alike: x.div(0) faults. It is the only Int operation that faults. A program that expects a zero divisor as ordinary input uses int.checked_div, which returns an Option (stdlib.md), or checks first:

flow average(total: Int, count: Int) -> Option<Int> = {
    guard count != 0 else { return None }
    Some(total.div(count))
}
Open in playground →

There is no integer-division operator. / is not declared for Int: 7 / 2 is a type error that names int.div, so an integer quotient can never silently drop a fraction.

Integer literals

An integer literal denotes the exact whole number its digits spell, in decimal, hexadecimal (0x) or binary (0b), with _ separators carrying no meaning. There is no octal form. A literal has no sign: -5 is unary minus applied to 5, which is the same value. No literal is too large.

Decimal

Decimal is the exact fractional type: every number with a finite decimal expansion. 0.1 + 0.2 == 0.3 is true. There is no floating-point type, because a value that is not equal to itself, such as a NaN, could not be data (types.md).

A decimal literal, such as 4.2, denotes exactly the number its digits spell. A numeral with a point is a Decimal; one without is an Int (grammar.md).

Decimal values are normalized: a value is its number, not its digits. 1.50 and 1.5 are the same value, equal, and identical in text and in every encoding, which drop trailing zeros after the point. Zero has one value, with no sign.

The text of a Decimal is plain decimal notation: no exponent, no trailing zero after the point, no point when the value is whole, - before a negative value, and 0 for zero. 1.50 prints as 1.5 and 2.0 as 2.

Addition, subtraction, multiplication and negation are exact and never fault. / is division:

  • a quotient with a finite decimal expansion is that exact value;
  • any other quotient is rounded to 34 significant digits, rounding half to even, the same way on every implementation;
  • a zero divisor faults.
let third = 1.0 / 3.0       // 0.3333333333333333333333333333333333
let half = 1.0 / 2.0        // 0.5, exact
let price = 19.99 * 3.0     // 59.97, exact
Open in playground →

% and int.div are not declared for Decimal.

Int and Decimal never mix implicitly. 1 + 0.5 is a type error, and so is 1 == 1.0. Converting between them is an explicit std function call (stdlib.md).

Bool

Bool has two values, true and false, which are keywords typed through std's declaration of Bool (stdlib.md). ! negates. && and || evaluate their left operand first and evaluate the right one only when it can change the result. A Bool prints as true or false. A condition in if, guard or a match guard is a Bool; nothing else is converted to one.

Text

Text is a finite sequence of Unicode code points. Source text and every encoding are UTF-8, so a Text never holds a surrogate. There is no character type; one character is a Text of length 1.

Text is counted in code points. Length, indexing and slicing count code points, so every cut leaves valid text, and a count does not change between Unicode versions. What a reader sees as characters (grapheme clusters) and the UTF-8 size in bytes are separate, explicit std functions.

Nothing is normalized implicitly. A Text is exactly the code points that built it. Equality compares code points, so "é" written as one precomposed code point and "é" written as e plus a combining accent are different until a program calls text.normalize. Ordering is lexicographic by code point, with a proper prefix first. There is no locale anywhere in the language: locale-aware collation or formatting is a Tool.

Case mapping follows Unicode's default, locale-independent full mappings, with the conditional final sigma and no other context. The Unicode version that case mapping, normalization, grapheme segmentation and a Regex's Unicode classes (stdlib.md) use is fixed by the std version, which is the language version (modules.md), never by the toolchain that built an implementation, so replay never changes a result; std v1 uses Unicode 16.0.0.

++ joins two texts. Interpolation, "${expr}", inserts the text form of any data value (types.md); a Text value inserts its own code points. Inside the text of another value, such as a list or a record, a Text is written as the one-line literal that denotes it (grammar.md): between " marks, with \\, \", \n and \t for a backslash, a quotation mark, a line feed and a tab, \${ for ${, \u{...} in uppercase hexadecimal with no leading zero for every other code point below U+0020 and for U+007F, and every other code point as itself. So Some("say \"hi\"") is the text of a Some holding say "hi".

A text literal denotes the code points it spells after its escapes are replaced, and a """ block denotes its lines with the margin removed (grammar.md).

Bytes

Bytes is a finite sequence of bytes, each 0 through 255, and the one type whose contents are raw and unnormalized. It has no literal. Equality compares bytes, and ordering is lexicographic by unsigned byte value, with a proper prefix first. Converting between Text and Bytes is explicit: text to bytes is its UTF-8 encoding, and bytes to text returns an Option that is None exactly when the bytes are not valid UTF-8, never substituting a replacement character. The text of Bytes is its standard base64 with padding.

List

List<T> is a finite sequence of values of one type, and the one sequence type. The literal [a, b, c] builds a list from its elements, evaluated left to right; [] is the empty list. ++ joins two lists. List patterns, [], [x], [x, ..rest] and [..init, last], take lists apart (semantics.md).

Indexing is zero-based, and looking up an index returns an Option: None for an index that is negative or not less than the length. Nothing faults on a missing element. A list's representation is the implementation's choice; growing at either end, indexing and joining are expected to be efficient, and no representation is observable.

Lists are equal when they have the same length and equal elements in order. Ordering is lexicographic by element, with a proper prefix first. A list prints as its elements' text in order, separated by , and between [ and ]: [1, 2], []. Sorting in std is stable: elements whose keys are equal keep their input order.

Dict

Dict<K, V> is a finite map from keys to values. K must be a data type, and any data type may be a key. The literal ["alpha": 3, "beta": 5] builds a dictionary, evaluated left to right; when two entries in one literal have equal keys, the later one is kept. [:] is the empty dictionary.

Iteration is sorted by key, in the key type's order. It never depends on insertion order or on a hash, so two equal dictionaries iterate identically. Looking up a key returns an Option; nothing faults on a missing key.

let scores = ["alpha": 3, "beta": 5]
let best = match scores.get("beta") {
    Some(n) => n,
    None => 0,
}
Open in playground →

Dictionaries are equal when they hold equal keys mapped to equal values. They order as their entry lists, in key order, would. A dictionary prints as its literal does, in key order: ["alpha": 3, "beta": 5], [:].

Set

Set<T> is a finite set of values of a data type. It has no literal; it is built from a list by a std function. Iteration is sorted by the element type's order, and equal sets iterate identically. Sets are equal when they have the same elements, and order as their sorted element lists would, and a set prints as its sorted element list does.

Option

Option<T> is an ordinary std enum, exported by the prelude (modules.md):

pub type Option<T> { None, Some(T) }
Open in playground →

It is declared with None first, so None orders below every Some, and two Some values order by their payloads. It is the one way to say a value may be absent: Flow has no null and no default value. An external field declared Option<T> is None when it is missing or null, and only an Option field may be missing or null (tools.md). Option is taken apart with match and guard let; ? does not apply to it (errors.md).

Duration and Instant

Duration, declared in std's duration module, is a length of time. Instant, declared in std's time module, is a point in time. Both are data: they compare, order chronologically, print, and cross Tool boundaries, as ISO 8601 text (runtime/spec/abi.md). Their text is that canonical ISO 8601 form: PT90S, 2026-03-21T09:30:00Z.

Neither reads a clock. The current time is a Tool call to std's clock module, recorded like any other (stdlib.md); once received, an Instant is ordinary data, and arithmetic over it is deterministic. Calendars, time zones and local time are not part of the language; a library or a Tool supplies them.

Faults

The operations in this file fault in exactly these cases:

  • int.div or % with a zero divisor;
  • / with a zero divisor.

Every other operation on these types returns a value for every argument: absence, a missing key and an out-of-range index are None, never faults. Exceeding a host limit, such as memory for a large value, faults as errors.md states for every operation.

  • grammar.md owns literal, operator and escape syntax.
  • types.md owns the data-type category and structural equality, ordering and text.
  • stdlib.md owns the std modules that declare these types and the functions over them.
  • tools.md owns what crosses a Tool boundary and how a reply is decoded; runtime/spec/abi.md owns the encoding.
  • errors.md owns faults.

Serves foundations: Deterministic reconstruction and Stable meaning.