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 // -42Open 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 // -1Open 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, exactOpen 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.divor%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.
Related documents
- 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.