Synsemadocsv0.6.xENES

Reference

Builtins index

A categorized map of the built-ins. The full signatures live on each topic page; the core ones are doctested here so this reference can't drift.

builtins.syn
-- Doc reference: a few core builtins, doctested so the reference can't drift.
intent: "doc reference: core builtins"

print("upper(\"hi\") = " + upper("hi") + ",  join([a,b],\"-\") = " + join(["a", "b"], "-"))

test "collections"
    assert_eq(length([1, 2, 3]), 3)
    assert(contains([1, 2, 3], 2))
    assert_eq(slice([1, 2, 3, 4], 1, 3), [2, 3])     -- Python-style end-exclusive
    assert_eq(keys({"a": 1, "b": 2}), ["a", "b"])
    assert_eq(values({"a": 1, "b": 2}), [1, 2])

test "text"
    assert_eq(upper("hi"), "HI")
    assert_eq(trim("  x  "), "x")
    assert_eq(split("a,b,c", ","), ["a", "b", "c"])
    assert_eq(join(["a", "b"], "-"), "a-b")
    assert(starts_with("hello", "he"))

test "intentional ops"
    assert_eq(apply((n) => n * 2, [1, 2, 3]), [2, 4, 6])
    assert_eq(where([1, 2, 3, 4], (n) => n > 2), [3, 4])
    assert_eq(reduce([1, 2, 3], (a, b) => a + b, 0), 6)

test "intentional ops accept both orders (fn-first and list-first)"
    assert_eq(apply([1, 2, 3], (n) => n * 2), [2, 4, 6])     -- list-first also works
    assert_eq(where((n) => n > 2, [1, 2, 3, 4]), [3, 4])     -- fn-first also works
    assert_eq(reduce((a, b) => a + b, [1, 2, 3], 10), 16)    -- extra args stay at the end

test "unique and index_of"
    assert_eq(unique([3, 1, 3, 2, 1]), [3, 1, 2])            -- first-appearance order
    assert_eq(index_of([10, 20, 30], 20), 1)                 -- by item
    assert_eq(index_of([1, 2, 3], (n) => n > 1), 1)          -- by predicate
    assert_eq(index_of([1, 2], 9), nothing)                  -- absent → nothing, not -1
    assert_eq(index_of("héllo", "llo"), 2)                   -- on text: the character position

test "reverse and split(text, \"\") (engine v0.6.20+)"
    assert_eq(reverse([1, 2, 3]), [3, 2, 1])                -- a NEW list; the original is untouched
    assert_eq(reverse("abc"), "cba")                        -- text too (by Unicode scalar)
    assert_eq(split("abc", ""), ["a", "b", "c"])            -- empty separator → the characters
    assert_eq(split("", ""), [])

test "renamed in v0.6.29: the new names (the old ones are deprecated aliases until v1.0)"
    assert_eq(replace("a-b-c", "-", "+"), "a+b+c")                      -- was replace_text
    assert_eq(regex_find_all("a1b2", "[0-9]"), ["1", "2"])              -- was find_all
    assert_eq(regex_replace("a1b2", "[0-9]", "#"), "a#b#")              -- was replace_re
    assert_eq(fold_text("Continúa"), "continua")                        -- was fold
    assert_eq(identity(2), array([[1, 0], [0, 1]]))                     -- was eye
    -- regex_capture (was capture) is ALWAYS a list, or nothing
    assert_eq(regex_capture("2026-09", "([0-9]+)-([0-9]+)"), ["2026", "09"])   -- the groups
    assert_eq(regex_capture("hello world", "w[a-z]+"), ["world"])              -- no groups → [match]
    assert_eq(regex_capture("abc", "[0-9]+"), nothing)
    -- hmac (was hmac_sha256) returns BYTES; hmac_sha256 returned hex text
    let mac be hmac("data", "key")
    assert_eq(type_of(mac), "bytes")
    assert_eq(hex(mac), "0x" + decode(mac, "hex"))

