Global builtins reference
Around 95 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.
Build: push, pop, set, insert, remove, range, list.
Three 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.
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, is_null, is_map, is_list, is_string, is_number.
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.
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, uuid.
See Embeddings, Budgeting, Retry, Sessions.
Built-in types and their variants
Five structured types are defined before your program starts, so match works on them out of the box and ai[T] has something ready to extract into:
| type | variants |
|---|---|
Result | Ok(value), Err(value) |
Option | Some(value), None |
Sentiment | Positive, Negative, Neutral |
Classification | Spam, Important, Social, Promotions |
Entity | Person(name, role), Organization(name, industry), Location(name, country) |
The variants with fields are called; the rest are values you write directly:
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, pmap.
sleep takes seconds, and accepts a fraction - sleep(0.25) is a quarter of a second. Worth stating because the rest of the language is not uniform here: std.bg's schedulers take milliseconds (bg.after(ms, f)), as do io.timeout and watch.next, and every tuning environment variable is *_MS. Each signature below says which it means.
Secrets
secret, reveal, is_secret. See Secrets.
Errors and assertions
error, assert. See Error handling.
Output
print, print_no_newline, read_file, write_file.
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_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, because it is usually accidental. 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.