Types
Base types
Section titled “Base types”The base types are:
int— a signed integerfloat— a doubleboolstringauto— inference placeholder (see Variables)
There is no void type. A function that returns nothing leaves the return-type
position empty.
The width of int
Section titled “The width of int”int is a signed 49-bit integer, with a range of [-2^48, 2^48 − 1] (about
±2.81 × 10^14). This is a deliberate trade-off: the NaN-boxed 8-byte value representation
reserves bits for the type tag, leaving 49 bits for the integer payload.
For everyday scripting — file sizes, timestamps in seconds, array indices, loop counters —
this is more than enough (±2.81e14 is roughly 8900 years of seconds, or 281 TB of bytes).
Values beyond the range are truncated to the low 49 bits with sign extension (two’s
complement wraparound).
No implicit conversions
Section titled “No implicit conversions”Type-crossing conversions never happen implicitly. Every conversion is written explicitly. This rule has no exceptions:
int + float # compile error (no automatic promotion) — write to_float(1) + 2.5string + int # compile errorif (5) # compile error — conditions must be boolUse the built-in conversion functions (to_int, to_float, to_string, to_bool) to
convert between types. See Built-in functions.
Static type checking
Section titled “Static type checking”Type errors are caught before the VM executes a single instruction — this is what “statically typed” means in Pluvial. Type errors are exit 65 (compile error). Only errors that cannot be known statically are runtime errors (exit 70): integer division by zero and stack overflow (deep recursion).
Nullable types (T?)
Section titled “Nullable types (T?)”Any base type can be made nullable by appending ?: int?, float?, bool?, string?.
A T? value is either a T or null (absence).
Non-nullable is the default. A plain int or string can never hold null. This is
the core of Pluvial’s null safety — null cannot sneak in through a type. The literal
null (exactly four characters) represents absence.
int? a = null # OKint? a = 5 # OK — widening T → T? is a safe expansionint x = null # compile error — 'int' is not nullableint x = 5x = null # compile error — cannot assign null to a non-nullable variable- Widening (
T → T?) is allowed; the reverse (T? → T) requires narrowing — see Control flow — Flow typing. - An un-narrowed nullable cannot be used as its base type (in arithmetic, comparison, as a plain-T argument or return value, etc.) — there is no implicit unwrap.
array<T>
Section titled “array<T>”array<T> is a homogeneous, growable collection of elements of type T — Python-list
style, with one kind only (no separate fixed/variable-length types).
The element type T is a base type, optionally nullable: array<int>, array<string>,
array<int?>. Nesting (array<array<T>>) and array<map<string, T>> are supported (one
level deep, base-typed leaves).
array<int> a = [1, 2, 3] # element type inferredarray<string> s = ["a", "b"]auto mixed = [1, null, 3] # array<int?> — null makes the element nullablearray<int> empty = [] # empty literal: element type from context- All elements must be the same type —
[1, 2.5]is a compile error. [null]alone is a compile error (cannot infer a base type).auto a = []is a compile error (cannot infer an element type).- Invariance:
array<int>andarray<int?>are distinct, incompatible types. This closes the unsafe covariance hole found in Java arrays.
Index access
Section titled “Index access”arr[i] # read — returns T (array<int?> yields int?, needs narrowing)arr[i] = expr # write — expr's type must match T exactlyThe index i must be an int. An out-of-bounds access (i < 0 or i >= length) is a
runtime error (exit 70), reported as "array index N out of bounds (length M)".
Reference semantics
Section titled “Reference semantics”Arrays are mutable reference values — assignment copies the reference, not the contents:
array<int> a = [1]array<int> b = a # shares the same arrayb.push(2)a # [1, 2]There is no implicit deep copy.
Array methods
Section titled “Array methods”| Member | Kind | Result |
|---|---|---|
arr.length | property | element count (int) |
arr.push(x) | method | append x (no value) |
arr.pop() | method | remove and return the last element; runtime error on an empty array |
arr.contains(x) | method | bool — linear search |
arr.reverse() | method | reverse in place (no value) |
arr.join(sep) | method | string — array<string> only |
arr.sort() | method | sort ascending in place (no value); non-nullable int/float/bool/string only |
arr.slice(start, end) | method | a new array for the half-open range [start, end); out-of-range is a runtime error |
arr.map(fn) | method | a new array applying fn to each element |
arr.filter(pred) | method | a new array of elements where pred returns true |
arr.reduce(init, fn) | method | fold over the array |
array<int> a = [3, 1, 4, 1, 5, 9]a.sort() # [1, 1, 3, 4, 5, 9]a.reverse() # [9, 5, 4, 3, 1, 1]println(a.contains(5)) # trueint last = a.pop() # last == 1array<int> head = a.slice(0, 3)
array<string> words = ["one", "two", "three"]println(words.join(" / ")) # "one / two / three"Spreading is supported in literals: [...a, 4, 5].
map<K,V>
Section titled “map<K,V>”map<K,V> is a mutable hash map from keys of type K to values of type V.
- K must be a non-nullable
string,int, orbool— nullable keys are forbidden. - V is a base type, optionally nullable. (One level of nesting such as
map<string, array<T>>with base-typed leaves is supported.)
map<string, int> m = {"a": 1, "b": 2}map<string, int> empty = {} # K/V from contextauto m = {} # compile error — cannot inferThe { token is a block in statement position and a map literal in expression position, so
there is no grammatical ambiguity.
Index access — absence is null
Section titled “Index access — absence is null”m[key] # read — returns V? (always nullable)m[key] = value # write — key: K and value: V are required exactlyThis is the key safety choice: where an array reports an out-of-range read as a runtime
error, a map reports a missing key by returning null. Reading a map index always returns
V? and must be narrowed before use:
int? n = m["x"]if (n != null) { println(n + 1)}Writing never raises a runtime error — it inserts or overwrites.
Map methods and iteration
Section titled “Map methods and iteration”| Member | Kind | Result |
|---|---|---|
m.length | property | live entry count (int) |
m.has(key) | method | bool |
m.delete(key) | method | bool — whether the key existed |
m.keys() | method | array<K> (hash-slot order, not insertion order) |
m.values() | method | array<V?> |
m.clear() | method | remove all entries (no value) |
Maps are iterable in two forms:
for k in m { ... } # keys only (k: K, read-only)for (k, v) in m { ... } # key/value pairs (k: K, v: V?, both read-only)Map literals support spreading: {...m, "c": 3} (later duplicate keys win).
Tuples
Section titled “Tuples”A tuple is an anonymous, fixed-shape group of values:
auto t = (1, "hello") # a (int, string)println(t.0) # 1 — fields are accessed by indexprintln(t.1) # helloauto (x, y) = t # destructuring
def min_max(int a, int b) (int, int) { # a tuple return type if (a < b) { return (a, b) } return (b, a)}auto (lo, hi) = min_max(5, 2)Tuples are structurally typed: two (int, string) tuples are the same type. They are
implemented as anonymous structs and need at least two elements.
Structs and enums
Section titled “Structs and enums”User-defined record types (struct) and tagged types (enum) are covered on their own
pages:
- Structs — fields, methods, value semantics, struct update syntax.
- Enums come in three forms: simple, value-bearing, and data-carrying.
enum Direction { North South East West } # simpleenum Status { Ok = 200 NotFound = 404 Error = 500 } # valueenum Shape { Circle(float radius) # data Rectangle(float w, float h) Point }- Simple and value enums are represented at runtime as plain
ints; only value enums expose.value. - Data enum variants carry payloads and are matched and destructured with
match. array<Direction>and similar arrays of enums are supported.
Result<T> and Result<T, E>
Section titled “Result<T> and Result<T, E>”Result<T> is either an ok holding a value of type T, or an err holding a string
error message.
Result<T, E> lets the error be a struct type E instead of a string, so you can carry
structured errors:
struct ParseError { string message int line }
def parse(string s) Result<int, ParseError> { if (s.is_empty()) { return err(ParseError { message: "empty", line: 1 }) } Result<int> r = to_int(s) if (r is ok) { return ok(r.value) } return err(ParseError { message: "not a number", line: 1 })}
Result<int, ParseError> r = parse("bad")if (r is err) { println(r.error.message + " at line " + to_string(r.error.line))}Tis a non-Result, non-nullable base type.Emust be astruct(soResult<int, string>is a compile error — string errors belong toResult<T>).- Errors are nominally typed: two structs with identical fields are still distinct types.
Resultis not a bool:if (r) { }is a compile error. Useif (r is ok) { }.- An un-narrowed
Resultcannot be used directly; narrow it withis ok/is errbefore reading.value/.error, or destructure it withmatch. See Error handling.
ok and err constructors
Section titled “ok and err constructors”ok(expr)produces a success Result; the static type comes fromexpr.err("msg")produces a failure with a string message; the success typeTis inferred from context.err(StructInstance)produces a failure carrying a struct (forResult<T, E>).
Result<int> r = ok(5) # OKResult<int> r = err("bad") # OKResult<int> r = ok("x") # compile error — ok carries a stringauto x = err("m") # compile error — no context for T
struct AppError { string kind }Result<int, AppError> r = err(AppError{kind:"io"}) # OKResult<int, AppError> r = ok(5) # OK — ok leaves E unspecifiedResult<int, AppError> r = err("bad") # compile error — string ↛ AppErrorNesting (Result<Result<...>>) and Result<int?> are not allowed.