Skip to content

Standard library

Pluvial’s built-ins are implemented as native (OBJ_NATIVE) functions and are visible from any module. The set is intentionally small — the dedicated standard library at std/ is reserved but not yet shipped.

Writes x to standard output. Does not append a newline.

Writes x to standard output and appends a newline.

Both accept any value type: int, float, bool, string, array<T>, Result<T>, or T?. The formatting matches the internal value_print used everywhere else:

TypeFormatted as
intdecimal
float%g with a trailing .0 for integral values
booltrue / false
stringunquoted
array<T>[e1, e2, e3]
Result<T>ok(x) / err(msg)
T?the value, or null

print / println return no value: int x = println(42) is a compile error.

Variadic forms (print(a, b, c)), format strings, and writing to stderr or files are not currently provided.

All conversions are explicit. There is no automatic promotion between numeric types or between bool and numeric types.

Built-inResultNotes
to_string(int | float | bool)stringOne built-in dispatches on the argument type.
to_int(float)intTruncates toward zero. Loss of precision is intentional.
to_float(int)floatLossless.
to_int(string)Result<int>Whole-string parse; rejects trailing whitespace and partial matches. Overflow returns err(...).
to_float(string)Result<float>Whole-string parse.
to_bool(string)Result<bool>Accepts "true" and "false" only; case-sensitive.

Forbidden conversions (compile errors): to_int(bool), to_float(bool), to_bool(int), to_bool(float). bool and numeric types are not interconvertible — this preserves the “no implicit conversions” rule on the explicit-conversion side.

Result<int> r = to_int("42")
if (r is ok) { println(r.value) } # 42
Result<int> bad = to_int("hello")
if (bad is err) { println(bad.error) } # parse error message

The receiver s must be string (non-nullable). Calling a string method on a string? without narrowing is a compile error.

  • s.lengthint — byte length. For ASCII, this equals the character count.
MethodReturnsNotes
s.is_empty()booltrue if s.length == 0
s.is_digit()booltrue if every byte is a digit and s is non-empty
s.is_alpha()booltrue if every byte is an ASCII letter and s is non-empty
s.contains(sub)boolsubstring search
s.starts_with(pre)bool
s.ends_with(suf)bool
MethodReturnsNotes
s.to_upper()stringASCII range only
s.to_lower()stringASCII range only
s.trim()stringstrips ASCII whitespace (' ', \t, \r, \n) from both ends
s.lstrip()stringleft side only
s.rstrip()stringright side only
s.replace(old, new)stringreplaces all non-overlapping occurrences; if old == "" returns s unchanged
s.repeat(n)stringn must be int; n == 0 returns ""; n < 0 is a runtime error ("repeat count must be non-negative")
s.count(sub)intnon-overlapping occurrences; empty sub returns 0
  • s.index(sub) -> Result<int> — returns ok(byte_offset) if found, err("substring not found") otherwise. Empty sub returns ok(0).
  • s.substring(start, end) -> Result<string> — half-open slice [start, end). Out-of-range indices return err("substring index out of range").
  • s.split(sep) -> array<string> — splits on sep. If sep == "", splits into single bytes ("hello".split("") → ["h","e","l","l","o"]). On an empty receiver the result is always [""].
  • sub, pre, suf, old, new, sep must be string.
  • start, end, n must be int.

These restrictions are not user-visible overloads; they are part of the same multi-type built-in mechanism used elsewhere (only the built-ins use it — user-defined functions still follow “one name = one definition”).

The receiver arr has type array<T>.

  • arr.lengthint — current number of elements.
  • arr.push(x) — appends x to the end. x must have type T exactly. Returns no value.
  • arr.pop() -> T — removes and returns the last element. Runtime error on an empty array (exit 70).
  • arr.reverse() — reverses the array in place. Returns no value.
  • arr.sort() — sorts in place, ascending. Supported only for non-nullable array<int>, array<float>, array<bool>, array<string>. Returns no value.
  • arr.contains(x) -> bool — linear search; x must match the element type exactly.
  • arr.slice(start, end) -> array<T> — half-open slice. Out-of-range indices are a runtime error.
  • arr.join(sep) -> string — only valid on array<string>. Other element types are a compile error.
  • arr.map(fn) -> array<T2> — applies the lambda to every element, returning a new array. T2 is the lambda’s return type.
  • arr.filter(pred) -> array<T> — keeps the elements where pred returns true.
  • arr.reduce(init, fn) -> R — folds the array with fn(acc, element) starting from init.
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)) # true
int last = a.pop() # last == 1, a == [9, 5, 4, 3, 1]
array<int> head = a.slice(0, 3) # [9, 5, 4]
array<string> words = ["one", "two", "three"]
println(words.join(" / ")) # "one / two / three"
println([1,2,3,4,5].map((x) => x * 2)) # [2, 4, 6, 8, 10]
println([1,2,3,4,5].filter((x) => x > 2)) # [3, 4, 5]
println([1,2,3,4,5].reduce(0, (acc, x) => acc + x)) # 15

The receiver m has type map<K, V>.

MethodReturnsNotes
m.lengthintlive entry count (property — no parentheses)
m.has(key)boolkey must match K exactly
m.delete(key)boolreturns whether the key was present; uses tombstones internally
m.keys()array<K>hash-table order, not insertion order
m.values()array<V?>element type is always V? to match m[key]
m.clear()(no value)removes all entries; the underlying buffer is reused, no reallocation

Indexing: m[key] returns V? — missing keys yield null, never a runtime error. m[key] = value strictly type-checks both key and value.

map<string, int> counts = {}
for w in "a b c a b a".split(" ") {
int? n = counts[w]
if (n != null) { counts[w] = n + 1 }
else { counts[w] = 1 }
}
for (k, v) in counts {
if (v != null) { println(k + "=" + to_string(v)) }
}
# e.g. a=3, b=2, c=1 — hash-table order

Methods defined inside a struct are part of that type and are called with the same .method(args) syntax. See Types — struct for the definition syntax. Struct methods are not part of any global namespace — they exist only through a receiver.