Primeros pasos
Python → Synsema — la tabla de traducción
Vos (un LLM, o un dev de Python) ya sabés Python. Esta página mapea el reflejo de Python a la forma Synsema y marca exactamente dónde diverge la semántica. Es más rápido que aprender de cero, y previene el modo de falla clásico: escribir Python con keywords de Synsema.
La regla que previene la mayoría de las alucinaciones: si no lo viste en estas docs, no existe. No hay import, no hay stdlib de Python, no hay clases, no hay comprehensions, no hay decoradores, no hay with, no hay generadores, no hay sintaxis de método sobre valores (xs.append(x) → los builtins son tasks planas: append(xs, x)).
Cuando igual se escapa un reflejo de Python, el engine contesta en términos de Python: el error nombra la forma Synsema que hay que escribir (return → give, len → length, d.get → get(m, key, default), x = 1 → let/set). Los mensajes citados abajo son los del engine. Desde el motor v0.6.45 synsema check también atrapa, antes de que corra la línea, una llamada a un nombre que no existe en ningún lado: len(xs) es el aviso `there is no task, variable or builtin named 'len' in this file; it fails when that line runs — did you mean length(x)?` (un aviso: el código de salida no cambia).
Cada afirmación de runtime de abajo está asegurada por este doctest que pasa. Las pistas de parseo (una keyword u operador de Python que impide cargar el programa) no pueden correr dentro de un test, así que están citadas de synsema check:
-- Doc example: the Python → Synsema divergences that bite the hardest.
-- Every runtime claim in the translation-table page is asserted here (doctested).
-- Parse-time hints (`x = 5`, `return v`, `elif`, `continue`, f"…"…) stop a program from
-- loading, so they cannot live in a test block: the page quotes them from `synsema check`.
intent: "doc example: python-to-synsema translation table"
require env("DOCS_UNSET_VAR")
-- 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 boom()
raise("kaput")
task reraise_error()
try
boom()
recover err
raise(err)
task add(a, b)
give a + b
-- With value semantics a task gets its own copy: the caller sees a change only if it is given back.
task mark(m)
set m["seen"] to true
give m
test "assignment is let/set, not = (x = 5 is a parse error)"
let x be 1
set x to 2
assert_eq(x, 2)
test "None/True/len/str are undefined names, and the error names the Synsema form"
assert(contains(error_of(() => None), "in Synsema: nothing"))
assert(contains(error_of(() => True), "in Synsema: true"))
assert(contains(error_of(() => len([1])), "in Synsema: length(x)"))
assert(contains(error_of(() => str(1)), "in Synsema: text(x)"))
assert(contains(error_of(() => dropna([])), "drop_missing(rows)"))
assert_eq(nothing == nothing, true)
assert(is_missing(nothing))
test "no methods: xs.append / d.get fail with the builtin to use"
let xs be [1]
let d be {"a": 1}
assert(contains(error_of(() => xs.append(2)), "set xs to append(xs, item)"))
assert(contains(error_of(() => d.get("a")), "get(m, key, default)"))
test "try/recover (not try/except); recover SWALLOWS unless you raise(err)"
let seen be ""
try
boom()
recover err
set seen to err
assert(contains(seen, "kaput"))
assert_error(reraise_error)
test "and/or short-circuit and always yield a bool"
let m be {"a": 1}
assert_eq(contains(m, "b") and m["b"] == 1, false)
assert_eq(contains(m, "a") and m["a"] == 1, true)
assert_eq(contains(m, "a") or m["zz"] == 1, true)
assert_eq(1 or 0, true)
test "truthiness: nothing, false, 0, empty text/list/map are falsy"
each v in [nothing, false, 0, "", [], {}]
assert_eq(when v then "t" otherwise "f", "f")
each v in [1, "a", [0], {"k": 0}]
assert_eq(when v then "t" otherwise "f", "t")
test "append/insert return a NEW list; reassign with set"
let xs be [1, 2]
let ys be append(xs, 3)
assert_eq(xs, [1, 2])
assert_eq(ys, [1, 2, 3])
assert_eq(insert([1, 2, 3], 1, 9), [1, 9, 2, 3])
assert_eq(insert([1], -1, 0), [0, 1])
assert(contains(error_of(() => insert([1], 5, 0)), "out of range"))
test "lists and maps are values: a copy never changes behind your back"
let xs be [1, 2]
let ys be xs
set ys[0] to 9
assert_eq(xs, [1, 2])
let orig be {"a": 1}
let changed be mark(orig)
assert_eq(orig, {"a": 1})
assert_eq(changed, {"a": 1, "seen": true})
let rows be [{"p": 1}, {"p": 2}]
each r in rows
set r["p"] to 0
assert_eq(rows, [{"p": 1}, {"p": 2}])
set rows[0]["p"] to 5
assert_eq(rows, [{"p": 5}, {"p": 2}])
let nested be {"a": {}}
set nested["a"]["b"] to 1
assert_eq(nested, {"a": {"b": 1}})
assert_eq(apply((r) => merge(r, {"x": 1}), [{"p": 1}]), [{"p": 1, "x": 1}])
test "strict arity: a missing or an extra argument is an error"
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(() => append([1], 2, 3)), "takes at most 2 arguments"))
assert(contains(error_of(() => round(2.567, 2)), "round_to(x, n)"))
assert_eq(round_to(2.567, 2), 2.57)
test "f-string equivalent is the backtick string; quoted strings stay literal"
let n be 1
assert_eq(`n={n + 1}`, "n=2")
assert_eq(`xs={[1, "a"]} v={nothing}`, "xs=[1, \"a\"] v=nothing")
assert(contains("n={1+1}", "{"))
assert(contains(error_of(() => "%d" % 5), "there is no %-formatting"))
test "fmt is strict like str.format: a {name} without a value is an error"
assert_eq(fmt("{a}-{b}", {"a": 1, "b": 2}), "1-2")
assert_eq(fmt("{{a}}", {}), "{a}")
assert(contains(error_of(() => fmt("{a}", {})), "no value for {a}"))
test "\u escapes like Python and JSON; \x is not an escape"
assert_eq("\u00e9", "é")
assert_eq("\u{1F600}", "😀")
assert_eq(length("\uD83D\uDE00"), 1)
assert_eq(length("\\u00e9"), 6)
assert_eq(length("C:\users"), 8)
assert_eq(length("C:\build\x64"), 12)
test "each over a map walks its keys; items gives key and value"
let ks be []
each k in {"x": 1, "y": 2}
set ks to append(ks, k)
assert_eq(ks, ["x", "y"])
assert_eq(items({"x": 1}), [{"key": "x", "value": 1}])
assert_eq(keys({"x": 1}), ["x"])
assert_eq(values({"x": 1}), [1])
let cs be []
each c in "ab"
set cs to append(cs, c)
assert_eq(cs, ["a", "b"])
let bs be []
each b in bytes([1, 255])
set bs to append(bs, b)
assert_eq(bs, [1, 255])
test "comprehension equivalent: apply + where; slicing is slice()"
assert_eq(apply((x) => x * 2, where([1, 2, 3], (x) => x > 1)), [4, 6])
assert_eq(apply([1, 2], (x) => x + 1), [2, 3])
assert_eq(slice([10, 20, 30, 40], 1, 3), [20, 30])
assert_eq(slice([10, 20, 30, 40], -2), [30, 40])
assert_eq(reverse("abc"), "cba")
assert_eq(split("ab", ""), ["a", "b"])
test "negative and text indexes work like Python; an index must be an integer"
assert_eq([1, 2, 3][-1], 3)
assert_eq("abc"[0], "a")
assert_eq("abc"[-1], "c")
assert_eq([1, 2][1.0], 2)
assert(contains(error_of(() => [1, 2][1.7]), "index must be an integer"))
test "`in` / `not in`: list item, map KEY, substring; text needs text"
assert(2 in [1, 2])
assert(3 not in [1, 2])
assert("a" in {"a": 1})
assert("b" in "abc")
assert(bytes([2, 3]) in bytes([1, 2, 3]))
assert(contains([1, 2], 2))
assert(contains(error_of(() => 1 in "a1"), "text(x) in s"))
test "chained comparisons like Python"
let x be 5
assert(1 < x <= 10)
assert_eq(1 < x < 3, false)
test "text + number CONCATENATES; text + nothing/list/map is an error; no text * n"
assert_eq("a" + 1, "a1")
assert_eq("a" + true, "atrue")
assert(contains(error_of(() => "a" + nothing), "Cannot add text and nothing"))
assert(contains(error_of(() => "a" + [1]), "Cannot add text and list"))
assert_error(() => "ab" * 2)
assert_error(() => 1 + true)
test "inside a list or map, text is shown quoted"
assert_eq(text(["1", 1]), "[\"1\", 1]")
assert_eq(text("x"), "x")
test "len/str/sorted are length/text/sort; d.get(k, default) is get(m, k, default)"
assert_eq(length("abc"), 3)
assert_eq(text(42), "42")
assert_eq(sort([3, 1, 2]), [1, 2, 3])
assert_eq(sort([3, 1, 2], desc = true), [3, 2, 1])
assert_eq(sort([3, nothing, 1]), [1, 3, nothing])
assert(contains(error_of(() => sort([1, "a"])), "cannot order number and text"))
assert_eq(sort_by([{"n": 1}, {"n": 3}], (r) => r.n, desc = true), [{"n": 3}, {"n": 1}])
assert_eq(get({"a": 1}, "a"), 1)
assert_eq(get({"a": 1}, "b"), nothing)
assert_eq(get({"a": 1}, "b", 0), 0)
assert_eq(get([1, 2, 3], -1), 3)
assert_eq(remove({"a": 1, "b": 2}, "a"), {"b": 2})
assert_eq(merge({"a": 1, "b": 2}, {"b": 3, "c": 4}), {"a": 1, "b": 3, "c": 4})
assert(contains(error_of(() => {"a": 1}["b"]), "Map has no key 'b'"))
test "sum/min/max, variadic max, reduce, index_of gives nothing when absent"
assert_eq(sum([1, 2, 3]), 6)
assert_eq(min([3, 1]), 1)
assert_eq(max(1, 5, 2), 5)
assert_eq(max([1, nothing, 3]), 3)
assert_eq(max(1, 2,), 2)
assert_eq(reduce([1, 2, 3], (a, b) => a + b, 0), 6)
assert_eq(index_of([1, 2], 9), nothing)
assert_eq(index_of("hello", "l"), 2)
assert_eq(range(0, 10, 3), [0, 3, 6, 9])
assert(contains(error_of(() => range(0, 2.5)), "must be an integer"))
test "starts_with / replace / regex: regex_capture is always a list"
assert(starts_with("hello", "he"))
assert_eq(replace("a-b", "-", "+"), "a+b")
assert(matches("abc", "[a-c]+"))
assert_eq(matches("abcd1", "[a-c]+"), false)
assert_eq(regex_find_all("a1b2", "[0-9]"), ["1", "2"])
assert_eq(regex_capture("a1", "([a-z])([0-9])"), ["a", "1"])
assert_eq(regex_capture("a1", "[0-9]"), ["1"])
assert_eq(regex_capture("ab", "[0-9]"), nothing)
assert_eq(regex_replace("a1b2", "[0-9]", "#"), "a#b#")
test "int() is exact; base and fallback go by name"
assert_eq(int("-42"), -42)
assert_eq(int("1_000"), 1000)
assert_eq(int("0x1f"), 31)
assert_eq(int(3.0), 3)
assert_eq(int("123456789012345678901"), 123456789012345678901)
assert_eq(int("ff", base = 16), 255)
assert_eq(int("abc", fallback = 0), 0)
assert(contains(error_of(() => int("ff", 16)), "int(x, base = 16)"))
assert(contains(error_of(() => int("ff", 2.0)), "int(x, base = 2)"))
assert(contains(error_of(() => int(1.5)), "not a whole number"))
assert_eq(number("42"), 42.0)
assert_eq(is_integer(number("42")), false)
test "hex, 0x/0o/0b literals, is_integer/is_text/is_list/is_map"
assert_eq(hex(7960), "0x1f18")
assert_eq(hex(bytes([0, 255])), "0x00ff")
assert_eq(0x1f18, 7960)
assert_eq(0o17, 15)
assert_eq(0b101, 5)
assert(is_integer(1))
assert_eq(is_integer(1.0), false)
assert(is_text("a") and is_list([]) and is_map({}))
assert_eq(type_of(1), "number")
test "// is floor division like Python, exact for big integers; / is always float"
assert_eq(7 // 2, 3)
assert_eq(-7 // 2, -4)
assert_eq(-7 % 3, 2)
assert_eq((10 ** 30 + 7) // 2, 500000000000000000000000000003)
assert_eq(7 // 0.1, 69.0)
assert_eq(10 / 2, 5.0)
assert_eq(1 / 2, 0.5)
test "-2 ** 2 is -4 and 1e18 is a float, as in Python; big integers compare exactly"
assert_eq(-2 ** 2, -4)
assert_eq((-2) ** 2, 4)
assert_eq(2 ** 3 ** 2, 512)
assert_eq(is_integer(1e18), false)
assert_eq(10 ** 18, 1000000000000000000)
assert_eq(2 ** 53 + 1 == 9007199254740992.0, false)
assert_eq(json_decode("12345678901234567890"), 12345678901234567890)
test "round is banker's rounding; floor gives an integer"
assert_eq(round(2.5), 2)
assert_eq(round(3.5), 4)
assert_eq(floor(3.7), 3)
assert(is_integer(floor(3.7)))
test "hmac gives bytes; hex() shows them"
let mac be hmac("msg", "key")
assert_eq(type_of(mac), "bytes")
assert(starts_with(hex(mac), "0x"))
test "enumerate gives {index, item} maps (Python's for i, x in enumerate)"
let e be enumerate(["a", "b"])
assert_eq(e[0].index, 0)
assert_eq(e[1].item, "b")
assert_eq(length(enumerate([])), 0)
test "pandas reflexes: a table is a list of maps"
let rows be [{"r": "n", "m": 1}, {"r": "s", "m": 2}, {"r": "n", "m": nothing}]
assert_eq(summarize(rows, "r", {"total": sum_of("m"), "n": count()}), [{"r": "n", "total": 1, "n": 2}, {"r": "s", "total": 2, "n": 1}])
assert_eq(group_by(rows, "r")[1], {"key": "s", "items": [{"r": "s", "m": 2}]})
assert_eq(count_by(rows, "r"), [{"key": "n", "count": 2}, {"key": "s", "count": 1}])
assert_eq(drop_missing(rows, "m"), [{"r": "n", "m": 1}, {"r": "s", "m": 2}])
assert_eq(fill_missing(rows, {"m": 0})[2], {"r": "n", "m": 0})
assert_eq(join([{"id": 1, "a": 1}], [{"id": 1, "b": 2}], "id", "left"), [{"id": 1, "a": 1, "b": 2}])
assert_eq(collect(rows, "m"), [1, 2, nothing])
assert_eq(mean([1, nothing, 3]), 2.0)
assert_eq(round_to(std([1, 2, 3, 4]), 3), 1.291)
assert_eq(round_to(std([1, 2, 3, 4], ddof = 0), 3), 1.118)
assert_eq(csv_parse("a,b\n1,\n", {"types": {"a": "int"}}), [{"a": 1, "b": nothing}])
assert_eq(random_int(rng(42), 1, 6), 1)
test "os.environ.get: env(name, nothing) is nothing when undefined (engine v0.6.45+)"
assert_eq(env("DOCS_UNSET_VAR", nothing), nothing)
assert_eq(env("DOCS_UNSET_VAR", true), "true") -- Synsema text, not Python's "True"
assert(contains(error_of(() => env("DOCS_UNSET_VAR")), "DOCS_UNSET_VAR"))Reflejos de sintaxis§
| En Python | En Synsema | ⚠️ Divergencia |
|---|---|---|
x = 5 … x = 6 | let x be 5 … set x to 6 | x = 5 → `declare with let x be …, change it with set x to … . = existe SOLO en params con default / args nombrados: task f(x, y = 1), f(x, y = 2)` |
x += 1 | set x to x + 1 | no hay asignación compuesta: `Synsema has no compound assignment (+=): write set x to x + 1` |
# comentario / // comentario | -- comentario | # → Unexpected character: '#'. // es división entera, así que un comentario estilo C falla con la pista `comments in Synsema start with -- ` |
if / elif / else: | when / otherwise when / otherwise (sin dos puntos) | un : al final → Synsema blocks have no colon; if, elif, else → `is not a Synsema statement con la forma a escribir (chain with otherwise when <condition> , the fallback branch is otherwise `) |
x if c else y / c ? x : y | when c then x otherwise y | forma expresión inline, usable en let/args; la forma de Python y la de C → `the inline form is when c then a otherwise b ` |
a and b / a && b / a; b | a and b / una sentencia por línea | &, | → `the logical operators are and / or ; ; → one statement per line (no ;)` |
for x in xs: | each x in xs | for → error de parseo |
break / continue / pass | stop / envolver el resto del cuerpo en when / dejar afuera la rama | no hay continue: `skip to the next item by wrapping the rest of the body in when ; break → leave a loop with stop ` |
d["k"] = v (agregar o pisar una clave) | set m["k"] to v | m["k"] la lee, "k" in m la prueba. Un camino de set arranca en una variable (set m["a"]["b"] to v) |
for k in un_dict: / for k, v in d.items(): | each k in m / each e in items(m) … e.key / e.value | each sobre un map recorre sus claves en orden de inserción; sobre texto, sus caracteres; sobre bytes, ints 0–255 |
for i, x in enumerate(xs): | each e in enumerate(xs) … e.index / e.item | enumerate(list) → [{index, item}, …]; desde el motor v0.6.46 también sobre un array numérico (su primer eje), que each no recorre |
while c: | while c | mismo keyword, sin dos puntos, sin tope de iteraciones |
def f(x): return v | task f(x) … give v | errores al cargar que nombran la palabra Synsema: `return is not a Synsema statement: give <value> returns from a task`, def is not a Synsema statement: a function is task name(params) …`` |
f(1) sobre def f(a, b) | un argumento que falta o sobra es un error | task 'f' is missing argument 'b' — pass it, or give the parameter a default, task 'f' takes 2 arguments, got 3; los builtins también: append() takes at most 2 arguments, got 3 |
lambda x: x + 1 | (x) => x + 1 | lambda y: y → a lambda is (y) => y |
None / True / False | nothing / true / false | las formas con mayúscula parsean y después fallan nombrando la forma Synsema: `Undefined variable: 'None' — in Synsema: nothing` |
x is None | x == nothing / is_missing(x) | no hay operador is de identidad (is es de match); el error de parseo lo dice |
os.environ.get("K") / os.getenv("K", "d") | env("K", nothing) / env("K", "d") (pide require env("K")) | una variable no definida con default nothing es nothing (motor v0.6.45+; antes era el texto "None"); otro default vuelve como texto de Synsema: env("K", true) → "true", no "True". Sin default, una variable no definida es un error |
f"n={n}" | ` n={n} ` (string con backticks) | f"..." → `an f-string is a template with backticks here. Un hueco acepta cualquier valor (listas, maps, nothing), como un f-string. Las strings con comillas "..." NO interpolan ("{n}" queda literal) y un newline literal adentro es Unterminated string` — los backticks hacen ambas cosas |
"%d" % x / "{}".format(x) | ` {x} / fmt("{x}", {"x": x})` | % sobre texto → there is no %-formatting: use a template. fmt es estricto como str.format: un {name} sin valor es error; {{/}} son llaves literales |
"""multilínea""" | ` multilínea ` | los backticks permiten newlines reales + {expr} |
"\u00e9", "\U0001F600" | "\u00e9", "\u{1F600}" | un par sustituto "\uD83D\uDE00" es un solo carácter; para una barra invertida + u literal escribí "\\u". \x no es un escape, y un \u incompleto queda como está ("C:\users") |
[f(x) for x in xs if p(x)] | apply(f, where(xs, p)) | la sintaxis de comprehension → no comprehensions: apply(xs, (v) => …) to transform, where(xs, (v) => …) to filter |
xs[-1], s[0] | xs[-1], s[0] | igual: los índices negativos cuentan desde el final (listas, texto, bytes); el texto se indexa por carácter. Un índice tiene que ser entero (xs[1.7] → index must be an integer, got 1.7; xs[1.0] anda) |
xs[1:3], xs[-2:] | slice(xs, 1, 3), slice(xs, -2) | [1:3] → no slice syntax: slice(xs, start, end?); slice acepta negativos estilo Python, funciona en listas/texto/bytes |
x in xs / x not in xs | x in xs / x not in xs | igual: elemento de lista, clave de map, substring, subsecuencia de bytes. El texto necesita texto: 1 in "a1" → error (text(1) in "a1"). contains(xs, x) es la forma de función |
a < x <= b | a < x <= b | comparaciones encadenadas como en Python; cada operando se evalúa una vez |
try/except E as e: | try … recover err | except → `except is not Synsema: after the try block write recover err. err es el TEXTO del mensaje (no hay tipos/jerarquía de excepciones). recover TRAGA por defecto — re-propagá con raise(err)` |
raise ValueError("x") | raise("x") (o la sentencia raise "x") | un solo tipo de error |
import json, import requests | nada que importar — los builtins son globales | import x → `import is not a Synsema statement: import a local module with use "./file.syn" as name`. JSON/HTTP/etc. son builtins gateados por capabilities (abajo) |
from mimodulo import f | use "./mimodulo.syn" as m … m.f() | solo módulos .syn locales; los exports necesitan export — Módulos |
class Person: | type Person (campos) + tasks planas | sin métodos/herencia/self; construí Person("Alice", 30), accedé p.name / name of p / p["name"]. xs.append(2) → Synsema has no methods: set xs to append(xs, item) |
match/case | match … is patrón | los brazos usan is, el default es otherwise — Control de flujo |
Además: las palabras LLM reason / decide / analyze / generate (como toda keyword — to, type, match…) son reservadas como nombres que bindeás (variables, parámetros, tasks): let reason be 1 → 'reason' is a reserved word in Synsema. Después de un . vale cualquier palabra (tx.to, ev.type, mod.decide(…)). Nombrá las cosas resolve, why, etc.
Equivalentes de builtins (los métodos son tasks planas)§
| En Python | En Synsema |
|---|---|
len(x) / type(x) | length(x) (text/list/map/bytes/array) / type_of(x) → "number", "text", … |
str(x) / int(s) / float(s) | text(x) / int(s) (entero exacto: "-42", "1_000", "0x1f", un float entero; int(1.5) es error — floor/round/trunc a propósito) / number(s) (siempre float) |
int(s, 16) | int(s, base = 16). La base y el fallback van por nombre: int(s, fallback = 0) devuelve 0 en vez de lanzar. int("ff", 16) (cualquier número entero 2..36 en segundo lugar, 2.0 incluido) es un error que nombra las dos formas |
hex(n) / b.hex() | hex(n) → "0x1f18", hex(b) → "0x00ff" (siempre con prefijo 0x) |
isinstance(x, int) / str / list / dict | is_integer(x) / is_text(x) / is_list(x) / is_map(x) — is_integer(1.0) es false |
a // b / a % b | a // b / a % b — redondean hacia abajo como Python (-7 // 2 → -4), exactos para enteros de cualquier tamaño; con floats, exactamente el resultado de CPython (7 // 0.1 → 69.0) |
round(x, 2) | round_to(x, 2) — round toma un argumento: round(x, 2) → round() takes at most 1 argument, got 2 — to round to n decimals: round_to(x, n) |
xs.append(x) (muta) | append(xs, x) → devuelve una lista NUEVA; reasigná: set xs to append(xs, x) (tiempo constante por paso, bien en un loop) |
xs.insert(i, x) | set xs to insert(xs, i, x) — negativos como Python, pero un i fuera de rango es error (Python lo recorta) |
s.upper() / s.lower() / s.strip() | upper(s) / lower(s) / trim(s) |
s.split(",") / ",".join(xs) | split(s, ",") / join(xs, ",") |
s.startswith(p) / s.replace(a, b) / s.find(p) | starts_with(s, p) / replace(s, a, b) / index_of(s, p) |
sorted(xs) / sorted(xs, key=f, reverse=True) | sort(xs) / sort_by(xs, f, desc = true) (estable; nothing y NaN van al final; mezclar números y texto es error) |
sum(xs) / min(xs) / max(xs) | sum(xs) / min(xs) / max(xs) (también variádico max(a, b, c); los nothing se saltean) |
map(f, xs) / filter(p, xs) | apply(f, xs) / where(xs, p) — ambos aceptan cualquiera de los dos órdenes |
functools.reduce(f, xs, init) | reduce(xs, f, init) |
xs.index(v) (lanza) | index_of(xs, v) → nothing si no está (no -1, no error) |
d.get(k) / d.get(k, default) | get(m, k) → nothing si falta / get(m, k, default); también listas: get(xs, -1) |
d.pop(k) / {a, b} | remove(m, k) / merge(a, b) — los dos devuelven un map NUEVO (en merge gana el valor de más a la derecha) |
d.keys() / d.values() / d.items() | keys(m) / values(m) / items(m) → [{key, value}, …] |
json.dumps(x) / json.loads(s) | json_encode(x) / json_decode(s) (puros, sin import; los enteros grandes quedan exactos; un secret adentro es un error, motor v0.6.46+) — JSON |
pd.read_parquet(p, columns=["a"]) | parquet_scan(p, {"columns": ["a"]}) (motor v0.6.46+; sólo se leen esas páginas del disco; require file.read(p)) — Análisis de datos |
range(n) | range(n) → una lista real (también range(a, b, paso); solo enteros) |
print(...) | print(...) — se escribe en el momento; el texto dentro de una lista o map sale entre comillas: ["1", 1] |
re.fullmatch / re.findall / re.search(p, s).groups() / re.sub | matches(s, pat) (match COMPLETO) / regex_find_all(s, pat) / regex_capture(s, pat) (siempre una lista — los grupos, o [match] sin grupos — o nothing) / regex_replace(s, pat, rep) — Builtins |
hmac.new(k, m, sha256).digest() | hmac(m, k) → bytes (hex(mac) para mostrarlo) |
open(p).read() / requests.get(url) | read_file(p) + require file(...) / fetch(url) + require net(host) — Archivos, HTTP |
requests.post(url, json=d) / r.json() / r.content | http_post(url, d) (un map → JSON + Content-Type) / json of r (nothing si no es JSON) / http_bytes(...) → bytes of r — HTTP |
xs[::-1] / list(s) | reverse(xs) (también texto) / split(s, "") |
os.getcwd() / os.path.expanduser("~/x") | cwd() + require file.read(".") / "~/x" funciona tal cual en rutas y scopes — Archivos |
os.remove(p) / shutil.rmtree(p) | delete_file(p) / delete_dir(p, {"recursive": true}) + file.write sobre cada ruta |
zipfile / tarfile | zip_create/zip_extract / tar_create/tar_extract — zip-slip rechazado, techo de bytes reales (nativo) |
tomllib.loads(s) / xmltodict.parse(s) | toml_parse(s) (fechas → texto ISO) / xml_parse(s) (misma forma) — JSON, XML y TOML |
Análisis de datos — reflejos de pandas / numpy§
Una tabla es una lista de maps (no hay DataFrame ni índice): cada operación toma filas y da filas. Los nombres de pandas (dropna, fillna, groupby, value_counts, read_csv…) fallan nombrando el builtin Synsema (`Undefined variable: 'dropna' — in Synsema: drop_missing(rows)`).
| En Python | En Synsema |
|---|---|
df.groupby("r").agg(total=("m", "sum"), n=("m", "size")) | summarize(rows, "r", {"total": sum_of("m"), "n": count()}) — grupos en orden de primera aparición |
for k, g in df.groupby("r"): | each g in group_by(rows, "r") … g.key / g.items — una lista de {key, items}, no un dict |
df["c"].value_counts() | count_by(rows, "c") → [{key, count}] |
df.dropna(subset=["m"]) / df.fillna({"m": 0}) | drop_missing(rows, "m") / fill_missing(rows, {"m": 0}) — lo faltante es nothing |
df.merge(o, on="id", how="left") | join(rows, o, "id", "left") (join(xs, sep) con dos argumentos sigue siendo el join de texto) |
df["c"] / df[df.x > 2] | collect(rows, "c") / where(rows, (r) => r.x > 2) |
df.mean() (saltea NaN) | mean(xs) saltea nothing; NaN se propaga |
df.std() / np.std(xs) | std(xs) es la cifra muestral, como pandas / std(xs, ddof = 0), como numpy |
pd.read_csv(p, dtype={"a": int}) | csv_parse(read_file(p), {"types": {"a": "int"}}) — las columnas sin tipo quedan texto; una celda vacía es nothing |
np.random.default_rng(42).integers(1, 7) | random_int(rng(42), 1, 6) — los mismos números que numpy; el tope es inclusivo |
El pipeline completo (CSV, estadística, gráficos) está en CSV, estadística y gráficos; cada builtin de tablas, en Builtins.
Trampas semánticas — parece Python, se comporta distinto§
| Parece que | Lo que pasa en realidad (doctesteado arriba) |
|---|---|
a and b cortocircuita y devuelve el operando (x or "default") | También cortocircuita pero siempre devuelve un bool — x or "default" es true/false, nunca el default. Usá when x == nothing … set x to "default", o get(m, k, default) para un map |
xs.append muta in place | append (y familia) devuelven valores nuevos; el original queda intacto. Reasigná con set |
| Dos nombres para una misma lista/dict ven los cambios del otro | Las listas y los maps son valores: let ys be xs + set ys[0] to 9 deja xs intacto, y una task que hace set adentro de un map que recibió NO cambia el del que la llamó — devolvelo con give y hacé set en el llamador |
for r in rows: r["x"] = 1 actualiza las filas | each r in rows + set r["x"] to 1 cambia la copia del loop (synsema check avisa). Escribí set rows to apply((r) => merge(r, {"x": 1}), rows), o por índice: set rows[i]["x"] to 1 |
d["missing"] → KeyError que atrapás por tipo | Map has no key 'missing' — atrapable solo como try/recover (texto del mensaje). Para una clave opcional, get(m, "missing", default) |
"a" + 1 → TypeError | Concatena: "a" + 1 → "a1" (texto + número/bool). Pero texto + nothing/lista/map/bytes SÍ es error (Cannot add text and nothing — convert it on purpose: text(x), or interpolate it), y también "ab" * 2 y 1 + true — no hay repetición ni aritmética de bools |
except: deja el programa muriendo | recover traga el error entero (la task termina normal). Para fallar hacia arriba, raise(err) dentro de recover |
Más trampas (también verificadas contra el engine): Contra tus instintos y Errores y exit codes.
Dónde tu intuición de Python SÍ es segura (verificado — confiá)§
- La división siempre devuelve float (
10 / 3→3.33…,10 / 2→5.0), como Python 3. La división entera es//. -2 2→-4,2 3 2→512, y1e18es un float — exactamente como Python. Un entero grande exacto se escribe1018.- Los enteros no tienen límite de tamaño, y comparar un entero con un float es exacto (
2 ** 53 + 1 == 9007199254740992.0→false);json_decodemantiene exactos los enteros grandes. - Iterar un dict da sus claves (
each k in m);in,not in, los índices negativos y las comparaciones encadenadas andan como en Python. - Llamar con un argumento de menos o de más es error.
round()es redondeo banker's, igual que Python:round(2.5)→2,round(3.5)→4.- Truthiness:
nothing/false/0/""/[]/{}son falsy, todo lo demás truthy. [1] + [2]→[1, 2](concatenación de listas),sliceacepta índices negativos.- Los literales de map
{"k": v}y de lista se ven y anidan como dicts/listas; una coma final vale en listas, maps y llamadas. - La indentación define bloques (4 espacios), comentarios hasta fin de línea,
and/or/notson palabras.
Sin equivalente en Python — leé la página del tema antes de usar§
- Capabilities: la I/O es deny-by-default; declará
require net("host")/file(...)/
db(...) / serve(PUERTO) / llm arriba o las llamadas fallan → Capabilities
- Ops LLM como keywords:
decide between [...] given x,generate,analyze,reason→ Primitivas LLM - Agentes/concurrencia:
agent/spawn/share/observe/signal/wait_for,
parallel_map → Multi-agente
- Servidor HTTP como sintaxis:
serve on 8080+ bloquesroute "GET /x"→ Serve - Reflejos de FastAPI:
@app.post("/x")+ un modelo Pydantic ↔route "POST /x"+expect body {…};/docsy/openapi.json↔ las mismas URLs, generadas;app.openapi()↔synsema openapi app.syn→ Construí una API - Secretos: valores
secret("KEY")que jamás se imprimen/serializan → Secretos - Humano en el loop:
approve/confirm/ask/show→ Humano en el loop - Tests en el archivo: bloques
test "nombre"+assert_eq, corridos porsynsema test→ CLI