test "maps and lists: get, items, merge, remove, insert, sort desc (v0.6.29+)"
    assert_eq(get({"a": 1}, "b", 0), 0)                     -- Python's d.get(k, default)
    assert_eq(get({"a": 1}, "b"), nothing)
    assert_eq(get([1, 2, 3], -1), 3)                        -- lists too; negative from the end
    assert_eq(items({"x": 1}), [{"key": "x", "value": 1}])
    assert_eq(merge({"a": 1, "b": 2}, {"b": 3, "c": 4}), {"a": 1, "b": 3, "c": 4})   -- rightmost wins
    assert_eq(remove({"a": 1, "b": 2}, "a"), {"b": 2})      -- a NEW map
    assert_eq(insert([1, 2, 3], 1, 9), [1, 9, 2, 3])
    assert_eq(sort([3, 1, 2], desc = true), [3, 2, 1])
    assert_eq(sort_by([{"n": 1}, {"n": 2}], (r) => r.n, desc = true), [{"n": 2}, {"n": 1}])

test "type predicates and exact integers (v0.6.29+)"
    assert(is_integer(1) and not is_integer(1.0))
    assert(is_text("a") and is_list([]) and is_map({}))
    assert_eq(int("ff", base = 16), 255)                    -- the base goes by name
    assert_eq(int("abc", fallback = 7), 7)                  -- so does the fallback
    assert_error(() => int("ff", 16))                       -- 2..36 in second place: ambiguous → error
    assert_eq(hex(7960), "0x1f18")
    assert_eq(int("0x1f18"), 7960)
    assert_eq(7 // 2, 3)                                    -- floor division
    assert_eq(-2 ** 2, -4)                                  -- ** binds tighter than unary minus

test "strict arity (v0.6.29+): extra or missing arguments are errors"
    task one(a)
        give a
    task two(a, b)
        give a
    assert_error(() => append([1], 2, 3))                   -- was: the 3 silently dropped
    assert_error(() => one(1, 2))
    assert_error(() => two(1))                              -- was: b arrived as nothing

test "text + nothing is an error; fmt is strict; a template hole takes any value"
    assert_eq("n=" + 1, "n=1")                              -- text + number still joins
    assert_error(() => "n=" + nothing)
    assert_error(() => "xs=" + [1, 2])
    assert_eq(`xs={[1, 2]} n={nothing}`, "xs=[1, 2] n=nothing")
    assert_error(() => fmt("{a} {b}", {"a": 1}))            -- a {name} without a value
    assert_eq(fmt("^[0-9]{3}-{n}$", {"n": 1}), "^[0-9]{3}-1$")   -- not a name → stays literal
    assert_eq(text(["1", 1]), "[\"1\", 1]")                 -- text inside a list is shown quoted

test "lists and maps are values (v0.6.29+): a task gives back what it changes"
    let xs be [1, 2]
    let ys be xs
    set ys[0] to 9
    assert_eq(xs, [1, 2])                                   -- only ys changed
    task mark(cfg)
        set cfg["seen"] to true
        give cfg
    let c be {"seen": false}
    mark(c)
    assert_eq(c["seen"], false)                             -- the task changed its own copy
    set c to mark(c)
    assert_eq(c["seen"], true)                              -- the change leaves with `give`

test "tables and statistics (v0.6.29+)"
    let rows be [{"region": "n", "amount": 1}, {"region": "s", "amount": 2}, {"region": "n", "amount": 3}]
    let groups be group_by(rows, "region")                  -- [{key, items}], first-appearance order
    assert_eq(groups[0].key, "n")
    assert_eq(length(groups[0].items), 2)
    assert_eq(summarize(rows, "region", {"total": sum_of("amount")}), [{"region": "n", "total": 4}, {"region": "s", "total": 2}])
    assert_eq(count_by(["x", "y", "x"]), [{"key": "x", "count": 2}, {"key": "y", "count": 1}])
    assert_eq(mean([1, nothing, 3]), 2.0)                   -- reductions skip nothing
    assert(std([1, 2, 3, 4]) > std([1, 2, 3, 4], ddof = 0))   -- sample by default, like pandas

test "dates, seeded randomness, Parquet and JSON Lines (v0.6.29+)"
    assert_eq(add_days(date("2026-09-24"), 7), date("2026-10-01"))
    assert_eq(type_of(datetime("2026-09-24T10:00:00Z")), "datetime")
    assert_eq(in_units(duration(hours = 1, minutes = 30), "minutes"), 90.0)
    assert_eq(rng(42)(), 0.7739560485559633)                -- numpy.random.default_rng(42).random()
    let rows be [{"a": 1, "day": date("2026-09-24")}]
    assert_eq(parquet_read(parquet_write(rows)), rows)
    assert_eq(jsonl_decode(jsonl_encode([{"a": 1}])), [{"a": 1}])

test "EVM names (v0.6.29+): pure helpers under the evm_/abi_ prefixes"
    assert_eq(abi_event_topic("Transfer(address,address,uint256)"), "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
    assert_eq(evm_create_address("0x0000000000000000000000000000000000000001", 0), "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459")

test "steps() counts executed statements — deterministic, no capability (engine v0.6.20+)"
    let before be steps()
    each i in range(100)
        let x be i
    assert(steps() - before > 100)

test "program & subprocess: args() and self_path() (no capability)"
    assert_eq(type_of(args()), "list")           -- the program's argv (empty under `synsema test`)
    assert_eq(type_of(self_path()), "text")       -- this executable's path
    assert(length(self_path()) > 0)

Renamed in v0.6.29§

v0.6.29 fixed the names once, before v1.0. The old name is a deprecated alias: it still runs, a program that uses one prints a single warning on stderr when it loads (also for a module loaded with use, v0.6.30+), synsema check flags every use, and it goes away in v1.0. Write the new name — and read the "meaning" column, because three of them do not return the same thing:

OldNewMeaning
replace_textreplacesame
find_allregex_find_allsame
replace_reregex_replacesame
captureregex_capturechanged: always a list — the groups, or [match] when the pattern has none — or nothing. The old capture returned the bare match text without groups, and still does
foldfold_textsame (lowercase + strip accents; not Python's casefold)
eyeidentitysame (numeric arrays)
hmac_sha256(data, key) → hex texthmac(data, key, algo?) → byteschanged: hex(mac) shows it as "0x…", decode(mac, "hex") gives the old bare hex
eth_rpc, eth_nonce, eth_balance, eth_gas_price, eth_chain_id, eth_estimate_gas, eth_call, eth_fee_historyevm_rpc, evm_nonce, evm_balance, evm_gas_price, evm_chain_id, evm_estimate_gas, evm_call, evm_fee_historysame. The JSON-RPC method names you pass to evm_rpc stay "eth_call", "eth_getLogs", …
eth_send_raw, eth_receipt, eth_wait_receipt, eth_addressevm_send, evm_receipt, evm_wait, evm_addresssame
tx_eip1559, tx_eip1559_rawevm_tx, evm_tx_rawsame; contract creation is the new evm_tx_create, and evm_tx without to is an error that points there
solana_message (builds)solana_tx(params)the name solana_tx changed meaning: it now BUILDS the message
solana_tx(message, signatures) (assembles)solana_tx_raw(message, signatures)the old two-argument solana_tx call is an error that points to solana_tx_raw
solana_confirmsolana_waitsame
algorand_tx_encode (bytes to sign)algorand_tx(txn)the name algorand_tx changed meaning: it now returns the bytes to sign
algorand_tx(txn, sig) (signed)algorand_tx_raw(txn, sig)the old two-argument algorand_tx call is an error that points to algorand_tx_raw
algo_addressalgorand_addresssame

Unchanged on purpose: matches, find_first, read_file(path, line, limit). Blockchain follows <family>_<action> (Bitcoin already did: btc_tx/btc_tx_raw/btc_send/btc_wait).

Rules every builtin follows (v0.6.29+)§

arguments, got 3); before, the extras were dropped in silence (trim(s, "x") ignored the "x"). A task or lambda you call with a missing argument (task 'f' is missing argument 'b') or an extra one (task 'f' takes 1 argument, got 3) is an error too. Builtins take arguments by position, except the documented named forms (sort/sort_by desc = true, int base = / fallback =, std/var ddof =, recall(from = …), random_normal(g, mean =, std =), duration(hours = …), …). Callbacks that a builtin or the host calls (apply, where, reduce`, route handlers, cron) still receive what they get.

insert, merge, remove, sort, reverse return a new one (and set xs to append(xs, v) is O(1), it writes in place when nobody else holds the list). A task that sets inside a map it received changes its own copy — give it back. Shared state stays explicit: blackboard, memory, bus, state_* — see Multi-agent and Memory & state.

purpose); text + number/bool still joins. A template hole takes any value: xs={xs} n={nothing} → xs=[1, 2] n=nothing. join(xs, sep)` refuses the same items.

brace); braces that do not surround a name — JSON, CSS, regex quantifiers such as {3} — stay literal.

print("x") is unchanged.

By category§