Language
Syntax
Synsema reads like prose. Blocks are defined by indentation (4 spaces or 1 tab) — there are no curly braces and no colons.
Coming from Python? Read Python → Synsema — the translation table first — it maps the Python reflexes to these forms and flags the semantic traps (doctested).
-- Doc example: core syntax. Universal code; English comments.
intent: "doc example: variables, operators, pipe, inline conditional, strings, numbers"
task double(n)
give n * 2
task add(a, b)
give a + b
print("5 |> double = " + text(5 |> double)) -- run shows: 5 |> double = 10
test "let / set / arithmetic"
let x be 10
set x to x + 5
assert_eq(x, 15)
assert_eq(10 % 3, 1)
assert_eq(10 / 4, 2.5)
assert_eq(10 / 2, 5.0)
assert_eq(10 // 3, 3)
assert_eq(-7 // 2, -4)
assert_eq(-7 % 2, 1)
assert_eq(2 ** 10, 1024)
assert_eq(7 // 0.1, 69.0)
test "** binds tighter than unary minus and groups to the right"
assert_eq(-2 ** 2, -4)
assert_eq((-2) ** 2, 4)
assert_eq(2 ** 3 ** 2, 512)
assert_eq(2 ** -1, 0.5)
test "comparison + boolean logic"
assert(1 < 2 and 2 <= 2)
assert(true or false)
assert(not false)
assert_eq(1 == 1.0, true)
assert_eq(2 ** 53 + 1 == 9007199254740992.0, false)
test "chained comparisons"
let x be 5
assert(1 < x <= 10)
assert_eq(1 < x < 3, false)
assert(0 < 1 < 2 < 3)
test "membership: in / not in"
assert(2 in [1, 2])
assert(3 not in [1, 2])
assert("a" in {"a": 1})
assert("ell" in "hello")
assert(bytes([2]) in bytes([1, 2]))
assert_error(() => 1 in "a1")
test "set writes along a path that starts from a variable"
let m be {"a": {}}
set m["a"]["b"] to 1
assert_eq(m, {"a": {"b": 1}})
let s be {"items": [1]}
set s.items to append(s.items, 2)
assert_eq(s.items, [1, 2])
let xs be [1, 2]
set xs[0] to 9
assert_eq(xs, [9, 2])
test "and/or short-circuit and give a bool"
let m be {"a": 1}
assert_eq("k" in m and m["k"] == 1, false)
assert_eq("a" in m or m["zz"] == 1, true)
assert_eq(1 or 0, true)
test "pipe: lowest precedence; a call step gets the value as its first argument"
assert_eq(5 |> double, 10)
assert_eq(5 |> double |> double, 20)
assert_eq(1 + 2 |> double, 6)
assert_eq(5 |> add(1), 6)
assert_eq([3, 1, 2] |> sort_by((v) => v), [1, 2, 3])
assert_eq(5 |> ((v) => add(v, 10)), 15)
test "inline conditional is an expression"
let label be when 80 >= 50 then "pass" otherwise "fail"
assert_eq(label, "pass")
let n be -3
assert_eq(when n > 0 then "pos" otherwise when n < 0 then "neg" otherwise "zero", "neg")
test "strings: escapes, \u escapes, backtick templates"
assert_eq(length("a\nb"), 3)
assert_eq(length("a\\b"), 3)
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)
let name be "Ada"
assert_eq(`Hello {name}, {1 + 1}`, "Hello Ada, 2")
assert_eq(`xs={[1, "a"]} n={nothing}`, "xs=[1, \"a\"] n=nothing")
assert_eq("{name}", "{" + "name}")
assert_eq('single', "single")
assert_eq('it\'s', "it's")
assert_eq(length("say \"hi\""), 8)
assert_eq(`\u00e9`, "é")
let a be 1
assert_eq(`x\u{a}y`, "x\\u1y")
assert_eq(text(["1", 1]), "[\"1\", 1]")
test "text indexing and negative indexes"
assert_eq("abc"[0], "a")
assert_eq("abc"[-1], "c")
assert_eq([10, 20, 30][-2], 20)
assert_error(() => [1, 2][1.5])
test "text + number joins; text + nothing does not"
assert_eq("n=" + 1, "n=1")
assert_eq("ok=" + true, "ok=true")
assert_error(() => "n=" + nothing)
assert_error(() => "xs=" + [1])
test "number literals"
assert_eq(1_000_000, 1000000)
assert_eq(0x1f18, 7960)
assert_eq(0o17, 15)
assert_eq(0b101, 5)
assert_eq(0x_ff, 255)
assert_eq(1e3, 1000.0)
assert_eq(is_integer(1e3), false)
assert_eq(1.5e-3, 0.0015)
assert(is_integer(10 ** 18))
test "a trailing comma is fine in calls, lists and maps"
assert_eq(add(1, 2,), 3)
assert_eq([1, 2,], [1, 2])
assert_eq({"a": 1,}, {"a": 1})
test "comments: -- to the end of the line, glued or not"
let x be 5 -- a note
print(x)--note
assert_eq(x, 5)
assert_eq(5 - -1, 6)
test "after a dot any word is a member name"
let tx be {"to": "0xabc", "type": 2}
assert_eq(tx.to, "0xabc")
assert_eq(tx.type, 2)Variables§
let name be value introduces a binding; set name to value reassigns it. set also writes inside a list or map, along a path that starts from a variable: set m["a"]["b"] to v, set xs[0] to v, set s.items to append(s.items, x). x = 1 is not an assignment — the error says to write let/set — and there is no +=.
Operators§
- Arithmetic:
+-///%*.
- / is always float (10 / 4 → 2.5, 10 / 2 → 5.0). - // is floor division, like Python: exact for integers of any size (10 // 3 → 3, -7 // 2 → -4); with floats, exactly CPython's answer (7 // 0.1 → 69.0, because 0.1 is stored as slightly more than 0.1). % floors the same way (-7 % 2 → 1). - binds tighter than unary minus and groups to the right: -2 2 → -4, (-2) 2 → 4, 2 3 2 → 512, 2 -1 → 0.5.
- Comparison:
==!=<><=>=. They chain like Python:1 < x <= 10means
1 < x and x <= 10, each operand evaluated once. An integer compared with a float is exact (2 ** 53 + 1 == 9007199254740992.0 → false).
- Membership:
x in coll/x not in coll— a list item, a map key, a piece of text, a
bytes subsequence. Text needs text: 1 in "a1" is an error (write text(1) in "a1").
- Logic:
andornot.and/orshort-circuit:"k" in m and m["k"] == 1is a valid
guard — the index does not run when the key is missing, and or skips its right side when the left is already true. The result is always a bool, never the operand: x or "default" is true/false, not the default (use when, or get(m, k, default)).
+joins text with a number or a bool ("n=" + 1→"n=1"); text +nothing, a list, a map
or bytes is an error — convert on purpose with text(x), or use a backtick template.
- Pipe
|>passes a value along:x |> fisf(x), and a step that is a call receives the
value as its first argument: 5 |> add(1) is add(5, 1), xs |> sort_by(f) is sort_by(xs, f). Steps chain (data |> clean |> validate), and a lambda works as a step: x |> ((v) => f(v, extra)).
Precedence, loosest first: |> · or · and · not · comparisons and in · + - · / // % · unary - · *. So 1 + 2 |> double is double(3).
Inline conditional§
when cond then a otherwise b is an expression (usable inside let, a map, or a call), distinct from the when block statement. It chains: when n > 0 then "pos" otherwise when n < 0 then "neg" otherwise "zero".
Strings§
"..."/'...'— single-line, with escapes:\n,\t,\\,\",\', and Unicode:
\u00e9 (exactly 4 hex digits) → é, \u{1F600} (1–6 hex digits) → 😀, and a surrogate pair \uD83D\uDE00 is one character. A literal newline inside is an error. Anything that is not a complete escape stays as written ("C:\users"); \x is not an escape ("C:\build\x64" is unchanged). For a literal backslash + u, write \\u.
- `
...— multiline + interpolation:Hello {name}evaluates{expr}`. A hole takes
any value, like a Python f-string (` xs={xs} n={nothing} → xs=[1, "a"] n=nothing). Escapes work too, but only the 4-digit \u (the braces of \u{…}` interpolate). Ideal for SQL/HTML.
- A quoted string never interpolates:
"{name}"is those six characters. - Text is indexed by character:
s[0], and negative indexes count from the end:"abc"[-1]→
"c" (lists and bytes too). An index must be an integer (xs[1.5] is an error; xs[1.0] is fine).
- Inside a list or map, text is shown quoted:
print(["1", 1])→["1", 1].
Numbers§
- Integers have no size limit:
10 ** 18is exact._goes only between digits:1_000_000,
1_000.5 (1__0 or 1_ is an error).
- Hex, octal and binary literals are integers:
0x1f18(7960),0o17(15),0b101(5), and
_ may follow the prefix (0x_ff).
- An exponent makes a float, as in Python:
1e3→1000.0,1.5e-3→0.0015.1e18is a
float, not an exact integer — write 10 ** 18.
Calls§
Arguments go in parentheses, separated by commas; a trailing comma is fine (f(1, 2,)), as in lists and maps. A call with a missing or an extra argument is an error — see Tasks & lambdas. After a ., any word is a member name (tx.to, ev.type), even a keyword.
Comments§
-- a comment runs to the end of the line, glued to the code or not (print(x)--note). Two cases are errors instead of a silent comment: -- glued to a value and followed by a number or ( (5--1 — write 5 - -1 or 5 -- note), and x --1 inside parentheses whose ) is on the same line (the comment would swallow it). // is floor division, so a C-style // note fails with the hint `comments in Synsema start with -- ; #` is an unexpected character.