core — list, map, str, math, rand, time
The core methods and modules that work without `use` — list/map methods and the str/math/rand/time functions.
All of these are core — they work without use (just like log).
list — list methods
On a value, .method:
l.len # length
l.push x # adds an element → a new list
l.filter \x -> x > 0 # keeps those matching the condition → a new list
l.map \x -> x * 2 # transforms each → a new list
l.has x # is it inside → bool
l.index x # index of the first matching element, -1 if not found
l.find \x -> x > 4 # first element matching the predicate, nil if not found
l.0 l.1 # element by index
l.slice a b # the a..b range (b excluded) → a new list
l.join ", " # → text: [1 2 3].join "," → "1,2,3"
l.reduce 0 \acc x -> acc + x # accumulate: (initial value, function)
l.sort # natural order (numbers or strings) → a new list
l.sort \a b -> a.p - b.p # comparator returns a number: negative → a first
l.reverse # reversed order → a new list
l.uniq # removes duplicates (first occurrence kept)
l.flat # flattens one level: [[1 2] [3]] → [1 2 3]
l.zip other # pairs up: [1 2].zip ["a" "b"] → [[1 "a"] [2 "b"]]
l.any \x -> x > 4 # does any match → bool (stops at first match)
l.all \x -> x > 0 # do all match → bool (stops at first mismatch)Important
To build a list use l.push x, not l + [x]. To filter, use l.filter
instead of a manual each loop; to build text, use l.join instead of a manual
accumulator:
# Manual (long): With methods (clean):
result <- [] result = items.filter \t -> t.active
each t in items
if t.active
result <- result.push t
text <- "" text = names.join ", "
each n in names
text <- text + n + ", "map — key-value methods
On a value, .method:
m.set k v # sets/updates a key → a new map
m.del k # removes a key → a new map
m.merge other # merges two maps (other's keys win) → a new map
m.has k # is the key present → bool
m.keys # a list of keys
m.vals # a list of values
m.key m[k] # read (m[k] — dynamic, variable key)Important
To write to a map use m.set k v. m[k] only reads (does not write).
This is consistent with lists: push for a list, set for a map. Shared state
(for example, who is in which room in realtime) is managed with these methods.
str — text functions
str.len s # length (number)
str.slice s 0 3 # the 0..3 range (3 excluded): "hello" → "hel"
str.up s # UPPERCASE
str.low s # lowercase
str.split s "," # split → a list: "a,b" → ["a" "b"]
str.has s "part" # is it inside → bool
str.int "42" # text → number
str.str 42 # number → text
str.trim " s " # strips leading/trailing whitespace → "s"
str.replace s "-" "+" # replaces every "-" with "+"
str.starts s "/api" # starts with prefix → bool
str.ends s ".fx" # ends with suffix → bool
str.pad "7" 3 "0" # pads on the LEFT → "007"
str.repeat "ab" 3 # repeat → "ababab"Why is str.len s different from list.len?
List length is a member (list.len), text length is a module function
(str.len s). The reason: a list and text are separate types, and their
operations should not mix. If both were the same .len it would be confusing.
math — math
math.floor 3.7 # → 3
math.ceil 3.2 # → 4
math.abs -5 # → 5
math.min 3 7 # → 3 (ints in, int out)
math.max 3 7 # → 7
math.pow 2 10 # → 1024 (int ^ non-negative int → int)
math.sqrt 9 # → 3.0 (always flt; negative input is an error)rand — random
rand.int 1 100 # a random integer in the range 1..100
rand.str 6 # a random string of 6 characters (short codes)rand is backed by the OS cryptographic CSPRNG, so its output is not predictable.
But length matters too: rand.str 6 yields only ~36 bits of entropy (62⁶) —
fine for a short code, but brute-forceable as a secret. For session IDs, tokens,
and other secrets use at least rand.str 24 (~140+ bits).
time — time and date
time.now # the current time (timestamp)
time.ago 24 :hr # the time 24 units ago. Units: :sec :min :hr :day
time.in 60 :min # the time 60 units later (TTL/expiry). Same units
time.fmt t "..." # format a timestamp into text
time.sleep 1 # waits 1 second (flt too — 0.5). Polling/retry backoff
time.parse "2026-06-10T10:00:00Z" # arbitrary ISO text -> canonical UTC timestamp ("Z"/"±HH:MM")
time.add t 30 :min # offset from ANY timestamp (not now): end_at = start_at + duration
time.sub t 5 :min # mirror of time.add — shift backward (e.g. buffer before)
time.diff a b # (a - b) difference in seconds (int); / 60 -> minutesDifference between time.in/time.ago (offset from now) and
time.add/time.sub (offset from any given timestamp): a booking server
computes end_at = time.add start_at 30 :min from a client-supplied start_at.
Instead of writing raw now() - interval '24 hours' in a DB query, use time.ago
— it is clean and safe:
r = db.one "select count(*) c from tickets where created > $1" [time.ago 24 :hr]Duration & interval recipes (interval arithmetic IS available —
time.add/diff exist):
end_at = time.add start_at dur :min # duration: start + dur minutes
mins = (time.diff end_at start_at) / 60 # gap between two times -> minutes
overlap = a.start < b.end & a.end > b.start # do two intervals overlap? (bool)
buf_start = time.sub start_at 15 :min # buffer: 15 min before startIANA timezone / DST — time.parse takes an optional zone name; time.fmt
takes an optional zone as a third argument. Wall-clock ↔ UTC conversion is DST-aware
(NOT a fixed offset), so "09:00 local every day" lands on the correct UTC instant
across summer/winter transitions:
utc = time.parse "2026-07-15 09:00:00" "Asia/Tashkent" # local wall-clock -> UTC
loc = time.fmt utc "HH:mm" "America/New_York" # UTC instant -> zone wall-clockA wall-clock time in a spring-forward gap (e.g. 02:30 on the night clocks jump)
does not exist and raises an error; an unknown zone name raises too.