Language
Tasks & lambdas
A task is a function: parameters, a body, and give to return a value. give leaves the task at once, even from inside a loop. Without give, a task returns whatever its last statement produced (nothing after a print) — write give whenever the result matters.
-- Doc example: tasks, default args, named args, strict arity, recursion, lambdas.
intent: "doc example: tasks and lambdas"
task add(a, b)
give a + b
task greet(name, greeting = "Hello")
give greeting + ", " + name + "!"
task factorial(n)
when n <= 1
give 1
otherwise
give n * factorial(n - 1)
let calls be 0
task next_id()
set calls to calls + 1
give calls
task stamp(id = next_id())
give id
task first_big(xs)
each x in xs
when x > 10
give x
give nothing
task no_give()
3 + 4
task only_prints()
print("side effect")
task twice(a, b = a * 2)
give b
task with_total(order)
set order["total"] to order["qty"] * order["price"]
give order
task make_adder(n)
give (x) => x + n
-- 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
print(greet("Ada") + " factorial(5) = " + text(factorial(5))) -- run shows the result
test "call, default arg, named arg"
assert_eq(add(2, 3), 5)
assert_eq(greet("Ada"), "Hello, Ada!")
assert_eq(greet("Ada", "Hi"), "Hi, Ada!")
assert_eq(greet("Ada", greeting = "Hey"), "Hey, Ada!")
assert_eq(greet(greeting = "Hey", name = "Ada"), "Hey, Ada!")
test "a default is evaluated at each call"
assert_eq(stamp() + 1, stamp())
assert_eq(stamp(100), 100)
assert_error(() => twice(3))
assert_eq(twice(3, 6), 6)
test "arity is strict: missing, extra and unknown arguments are errors"
assert(contains(error_of(() => add(1)), "task 'add' is missing argument 'b'"))
assert(contains(error_of(() => add(1, 2, 3)), "task 'add' takes 2 arguments, got 3"))
assert(contains(error_of(() => greet("Ada", tone = "x")), "unknown parameter 'tone'"))
assert(contains(error_of(() => ((x) => x)(1, 2)), "this lambda takes 1 argument, got 2"))
test "give returns at once, also from inside a loop"
assert_eq(first_big([1, 20, 30]), 20)
assert_eq(first_big([1, 2]), nothing)
assert_eq(no_give(), 7)
assert_eq(only_prints(), nothing)
test "a task gets a copy of a list or map: give it back to keep the change"
let order be {"qty": 2, "price": 5}
let done be with_total(order)
assert_eq(order, {"qty": 2, "price": 5})
assert_eq(done["total"], 10)
set order to with_total(order)
assert_eq(order["total"], 10)
test "recursion"
assert_eq(factorial(5), 120)
test "lambdas are values, passed to higher-order tasks"
let triple be (n) => n * 3
assert_eq(triple(4), 12)
assert_eq(apply(triple, [1, 2, 3]), [3, 6, 9])
assert_eq(where([{"active": true}, {"active": false}], (x) => x.active), [{"active": true}])
test "lambdas close over names; a task can give a lambda back"
let k be 10
let add_k be (x) => x + k
assert_eq(add_k(1), 11)
let add5 be make_adder(5)
assert_eq(add5(1), 6)
test "a callback that a builtin calls declares only what it uses"
assert_eq(apply(() => 0, [1, 2]), [0, 0])
assert_eq(reduce([1, 2, 3], (acc, x) => acc + x, 0), 6)
-- v0.6.46: types on parameters, checked only where arguments arrive as data (call, call_tool).
task estado(id: text, items: bool = false)
give {"id": id, "items": items}
task page(limit, offset)
give [limit, offset]
test "typed parameters are checked by call, not by a normal call"
assert_eq(call(estado, {"id": "a1"}), {"id": "a1", "items": false})
assert(contains(error_of(() => call(estado, {"id": 5})), "'id' must be text, got number"))
assert(contains(error_of(() => call(estado, {})), "missing 'id' (text)"))
assert_eq(estado(5), {"id": 5, "items": false})
test "a misspelled named argument names the closest one and lists them all"
assert(contains(error_of(() => page(limt = 1, offset = 2)), "unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset"))Default & named arguments§
task greet(name, greeting = "Hello")
give greeting + ", " + name + "!"
greet("Ada") -- uses the default
greet("Ada", "Hi") -- positional
greet("Ada", greeting = "Hey") -- named
greet(greeting = "Hey", name = "Ada") -- named, any order
The default value (= expr) is evaluated at each call (it cannot refer to the other parameters). Note = here is the argument form — general assignment is let/set, and equality is ==. Builtins take their arguments by position, except the named forms they document (sort(xs, desc = true), int(s, base = 16)…).
Arity is strict§
A call must match the task: a missing argument without a default, an extra one, or an unknown name is an error — never a silent nothing or a dropped value:
add(1)→task 'add' is missing argument 'b' — pass it, or give the parameter a defaultadd(1, 2, 3)→task 'add' takes 2 arguments, got 3greet("Ada", tone = "x")→unknown parameter 'tone'; the parameters are: name, greeting, and a near miss names the closest one (engine v0.6.46+):f(limt = 1)→unknown parameter 'limt' — did you mean 'limit'?; the parameters are: limit, offset- a lambda too:
this lambda takes 1 argument, got 2; and builtins:append() takes at most 2 arguments, got 3
A callback that a builtin or the host calls (apply, where, reduce, route handlers, cron) is the exception: it declares only the parameters it uses (apply(() => 0, xs) is fine).
Arguments are values§
A task receives a copy of a list or map (copy-on-write, so passing it costs nothing). A set inside the task changes the task's copy, not the caller's — to keep the change, give it back and set it in the caller:
task with_total(order)
set order["total"] to order["qty"] * order["price"]
give order
let order be {"qty": 2, "price": 5}
set order to with_total(order)
synsema check warns about a task that writes into a parameter and then neither gives it back, passes it on, nor reads it: that write would be lost.
Types on parameters§
Engine v0.6.46+. A parameter can carry a type, written after a colon, with or without a default:
task estado(id: text, items: bool = false)
give {"id": id, "items": items}
print(call(estado, {"id": "a1"})) -- {id: "a1", items: false}
- The five types are those of
expect body:text,number,bool,list,map. A secret counts astext. - They are checked only where the arguments arrive as data:
call(task, map)andcall_tool(task, map)— the forms a model's tool call and the host use. There the check is strict and never converts:call_tool(add): 'a' must be number, got text — …, and a typed parameter without a default that the map leaves out iscall_tool(add): missing 'a' (number)(an untyped one still arrives asnothing).nothingis accepted only when the parameter's default isnothing. - A normal call does not check them and costs nothing:
estado(5)runs. They are metadata for data that crosses a boundary, not a static type system. - Load errors, before anything runs: an unknown type (
task f(a: int)→unknown type 'int' for parameter 'a': the parameter types are text, number, bool, list or map) and a literal default of another type (task f(a: number = "x")→the default of 'a: number' is a text). Lambdas take no types:(x: number) => xdoes not parse. - They describe the task to a model:
tool_spec(task, describe)builds thellm_stepcatalog entry from the signature — see Tool calling.
synsema code outline --json publishes them per task, next to the unchanged params list of names:
"parameters": [
{"name": "id", "type": "text", "required": true},
{"name": "items", "type": "bool", "required": false, "default": false},
{"name": "extra", "type": null, "required": false, "default": null}
],
"input_schema": {
"type": "object",
"properties": {"id": {"type": "string"}, "items": {"type": "boolean", "default": false}, "extra": {}},
"required": ["id"]
}
type is null for an untyped parameter; default appears only for a literal default (null for nothing), and input_schema leaves default out for a nothing default or a computed one (= 1 + 2).
Recursion§
A task may call itself (factorial above). There is no special keyword — just give.
Lambdas§
A lambda is a value: (params) => expr. Store it, pass it, or hand it to a higher-order op:
let triple be (n) => n * 3
apply(triple, [1, 2, 3]) -- [3, 6, 9]
where(items, (x) => x.active) -- inline predicate
A lambda sees the names around it (let add_k be (x) => x + k), and a task can give one back (task make_adder(n) … give (x) => x + n).