Types
Pluvial is statically typed: every value’s type is known at compile time, and type errors are reported before the VM executes a single instruction. There are no implicit conversions between types — ever. Conversions are always written explicitly.
Basic types
Section titled “Basic types”int # signed integerfloat # double-precision floating pointbool # true / falsestring # immutable byte sequenceauto # type inferred from the initializer, then fixedThere is no void type. A function that returns nothing leaves the return-type slot empty (see Functions).
int width
Section titled “int width”int is a signed 49-bit integer: the range is [-2^48, 2^48 - 1] (approximately ±2.81 × 10¹⁴). This range is a deliberate consequence of the NaN-boxed value representation: the qNaN payload has 49 bits available, and Pluvial spends them on the int tag.
For typical scripting workloads (file sizes, timestamps, array indices, loop counters) this range is more than enough. Values that exceed it wrap (two’s-complement truncation to 49 bits) — the behavior is defined but probably not what you want.
No implicit conversions
Section titled “No implicit conversions”| Code | Result |
|---|---|
int + float | compile error |
string + int | compile error |
if (5) | compile error — if requires bool |
to_float(1) + 2.5 | 3.5 (explicit) |
Use the conversion built-ins (to_int, to_float, to_string, to_bool) to cross type boundaries.
Static vs runtime errors
Section titled “Static vs runtime errors”- Type errors are compile errors (exit 65). The VM never runs.
- Runtime errors (exit 70) are reserved for value errors that the type system cannot prevent — integer division by zero, integer
%by zero, negative integer exponents, shift counts out ofintwidth, array index out of bounds,pop()on an empty array, slice indices out of range, and stack overflow.
Nullable types — T?
Section titled “Nullable types — T?”Any basic type can be made nullable by appending ?: int?, float?, bool?, string?.
Non-nullable is the default. A plain int or string is guaranteed to hold a real value — null cannot sneak in through the type system. The literal null (exactly four characters) represents absence.
int? a = null # OKint? a = 5 # OK — widening T → T? is safeint x = null # compile error — int is not nullableint x = 5x = null # compile errorauto a = nullis a compile error: the base type cannot be inferred fromnullalone. Writeint? a = null.- A nullable that has not been narrowed cannot be used as its base type. Arithmetic, comparisons, passing to a plain-
Tparameter, returning as plainT, or assigning to a plain-Tvariable are all compile errors. - See Syntax — Flow typing for how to safely access a nullable.
Result types
Section titled “Result types”Result<T> represents either a successful value of type T (ok(value)) or an error message (err("…")). Result<T, E> extends this so that the error side can be a user-defined struct.
Result<int> r = to_int("42")if (r is ok) { println(r.value) # 42} else { println(r.error) # error message (string)}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 })}Rules:
Tmust be a non-Result, non-nullable base type (int,float,bool,string).E(when supplied) must be astruct.Result<int, string>is a compile error — thestring-error case is the job ofResult<T>.r.valueis only accessible after narrowing withis ok;r.erroris only accessible afteris err.Resultis not abool.if (r) { ... }is a compile error — useif (r is ok) { ... }.matchworks onResulttoo —match r { ok(v) => { ... } err(e) => { ... } }.auto x = err("m")is a compile error:Thas no source-level position to pin down. WriteResult<int> x = err("m")instead.
Collections — array<T>
Section titled “Collections — array<T>”array<T> is a same-type, variable-length sequence. There is one array type, not separate fixed-length and dynamic types.
array<int> nums = [1, 2, 3]array<string> words = ["a", "b"]array<int?> maybe = [1, null, 3]- All elements must share the same type.
[1, 2.5]is a compile error. - The empty literal
[]infers its element type from context (array<int> a = []is fine;auto a = []is not). array<T>is invariant:array<int>andarray<int?>are different incompatible types.- Arrays are reference values —
array<int> b = ashares the buffer;b.push(2)is visible througha.
Read and write with arr[i]:
arr[i]returns the element (typedT, orT?forarray<T?>).arr[i] = exprrequiresexprto match the element type exactly.- The index
imust be anint. Out-of-bounds access is a runtime error (exit 70).
Element nesting (array<array<T>>, array<Result<T>>) is not currently supported.
See the standard library for the full set of array methods.
Collections — map<K, V>
Section titled “Collections — map<K, V>”map<K, V> is a hash map. The key type K must be string, int, or bool (non-nullable). The value type V is any base type, optionally nullable.
map<string, int> m = {"a": 1, "b": 2}map<string, int> empty = {} # OK — K/V from contextauto m = {} # compile error — cannot infer{ is dispatched by position: at statement position it begins a block; at expression position it begins a map literal. The two never collide in the grammar.
Reading and writing:
m[key]returnsV?— keys that are not present returnnull, never a runtime error. The caller must narrow before using the value. This is the central safety choice that distinguishes map indexing from array indexing.m[key] = valueinserts or overwrites. The types ofkeyandvalueare checked strictly againstKandV.- Maps are reference values, like arrays.
Nested maps and maps of arrays (map<K, array<T>>) are not currently supported.
See the standard library for the methods.
User-defined types — struct
Section titled “User-defined types — struct”struct defines a record type with named fields and optional methods.
struct Point { int x int y
def distance() float { return to_float(self.x * self.x + self.y * self.y) }
def translate(int dx, int dy) Point { return Point { x: self.x + dx, y: self.y + dy } }}
Point p = Point { x: 3, y: 4 }float d = p.distance()Point q = p.translate(1, 1)Definition:
- Fields are declared
type name. There are no field initializers in the definition. - Methods are defined with
definside thestructbody.selfis an implicit first parameter — you do not list it.
Instantiation:
- Use
Name { field: value, ... }. Every field must be supplied, in any order. Missing, unknown, mistyped, or duplicate fields are compile errors.
Field access:
p.fieldreads.p.field = exprwrites.expr’s type must match the field type exactly.- Field access does not copy — nested writes like
outer.inner.x = 3work as expected.
Methods:
- Call with
p.method(args).selfis bound to the receiver and is read-only (self = otheris a compile error).self.field = expris allowed and updates the caller’s struct.
Value semantics:
structvalues are copied on assignment, on argument passing, on return, and when written into a struct field. This is the opposite ofarrayandmap(which share by reference). The choice is per-use-case: collections share, records are independent.
Nominal typing:
- Two structs with identical field lists are different types.
PointandColorare distinguished by name, not by shape.
Not currently supported in struct: equality (== / !=), default field values, static methods (def Point.create()), inheritance / traits / interfaces, recursive structs, and capturing self from a lambda.
Strings
Section titled “Strings”String literals support six escape sequences:
"\n" # newline (0x0A)"\t" # tab (0x09)"\\" # backslash"\"" # double quote"\r" # carriage return (0x0D)"\0" # NUL byte (0x00, reserved for future C interop)Any other escape (\x, \u, \q, …) is a compile error.
Strings are immutable byte sequences. Comparisons (== / !=) are content comparisons. See the standard library for the eighteen string methods.
Case conversion (to_upper, to_lower) and predicates (is_digit, is_alpha) currently operate on the ASCII range only.