Global builtins reference
About 110 functions are always in scope, with no import.
Collections
Transform: map, filter, reduce, flatten, zip, enumerate, chunk, window, unique, reverse, sort, sort_by, sort_with, group_by, partition, frequencies, select.
Access: first, last, take, drop, slice, get, has, len, contains, index_of, find, count, any, all, sum, min, max, empty_map, keys, values, merge.
Build: push, pop, set, insert, remove, range, list.
Four of those are easy to mix up:
set(coll, key, value)replaces;insertsplices a new element in and the list gets longer. On a map both set the key. Reaching forinsertwhen you meantsetchanges the length without saying so.drop(list, n)is the complement oftake, sotake(n) + drop(n)rebuilds the input.slice(list, start, end)is the half-open range, with out-of-range bounds clamped rather than raised.has(map, key)asks whether the key exists, which is not the same question asget(map, key) == null- a key can be present and hold null. It works on structs too, and refuses anything else rather than answeringfalse.sort_bytakes a key;sort_withtakes a comparator returning a negative number, zero or a positive one. Reach forsort_withwhen the order cannot be reduced to a key - a comparison whose answer depends on the operands' types, for instance. Only the sign counts, sofn(a, b) a - bsorts Floats as well as Ints; a comparator that returns NaN is an error.
Parallel: pmap.
get is the nullable lookup - get(m, k) and get(xs, i) return null on a miss, where m.k and xs[i] raise. See Structured data access.
Strings
upper, lower, trim, split, join, replace, contains, starts_with, ends_with, chars, lines, reverse, index_of, len, escape_html.
is_blank is not a global - it lives in std.str.
The full toolkit is std.str.
Numbers and conversion
int, float, decimal, bool, string, bytes, abs, floor, ceil, round, pow, sqrt, min, max, sum, approx, type_of, type_name, is_null, is_map, is_list, is_string, is_number.
type_of says what kind of value something is, so every record is "struct". type_name says which declared record or variant it is - type_name(Point(1, 2)) is "Point" - and for anything else gives the same kind type_of does.
The predicates read better than type_of(x) == "map" in a condition that already has two other clauses, which is most conditions in code that walks JSON. is_number is true for Int, Float and Decimal.
min and max take a list or range, max([3, 7, 5]), or the values themselves, max(3, 7, 5). Either way the result is one of the values, so max(1, n / 2) is a Float whenever n / 2 wins: write n // 2 for an Int.
approx(a, b, eps?) is tolerance comparison for floats - == on floats is exact IEEE. See Numbers.
JSON
json_encode, json_decode. File forms are in std.json.
AI
embed, embed_all, cosine, tokens, cost, retry, session, model. UUIDs are in std.uuid.
See Embeddings, Budgeting, Retry, Sessions, and Providers for model and ai ... via.
Built-in types and their variants
Two structured types are defined before your program starts, so match works on them out of the box:
| type | variants |
|---|---|
Result | Ok(value), Err(value) |
Option | Some(value), None |
Until 0.62 three demo types were defined too - Sentiment, Classification and Entity. They took common names from every program, so they were removed; declare the set you want, and ai[T] extracts into it the same way. Naming a type that does not exist is an error, not a null:
type Sentiment = Positive | Negative | Neutral
mood = ai[Sentiment] "How does this review feel? {review}"
match mood {
Positive => print("glad to hear it")
Negative => print("let us make it right")
Neutral => print("noted")
}
See Typed output and Pattern matching.
Concurrency
cell, cell_get, cell_set, cell_update, channel, send, recv, try_recv, close, select, cancel, sleep, with_timeout, pmap.
sleep takes milliseconds, a whole number - sleep(250) is a quarter of a second - like every other duration in Ecko: with_timeout, bg.after, io.timeout, watch.next, and every *_MS environment variable. It took seconds before 0.58, so a Float is refused: sleep(0.25) is an error that says to write sleep(250). See Migration.
with_timeout(ms, f) takes milliseconds. It calls f() and returns its value, or throws { kind: "timeout", ms } once f has run for ms milliseconds. See Deadlines.
Secrets
secret, reveal, is_secret. See Secrets.
Errors and assertions
error, assert. See Error handling.
Output
print, print_no_newline, read_file, write_file.
n = 3
print("total:", n) # total: 3
print() # an empty line
print takes any number of arguments and writes them joined by one space, then a newline. print_no_newline joins the same way and writes no newline.
Python escape hatch
print(py("math.sqrt", 144)) # 12
print(py("os.path.join", "a", "b")) # a/b
py(name, args...) calls a Python function in a persistent worker process, for the case where a library exists there and nowhere else. It is the deliberate escape hatch: reaching for it stays inside your Ecko program rather than becoming a separate service.
It takes a dotted function path, never an expression. py("math.sqrt", 16) works; py("[x*2 for x in range(5)]") is refused. Python resolves the name with importlib and getattr and never eval, so even a name built from untrusted data cannot execute arbitrary code. Arguments travel as JSON values and are never spliced into source.
The worker inherits nothing. It gets only what Python needs to run - PATH, HOME, the locale and PYTHON* variables - so a snippet cannot read ECKO_AI_API_KEY or any other credential your program holds. Forward variables deliberately with ECKO_PY_ENV=NAME,OTHER.
One call cannot park the rest. A call that does not answer within ECKO_PY_TIMEOUT_MS (default 30000) raises kind timeout, and the worker is restarted so later calls still work.
It needs Python on the machine, which is the one place Ecko's "one binary, nothing to install" promise does not reach - so treat it as a bridge, not a foundation.
Shadowing
Assigning to a builtin name shadows it in the current scope rather than erroring, so sum = 0 is always safe. ecko check warns when the same file also calls sum(...), because that call now hits your binding. Reach the original through core.*:
export fn get(c, key) { ... }
fn internal(m, k) = core.get(m, k)
Arity
Every builtin's arity is checked at the call site by ecko check, before the program runs - so a wrong argument count is caught rather than raising mid-run.