Synsemadocsv0.6.xENES

Language

Control flow & errors

control.syn
-- Doc example: control flow (when / match / each / while) and errors (try / recover / raise).
intent: "doc example: control flow and errors"

enum Status
    paid(amount)
    shipped(days, carrier)
    open

task grade(score)
    when score >= 90
        give "A"
    otherwise when score >= 70
        give "B"
    otherwise
        give "C"

task status_label(status)
    match status
        is "open"
            give "active"
        is "done"
            give "archived"
        otherwise
            give "unknown"

task describe(v)
    match v
        is 0
            give "zero"
        is Status.paid(amount)
            give "paid " + text(amount)
        is Status.shipped(d, c) when c == "DHL"
            give "express"
        is [first, ...rest]
            give "list of " + text(1 + length(rest)) + " starting at " + text(first)
        is {name, age} when age >= 18
            give "adult " + name
        is {status: 200, body}
            give "ok: " + body
        is _
            give "something else"

task shape(xs)
    match xs
        is []
            give "empty"
        is [a, b]
            give "pair " + text(a + b)
        is [...init, last]
            give "last " + text(last) + " after " + text(length(init))

task is_target(v, target)
    match v
        is target
            give true
        otherwise
            give false

task sum_list(xs)
    let total be 0
    each n in xs
        set total to total + n
    give total

task always_fails()
    raise("boom")

task fails_as_statement()
    raise "bad input"

task give_inside_try()
    try
        give "given"
    recover err
        give "recovered"

print("grade(85) = " + grade(85) + ",  sum_list([1,2,3]) = " + text(sum_list([1, 2, 3])))

test "when / otherwise when / otherwise"
    assert_eq(grade(95), "A")
    assert_eq(grade(75), "B")
    assert_eq(grade(50), "C")

test "match dispatches on value"
    assert_eq(status_label("open"), "active")
    assert_eq(status_label("done"), "archived")
    assert_eq(status_label("x"), "unknown")

test "match patterns: literal, enum variant, guard, list, map, wildcard"
    assert_eq(describe(0), "zero")
    assert_eq(describe(Status.paid(5)), "paid 5")
    assert_eq(describe(Status.shipped(2, "DHL")), "express")
    assert_eq(describe(Status.shipped(2, "UPS")), "something else")
    assert_eq(describe([7, 8, 9]), "list of 3 starting at 7")
    assert_eq(describe({"name": "Ana", "age": 20, "city": "x"}), "adult Ana")
    assert_eq(describe({"name": "Ana", "age": 10}), "something else")
    assert_eq(describe({"status": 200, "body": "hi"}), "ok: hi")
    assert_eq(describe("x"), "something else")

test "list patterns: [], [a, b], [...init, last]"
    assert_eq(shape([]), "empty")
    assert_eq(shape([1, 2]), "pair 3")
    assert_eq(shape([1, 2, 3]), "last 3 after 2")

test "a bare name in an `is` arm compares with its value (it does not bind)"
    assert_eq(is_target(3, 3), true)
    assert_eq(is_target(4, 3), false)

test "each accumulates"
    assert_eq(sum_list([1, 2, 3, 4]), 10)

test "each walks a map's keys, text's characters, bytes as integers"
    let ks be []
    each k in {"b": 1, "a": 2}
        set ks to append(ks, k)
    assert_eq(ks, ["b", "a"])
    let cs be []
    each c in "hé"
        set cs to append(cs, c)
    assert_eq(cs, ["h", "é"])
    let bs be []
    each b in bytes([1, 255])
        set bs to append(bs, b)
    assert_eq(bs, [1, 255])

test "stop leaves a loop; there is no continue — wrap the rest in when"
    let seen be []
    each n in [1, 2, 3, 4, 5]
        when n == 4
            stop
        when n != 2
            set seen to append(seen, n)
    assert_eq(seen, [1, 3])

test "each gives a copy of each item: write back by index"
    let rows be [{"p": 1}, {"p": 2}]
    each i in range(length(rows))
        set rows[i]["p"] to rows[i]["p"] * 10
    assert_eq(rows, [{"p": 10}, {"p": 20}])

test "while repeats until the condition is false"
    let n be 0
    while n < 5
        set n to n + 1
    assert_eq(n, 5)

test "try / recover catches; raise re-raises"
    let caught be ""
    try
        raise("boom")
    recover err
        set caught to err
    assert(contains(caught, "boom"))
    assert_error(always_fails)
    assert_error(fails_as_statement)

test "give and stop pass through try untouched"
    assert_eq(give_inside_try(), "given")
    let n be 0
    each x in [1, 2, 3]
        try
            when x == 2
                stop
            set n to n + 1
        recover err
            set n to 100
    assert_eq(n, 1)

