Synsemadocsv0.6.xENES

Getting Started

Counter your priors

No LLM was trained on Synsema, so a model writing Synsema falls back on habits from other languages — and on older Synsema. These are the differences that bite. Every claim here is verified against the engine (the ones below are a passing doctest):

Coming from Python specifically? Start with Python → Synsema — the translation table: it maps the Python reflexes (def/return, f-strings, comprehensions, try/except, d.get) to the Synsema forms in one place. This page collects the rest of the engine-verified gotchas.
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"))

The language rules that changed (what older code and older models get wrong)§

logical copy (copy-on-write, so it costs nothing until someone writes). A task that sets inside a map it received does not change the caller's: give it back and set cfg to set_flag(cfg). Inside each r in rows, set r["x"] to 1 changes the loop's copy — write set rows[i]["x"] to 1 or rebuild the list with apply + merge. Shared state is always explicit: the blackboard, memory, the bus, state_*, or an exported module variable.

(task 'add' takes 2 arguments, got 3, trim() takes at most 1 argument, got 2) is an error. Callbacks that a builtin or the host calls (apply, where, route handlers, cron) still receive what they get.

precedence: 1 + 2 |> double is double(3). A step that is a call gets the value as its first** argument: xs |> sort_by(f) is sort_by(xs, f).

with the hint `comments in Synsema start with -- `.

convert it on purpose: text(x), or interpolate it. Text + number or bool still joins; a backtick hole takes any value ( id={nothing} → id=nothing`).

CSS, JSON) pass through.

whole number 2..36 there (also 2.0) is ambiguous. Write int(s, base = 16) or int(s, fallback = 0).

(s[0]) and chained comparisons (1 < x <= 10) all exist.

Inside a list or map, text is shown quoted: print(["1", 1]) → ["1", 1].

Renamed builtins — write the new name§

The old names still run as deprecated aliases until v1.0: a program that uses one prints one warning on stderr, and synsema check flags every use. Three of them also changed meaning:

OldNewNote
replace_textreplacesame shape
find_allregex_find_allsame shape
replace_reregex_replacesame shape
captureregex_capture⚠️ always a list — the groups, or [match] without groups — or nothing
foldfold_textlowercase + strip accents
eyeidentitynumeric arrays
hmac_sha256(data, key) → hex texthmac(data, key, algo?) → bytes⚠️ hex(mac) to show it
eth_*, tx_eip1559, tx_eip1559_rawevm_*, evm_tx, evm_tx_rawthe JSON-RPC method names you pass to evm_rpc stay "eth_call"… — Blockchain
solana_message / solana_tx(message, signatures)solana_tx(params) builds / solana_tx_raw(message, signatures) assembles⚠️ the old two-argument solana_tx is an error pointing to solana_tx_raw
algorand_tx_encode / algorand_tx(txn, sig)algorand_tx(txn) builds / algorand_tx_raw(txn, sig) assembles⚠️ the old two-argument algorand_tx is an error pointing to algorand_tx_raw
solana_confirm, algo_addresssolana_wait, algorand_address

More gotchas (verified)§

the function form passes check but fails at runtime (Undefined variable: 'log').

auto-granted (random is for tokens/nonces, so it's deny-by-default). A seeded generator, rng(42), is pure and needs nothing: random_int(rng(42), 1, 6) gives numpy's numbers.

transform/reduce/sort_by/group_by/find_first/every/some/count_where/ zip_with) accepts both (fn, list, …) and (list, fn, …); two tasks or two lists where one-and-one is expected is an explicit error, never a guess. index_of returns nothing when absent (not -1 — check when idx != nothing).

default, get(m, "k", "default") or a when.

then x.c. After a . any word is a member name (tx.to, ev.type). And request/query/params exist only inside route handlers — pass them to helper tasks as parameters.

Capability not granted: memory unless the program has require memory("<name>") at the top (not auto-granted, even under run: it writes files). The declared name — not the filename — keys the .db; see Memory & state.

results (pass a 5th argument to raise the limit). Inside an agent, recall() reads only that agent's own entries by default — cross namespaces with recall(from = "other") or from = "*".

analyze X for "...", generate "..." given X. A bare reason "literal" is valid too.

To detect the suffix, read path of request — the param itself is already stripped.

file.read("./data/") is the files directly in ./data; "this folder and everything below it" is ./data/. file.read("") is not the whole disk — that is require file.read with no scope.

require file.read("./static/**") (the server does not start without it), and every { include }/{ layout } from disk needs file.read like render() (engine v0.6.45+).

json_for_script and csv_encode of a secret are an error (it used to be "[redacted]"), and so is a map with a secret inside in a template hole. A key the API wants in the URL, the query or the body does not need reveal(): bind it to the host, require secret("K") for net("api.example.com"), and put it there.

never loopback, private networks or the cloud metadata endpoint, whatever the name resolves to. A database or service on the private network is named: require net("db:5432"), its address, a CIDR, or require net(env("CONTROL_URL")).

--cap-set/--sandbox lets it remove only the principals it names (declassify=customers); require declassify("P") in the program declares it but does not bound it. And it can only be called by its name — no alias, no passing it around.

host's llm=P@<provider>, private data may reach a model, but what comes back (all of an llm_step map, its name included) is private too — declassify the part you act on.

is checked by call/call_tool (strictly, without converting) and not by a normal call.

GET /:lang/:version, a static "/assets" mount is never reached — serve assets with a declared GET /assets/*path route instead.

cannot leave the project — the directory of the entry file (../x.syn from the entry is denied; a module in a subdirectory can use "../core/x.syn" inside the project).

If a model keeps generating one of these wrong, copy this page into its context — it's exactly the prior that needs correcting.