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; insert splices a new element in and the list gets longer. On a map both set the key. Reaching for insert when you meant set changes the length without saying so.
  • drop(list, n) is the complement of take, so take(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 as get(map, key) == null - a key can be present and hold null. It works on structs too, and refuses anything else rather than answering false.
  • sort_by takes a key; sort_with takes a comparator returning a negative number, zero or a positive one. Reach for sort_with when the order cannot be reduced to a key - a comparison whose answer depends on the operands' types, for instance. Only the sign counts, so fn(a, b) a - b sorts 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:

typevariants
ResultOk(value), Err(value)
OptionSome(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.

See cell and Channels.

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.