task message_of(f)
    try
        f()
    recover err
        give err
    give nothing

task page(limit, offset = 0)
    give limit + offset

test "each walks lists, not arrays: enumerate(a) gives the index and the row (engine v0.6.46+)"
    let total be 0
    each e in enumerate(array([5, 6]))
        set total to total + e.index * 10 + e.item
    assert_eq(total, 21)
    assert(contains(message_of(() => enumerate(array(5))), "0-d array"))

test "messages that name the fix (engine v0.6.46+)"
    assert(contains(message_of(() => log(100)), "ln(x) for the natural log, log10(x), log2(x) or log_base(x, b)"))
    assert(contains(message_of(() => in_units(90, "minutes")), "in_units(duration(seconds = 90), \"minutes\")"))
    assert_eq(in_units(duration(seconds = 90), "minutes"), 1.5)
    assert(contains(message_of(() => page(limt = 5)), "unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset"))
    assert(contains(message_of(() => sort([3, 1], dsc = true)), "did you mean 'desc'?"))
    assert_eq(page(limit = 5), 5)

Conditionals§

when score >= 90
    give "A"
otherwise when score >= 70
    give "B"
otherwise
    give "C"

No colon after the condition, and no if/elif/else: those words fail with the Synsema form in the message (`elif is not a Synsema statement: chain with otherwise when <condition> `). The expression form is when c then a otherwise b` — see Syntax.

match§

match dispatches on a value with rich patterns — literals, enum variants, list/map patterns, guards, and _:

match result
    is "open"                                  -- a value (compared with ==)
        open_it()
    is Status.shipped(days, carrier) when carrier == "DHL"
        express(days)                          -- enum variant + binders + guard
    is [first, ...rest]                        -- list head + tail
        handle(first)
    is {name, age} when age >= 18              -- map keys as binders (extra keys ignored)
        greet(name)
    is {status: 200, body}                     -- map field + binder
        show_ok(body)
    is _                                       -- wildcard
        skip()
    otherwise
        fallback()

A bare name in an arm (is target) compares with that variable's value; it does not bind. Binders appear only inside list, map and variant patterns. List patterns also take [a, b], [] and [...init, last].

Loops§

order, over text its characters, over bytes integers 0–255. Each item is a copy: to change the list, write by index (set rows[i]["p"] to …) or rebuild it with apply. For an index next to each item, each e in enumerate(xs) → e.index (from 0) and e.item; since engine v0.6.46 enumerate also takes a numeric array and walks its first axis (each item a number, or a row). each itself does not walk an array (Cannot iterate over array): use enumerate(a) or to_list(a).

the condition changes.

(the parser says so if you write continue).

Prefer the intentional operations (apply/where/reduce) when you're transforming a list.

Errors — try / recover / raise§

try
    risky()
recover err
    log "failed: " + err            -- err is the message (text)
    raise(err)                      -- RE-PROPAGATE; without it, recover swallows the error

raise(message) fails deliberately. give and stop are not errors — they pass through try/recover untouched. (fail(...) is for HTTP responses, not runtime errors.) There is one kind of error (no exception types), no except/catch and no finally: put the cleanup after the try/recover block.

The statement form raise "msg" / raise err (no parens) also works — it desugars to the same call — and a bare raise alone is a loud parse error pointing at the fix. ⚠️ On the published v0.5.1 and older, the no-parens form silently did nothing (it parsed as two inert expressions); on those binaries always write raise("msg").

Messages that name the fix§

An error says what to write instead, and synsema check warns several of them before the program runs. Since engine v0.6.46:

You wroteThe messageWrite instead
log(x) (Python's math.log)`Undefined variable: 'log' — in Synsema: ln(x) for the natural log, log10(x), log2(x) or log_base(x, b); log "message" (no parentheses) writes a log line (synsema check` warns it too)ln(x), log10(x), log2(x) or log_base(x, b); log "message" is the log statement
in_units(90, "minutes") (a number)in_units() takes a duration, got number — make one first: in_units(duration(seconds = 90), "minutes")build the duration first: in_units(duration(seconds = 90), "minutes") → 1.5
a named argument the task or builtin does not have: page(limt = 5)unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset (without a close name, only the list: unknown parameter 'zzzzzz'; the parameters are: limit, offset)one of the names listed. Before v0.6.46 a misspelled name on a task said that the right one was missing

The in_units hint always shows seconds and "minutes", with your number: adapt the unit you meant. An unknown named argument is found when the call runs — synsema check does not warn it. The same holds for lambdas (f(1, c = 2) on (a, b) => … lists a, b) and builtins (sort(xs, dsc = true) → did you mean 'desc'?).