Synsemadocsv0.6.xENES

Referencia

Índice de builtins

Un mapa categorizado de los builtins. Las firmas completas viven en cada página de tema; los centrales están doctested acá para que esta referencia no driftee.

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)

Renombrados en v0.6.29§

v0.6.29 fijó los nombres una sola vez, antes de v1.0. El nombre viejo es un alias deprecado: sigue corriendo, un programa que lo usa imprime un único aviso en stderr al cargar (también para un módulo cargado con use, v0.6.30+), synsema check marca cada uso, y se va en v1.0. Escribí el nombre nuevo — y leé la columna "significado", porque tres de ellos no devuelven lo mismo:

ViejoNuevoSignificado
replace_textreplaceigual
find_allregex_find_alligual
replace_reregex_replaceigual
captureregex_capturecambió: siempre una lista — los grupos, o [match] cuando el patrón no tiene — o nothing. El capture viejo devolvía el texto del match pelado sin grupos, y lo sigue haciendo
foldfold_textigual (minúsculas + sin acentos; no es el casefold de Python)
eyeidentityigual (arrays numéricos)
hmac_sha256(data, key) → texto hexhmac(data, key, algo?) → bytescambió: hex(mac) lo muestra como "0x…", decode(mac, "hex") da el hex pelado de antes
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_historyigual. Los nombres de método JSON-RPC que le pasás a evm_rpc siguen siendo "eth_call", "eth_getLogs", …
eth_send_raw, eth_receipt, eth_wait_receipt, eth_addressevm_send, evm_receipt, evm_wait, evm_addressigual
tx_eip1559, tx_eip1559_rawevm_tx, evm_tx_rawigual; crear un contrato es el nuevo evm_tx_create, y evm_tx sin to es un error que apunta ahí
solana_message (construye)solana_tx(params)el nombre solana_tx cambió de significado: ahora CONSTRUYE el mensaje
solana_tx(message, signatures) (ensambla)solana_tx_raw(message, signatures)la llamada vieja de dos argumentos a solana_tx es un error que apunta a solana_tx_raw
solana_confirmsolana_waitigual
algorand_tx_encode (bytes a firmar)algorand_tx(txn)el nombre algorand_tx cambió de significado: ahora devuelve los bytes a firmar
algorand_tx(txn, sig) (firmada)algorand_tx_raw(txn, sig)la llamada vieja de dos argumentos a algorand_tx es un error que apunta a algorand_tx_raw
algo_addressalgorand_addressigual

Sin cambios a propósito: matches, find_first, read_file(path, line, limit). Blockchain sigue <familia>_<acción> (Bitcoin ya lo hacía: btc_tx/btc_tx_raw/btc_send/btc_wait).

Reglas que cumple todo builtin (v0.6.29+)§

arguments, got 3); antes, los sobrantes se descartaban en silencio (trim(s, "x") ignoraba el "x"). Una task o lambda que llamás con un argumento de menos (task 'f' is missing argument 'b') o de más (task 'f' takes 1 argument, got 3) también es un error. Los builtins toman los argumentos por posición, salvo las formas con nombre documentadas (sort/sort_by desc = true, int base = / fallback =, std/var ddof =, recall(from = …), random_normal(g, mean =, std =), duration(hours = …), …). Los callbacks que llama un builtin o el host (apply, where, reduce`, handlers de rutas, cron) siguen recibiendo lo que reciben.

append, insert, merge, remove, sort, reverse devuelven uno nuevo (y set xs to append(xs, v) es O(1): escribe en el lugar cuando nadie más tiene la lista). Una task que hace set dentro de un map que recibió cambia su propia copia — devolvelo con give. El estado compartido sigue siendo explícito: blackboard, memoria, bus, state_* — ver Multi-agente y Memoria y estado.

on purpose); texto + número/bool sigue uniendo. Un hueco de template toma cualquier valor: xs={xs} n={nothing} → xs=[1, 2] n=nothing. join(xs, sep)` rechaza los mismos ítems.

literal); las llaves que no rodean un nombre — JSON, CSS, cuantificadores de regex como {3} — quedan literales.

["1", 1]; un print("x") de primer nivel no cambia.

Por categoría§