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.
-- 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)§
- Las listas y los maps son valores.
let ys be xs, o pasarle una lista o un map a una
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.
- La aridad es estricta. Un argumento que falta (
task 'add' is missing argument 'b') o que
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.
-22 es-4(la potencia liga más fuerte que el menos unario), y|>tiene la
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).
//es división entera, no un comentario. Los comentarios empiezan con--. Un
// nota estilo C falla con la pista `comments in Synsema start with -- `.
- Texto +
nothing(o una lista, map o bytes) es error: `Cannot add text and nothing —
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`).
fmtes estricto:fmt: no value for {name}. Las llaves que no son un nombre ({3}en
un regex, CSS, JSON) pasan tal cual.
int("ff", 16)es error: el segundo argumento posicional deintes el fallback, así que
un número entero 2..36 ahí (también 2.0) es ambiguo. Escribí int(s, base = 16) o int(s, fallback = 0).
1e18es un float, como en Python. Un entero grande exacto es10 ** 18; los enteros no
tienen límite de tamaño.
- No hay
continue— envolvé el resto del cuerpo del loop enwhen.stopsale del loop. eachsobre un map recorre sus claves; existenin/not in, los índices negativos
(xs[-1]), los índices de texto (s[0]) y las comparaciones encadenadas (1 < x <= 10).
printse escribe en el momento bajosynsema run— no hace faltaflush()para ver la
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:
| Viejo | Nuevo | Nota |
|---|---|---|
replace_text | replace | misma forma |
find_all | regex_find_all | misma forma |
replace_re | regex_replace | misma forma |
capture | regex_capture | ⚠️ siempre una lista — los grupos, o [match] sin grupos — o nothing |
fold | fold_text | minúsculas + sin acentos |
eye | identity | arrays numéricos |
hmac_sha256(data, key) → texto hex | hmac(data, key, algo?) → bytes | ⚠️ hex(mac) para mostrarlo |
eth_*, tx_eip1559, tx_eip1559_raw | evm_*, evm_tx, evm_tx_raw | los 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_address | solana_wait, algorand_address |
Más gotchas (verificados)§
loges una sentencia, no una función. Escribílog "mensaje", nolog("mensaje")—
la forma de función pasa check pero falla en runtime (Undefined variable: 'log').
random()/random_int()necesitanrequire randomincluso bajorun— no se
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.
- El orden de
applyno es una trampa — la familia intencional (apply/where/
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).
and/ordevuelven un bool, nunca el operando:nothing or "default"estrue. Para
un default, get(m, "k", "default") o un when.
a of b.cse lee comoa of (b.c)— para(a of b).c, bindeá primero: `let x be
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.
- La memoria del agente se DECLARA —
remember/recall/reglas/progreso fallan con
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.
recall(...)es más-nuevo-primero —[0]es la entrada más reciente — y trunca a 200
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 = "*".
- Las operaciones LLM llevan un sujeto:
reason about X,decide between [...] given X,
analyze X for "...", generate "..." given X. Un reason "literal" pelado también vale.
- Una ruta
:paramse come un sufijo.md/.json(es negociación de contenido). Para
detectar el sufijo, leé path of request — el param ya viene sin él.
- *Un
en un scope de archivo se queda dentro de una carpeta** (engine v0.6.45+), como en
.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.
- Servir o incluir un archivo es leerlo.
static "/assets" from "./static"necesita
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+).
- Un secreto nunca se escribe como texto (motor v0.6.46+):
json_encode,jsonl_encode,
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í.
- *
net("") no es "todo"** (motor v0.6.46+): un comodín llega sólo a direcciones públicas —
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")).
declassifyno es gratis bajo un techo (motor v0.6.46+, con etiquetas): un host con
--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.
- La respuesta a un prompt privado es privada (motor v0.6.46+): con
require llm("P")y el
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.
- Los parámetros tipados se chequean sólo donde llegan datos (motor v0.6.46+):
task f(a: number)
lo chequean call/call_tool (de forma estricta, sin convertir) y no una llamada normal.
- Las rutas declaradas siempre ganan sobre los
staticmounts. Con rutas comodín como
GET /:lang/:version, un static "/assets" nunca se alcanza — serví los assets con una ruta declarada GET /assets/*path.
- El lexer rechaza el BOM UTF-8 al principio de un
.syn, y unuse "..."**no puede
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.