Synsemadocsv0.6.xENES

Primeros pasos

Contra tus instintos

Ningún LLM fue entrenado en Synsema, así que un modelo que escribe Synsema recae en hábitos de otros lenguajes — y de versiones viejas de Synsema. Estas son las diferencias que muerden. Cada afirmación acá está verificada contra el engine (las de abajo son un doctest que pasa):

¿Venís de Python en particular? Empezá por Python → Synsema — la tabla de traducción: mapea los reflejos de Python (def/return, f-strings, comprehensions, try/except, d.get) a las formas Synsema en un solo lugar. Esta página junta el resto de los gotchas verificados.
priors.syn
-- Doc example: places where Synsema differs from what a fresh LLM assumes.
-- Every claim below is a passing assertion against the engine (doctested).
intent: "doc example: counter-your-priors gotchas"
require secret("API_KEY")

print("1 / 2 = " + text(1 / 2) + "   (division is always float)")

-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
    try
        f()
    recover err
        give err
    give nothing

task double(n)
    give n * 2

task add(a, b)
    give a + b

task set_flag(cfg)
    set cfg["on"] to true
    give cfg

test "division is ALWAYS float (1/2 is 0.5, not 0); // is floor division"
    assert_eq(1 / 2, 0.5)
    assert_eq(type_of(1 / 2), "number")
    assert_eq(7 // 2, 3)
    assert_eq(-7 // 2, -4)

test "lists and maps are values: give a changed map back, the caller's copy never moves"
    let cfg be {"on": false}
    let out be set_flag(cfg)
    assert_eq(cfg, {"on": false})
    assert_eq(out, {"on": true})
    set cfg to set_flag(cfg)
    assert_eq(cfg, {"on": true})

test "calls check their arity: nothing is dropped or filled with nothing"
    assert(contains(error_of(() => add(1)), "missing argument 'b'"))
    assert(contains(error_of(() => add(1, 2, 3)), "takes 2 arguments, got 3"))
    assert(contains(error_of(() => trim(" a ", "x")), "takes at most 1 argument"))

test "-2 ** 2 is -4; |> has the lowest precedence and feeds the FIRST argument"
    assert_eq(-2 ** 2, -4)
    assert_eq(1 + 2 |> double, 6)
    assert_eq([3, 1, 2] |> sort_by((v) => v), [1, 2, 3])

test "text + nothing is an error; a backtick hole takes any value"
    assert(contains(error_of(() => "id=" + nothing), "Cannot add text and nothing"))
    assert_eq("n=" + 1, "n=1")
    assert_eq(`id={nothing}`, "id=nothing")

test "fmt fails on a {name} it has no value for"
    assert(contains(error_of(() => fmt("Hi {name}", {})), "no value for {name}"))
    assert_eq(fmt("^[0-9]{3}-{n}$", {"n": 1}), "^[0-9]{3}-1$")

test "int() takes the base BY NAME; a bare 16 in second place is an error"
    assert_eq(int("ff", base = 16), 255)
    assert(contains(error_of(() => int("ff", 16)), "int(x, base = 16)"))
    assert_eq(int("x", fallback = 0), 0)

test "1e18 is a float; an exact big integer is 10 ** 18"
    assert_eq(is_integer(1e18), false)
    assert(is_integer(10 ** 18))

test "each over a map walks its keys; stop leaves a loop; Python-style indexes and comparisons"
    let seen be []
    each k in {"a": 1, "b": 2, "c": 3}
        when k == "c"
            stop
        set seen to append(seen, k)
    assert_eq(seen, ["a", "b"])
    assert_eq([1, 2, 3][-1], 3)
    assert_eq("abc"[0], "a")
    assert(1 < 5 <= 10)
    assert_eq(text(["1", 1]), "[\"1\", 1]")

test "the renamed builtins (the old names are deprecated)"
    assert_eq(replace("a-b", "-", "+"), "a+b")
    assert_eq(regex_find_all("a1b2", "[0-9]"), ["1", "2"])
    assert_eq(regex_capture("a1", "[0-9]"), ["1"])
    assert_eq(regex_replace("a1", "[0-9]", "#"), "a#")
    assert_eq(fold_text("Ñandú"), "nandu")
    assert_eq(type_of(hmac("msg", "key")), "bytes")
    assert(starts_with(hex(hmac("msg", "key", "sha512")), "0x"))
    assert_eq(size(identity(2)), 4)

test "a map key test is `in` (or contains); there is no has()"
    let m be {"a": 1, "b": 2}
    assert("a" in m)
    assert("z" not in m)
    assert(contains(m, "a"))
    assert_eq(get(m, "z", 0), 0)

test "and/or give a bool, never the operand"
    assert_eq(nothing or "default", true)

test "the intentional family takes (fn, list) or (list, fn); index_of gives nothing"
    let big be (x) => x > 1
    assert_eq(where(big, [1, 2, 3]), [2, 3])
    assert_eq(where([1, 2, 3], big), [2, 3])
    assert_eq(find_first(big, [1, 2, 3]), 2)
    assert_eq(count_where([1, 2, 3], big), 2)
    assert_eq(zip_with([1, 2], [3, 4], (a, b) => a + b), [4, 6])
    assert(contains(error_of(() => apply(big, big)), "got two tasks"))
    assert_eq(index_of([1, 2], 9), nothing)

test "concatenating text + secret redacts the WHOLE string (prefix absorbed)"
    let k be secret("API_KEY", "sk-real-123")
    assert_eq(type_of("Bearer " + k), "secret")
    assert_eq(text("Bearer " + k), "secret(API_KEY)")

test "property access uses `of` (or dot); a of b.c reads as a of (b.c)"
    let cfg be {"host": "localhost", "port": 8080, "db": {"name": "main"}}
    assert_eq(host of cfg, "localhost")
    assert_eq(cfg.port, 8080)
    assert_eq(name of cfg.db, "main")
    assert_eq({"type": 1, "to": 2}.to, 2)

test "random() needs `require random`; a seeded rng(seed) is pure"
    assert(contains(error_of(() => random()), "require random"))
    assert_eq(random_int(rng(42), 1, 6), 1)

test "memory must be declared"
    assert(contains(error_of(() => remember("rule", "x")), "require memory"))

Las reglas del lenguaje que cambiaron (lo que el código y los modelos viejos hacen mal)§

task, da una copia lógica (copy-on-write: no cuesta nada hasta que alguien escribe). Una task que hace set adentro de un map que recibió no cambia el del llamador: devolvelo con give y set cfg to set_flag(cfg). Dentro de each r in rows, set r["x"] to 1 cambia la copia del loop — escribí set rows[i]["x"] to 1 o reconstruí la lista con apply + merge. El estado compartido es siempre explícito: el blackboard, memory, el bus, state_*, o una variable exportada de un módulo.

sobra (task 'add' takes 2 arguments, got 3, trim() takes at most 1 argument, got 2) es error. Los callbacks que llama un builtin o el host (apply, where, handlers de ruta, cron) siguen recibiendo lo que reciben.

menor precedencia: 1 + 2 |> double es double(3). Un paso que es una llamada recibe el valor como primer** argumento: xs |> sort_by(f) es sort_by(xs, f).

// nota estilo C falla con la pista `comments in Synsema start with -- `.

convert it on purpose: text(x), or interpolate it. Texto + número o bool sigue uniendo; un hueco de backticks acepta cualquier valor ( id={nothing} → id=nothing`).

un regex, CSS, JSON) pasan tal cual.

un número entero 2..36 ahí (también 2.0) es ambiguo. Escribí int(s, base = 16) o int(s, fallback = 0).

tienen límite de tamaño.

(xs[-1]), los índices de texto (s[0]) y las comparaciones encadenadas (1 < x <= 10).

salida en vivo. Dentro de una lista o map, el texto sale entre comillas: print(["1", 1]) → ["1", 1].

Builtins renombrados — escribí el nombre nuevo§

Los nombres viejos siguen corriendo como alias deprecados hasta v1.0: un programa que usa uno imprime un aviso en stderr, y synsema check marca cada uso. Tres además cambiaron de significado:

ViejoNuevoNota
replace_textreplacemisma forma
find_allregex_find_allmisma forma
replace_reregex_replacemisma forma
captureregex_capture⚠️ siempre una lista — los grupos, o [match] sin grupos — o nothing
foldfold_textminúsculas + sin acentos
eyeidentityarrays numéricos
hmac_sha256(data, key) → texto hexhmac(data, key, algo?) → bytes⚠️ hex(mac) para mostrarlo
eth_*, tx_eip1559, tx_eip1559_rawevm_*, evm_tx, evm_tx_rawlos nombres de método JSON-RPC que le pasás a evm_rpc siguen siendo "eth_call"… — Blockchain
solana_message / solana_tx(message, signatures)solana_tx(params) construye / solana_tx_raw(message, signatures) ensambla⚠️ el solana_tx viejo de dos argumentos es un error que apunta a solana_tx_raw
algorand_tx_encode / algorand_tx(txn, sig)algorand_tx(txn) construye / algorand_tx_raw(txn, sig) ensambla⚠️ el algorand_tx viejo de dos argumentos es un error que apunta a algorand_tx_raw
solana_confirm, algo_addresssolana_wait, algorand_address

Más gotchas (verificados)§

la forma de función pasa check pero falla en runtime (Undefined variable: 'log').

auto-otorga (random es para tokens/nonces, así que es deny-by-default). Un generador con semilla, rng(42), es puro y no necesita nada: random_int(rng(42), 1, 6) da los números de numpy.

transform/reduce/sort_by/group_by/find_first/every/some/count_where/ zip_with) acepta ambos órdenes (fn, lista, …) y (lista, fn, …); dos tasks o dos listas donde se esperaba una-y-una es error explícito, jamás una adivinanza. index_of devuelve nothing si no está (no -1 — chequeá when idx != nothing).

un default, get(m, "k", "default") o un when.

a of b, después x.c. Después de un . cualquier palabra es un nombre de miembro (tx.to, ev.type). Y request/query/params` existen solo dentro de los route handlers — pasalos a las tasks auxiliares como parámetros.

Capability not granted: memory salvo que el programa tenga require memory("<name>") al tope (no se auto-otorga, ni bajo run: escribe archivos). El nombre declarado — no el del archivo — keyea el .db; ver Memoria y estado.

resultados (pasá un 5º argumento para subir el límite). Dentro de un agent, recall() lee solo las entradas propias de ese agente por defecto — cruzá namespaces con recall(from = "other") o from = "*".

analyze X for "...", generate "..." given X. Un reason "literal" pelado también vale.

detectar el sufijo, leé path of request — el param ya viene sin él.

.gitignore: file.read("./data/") son los archivos que están directamente en ./data; "esta carpeta y todo lo de abajo" es ./data/. file.read("") no es el disco entero — eso es require file.read sin scope.

require file.read("./static/**") (el server no arranca sin eso), y cada { include }/{ layout } desde disco necesita file.read como render() (engine v0.6.45+).

json_for_script y csv_encode de un secreto son un error (antes salía "[redacted]"), y también un mapa con un secreto adentro en un hueco de template. Una clave que la API quiere en la URL, la query o el body no necesita reveal(): ligala al host, require secret("K") for net("api.example.com"), y ponela ahí.

nunca a loopback, a redes privadas ni al endpoint de metadata de la nube, resuelva a lo que resuelva el nombre. Una base de datos o un servicio de la red privada se nombra: require net("db:5432"), su dirección, un CIDR, o require net(env("CONTROL_URL")).

--cap-set/--sandbox la deja quitar sólo los principales que nombra (declassify=customers); require declassify("P") en el programa la declara pero no la acota. Y sólo se llama por su nombre — nada de alias ni de pasarla como valor.

llm=P@<proveedor> del host, un dato privado puede llegar a un modelo, pero lo que vuelve (todo el mapa de llm_step, name incluido) también es privado — desclasificá la parte sobre la que actuás.

lo chequean call/call_tool (de forma estricta, sin convertir) y no una llamada normal.

GET /:lang/:version, un static "/assets" nunca se alcanza — serví los assets con una ruta declarada GET /assets/*path.

salir del proyecto** — el directorio del archivo de entrada (../x.syn desde la entrada se deniega; un módulo en un subdirectorio puede hacer use "../core/x.syn" dentro del proyecto).

Si un modelo insiste en generar mal alguno de estos, copiá esta página a su contexto — es exactamente el instinto que hay que corregir.