Lambda Expressions and Statements

July 12, 2026 · View on GitHub

This document covers Lambda's expressions and statements, including control flow, operators, and pipe expressions.

Related Documentation:


Table of Contents

  1. Expression-Oriented Design
  2. Primary Expressions
  3. Arithmetic Expressions
  4. Comparison Expressions
  5. Logical Expressions
  6. Member Access and Null Safety
  7. Pipe Expressions
  8. Query Expressions
  9. Control Flow Expressions
  10. Match Expressions
  11. Statements
  12. Operators

Primary Expressions

Literals

// Literals are expressions
42
"hello"
true
null
[1, 2, 3]
{name: "Alice"}

Variables

// Variable references
x
myVariable
_underscore_name

Parenthesized Expressions

// Grouping for precedence
(x + y) * z

// Let expressions (parenthesized)
(let x = 42, x + 1)  // Returns 43

Collection Access

// Array index access
arr[0]
arr.1             // same as arr[1]
arr[1 to 4]       // Slice

// Map field access
map.key
map["key"]       // dynamic key
obj.nested.field

// Safe navigation (optional chaining) built-in
obj.maybeNull.field    // Returns null if maybeNull is null

Arithmetic Expressions

Basic Arithmetic

5 + 3              // Addition: 8
5 - 3              // Subtraction: 2
5 * 3              // Multiplication: 15
10 / 3             // Division: 3.333...
10 div 3           // Integer division: 3
17 % 5             // Modulo: 2
2 ** 3             // Exponentiation: 8

Unary Operators

-x                 // Negation
+x                 // Positive (identity)
not x              // Logical NOT
!T                 // Type negation (any except T)
*x                 // Spread (expand collection)

Spread Operator

The spread operator * expands a collection's items into the enclosing container:

let a = [1, 2, 3]
[0, *a, 4]             // [0, 1, 2, 3, 4] — items spread into array

let b = (10, 20)
(*a, *b)               // (1, 2, 3, 10, 20) — spread into tuple

let base = {x: 1, y: 2}
{*:base, x: 10}        // {x: 10, y: 2} — copy map fields, override x

// Spread in function calls
fn sum_all(...args) = args | reduce((a, b) => a + b, 0)
sum_all(*[10, 20, 30])  // 60

// Nested spreading
let nested = [[1, 2], [3, 4]]
[*nested[0], *nested[1]]  // [1, 2, 3, 4]

Map literals construct only the fields they list. Use map spread when an update should preserve the fields of an existing map or record-shaped value.

Vector Arithmetic

Lambda supports NumPy-style element-wise operations on arrays:

// Scalar-vector operations
1 + [2, 3, 4]              // [3, 4, 5]
10 - [1, 2, 3]             // [9, 8, 7]
3 * [1, 2, 3]              // [3, 6, 9]
2 ** [1, 2, 3]              // [2, 4, 8]

// Vector-vector operations
[1, 2, 3] + [4, 5, 6]      // [5, 7, 9]
[10, 20, 30] - [1, 2, 3]   // [9, 18, 27]
[2, 3, 4] * [1, 2, 3]      // [2, 6, 12]

// Broadcasting
[5] + [1, 2, 3]            // [6, 7, 8]

+ does not concatenate arrays or lists. It always means numeric addition: scalar broadcast for scalar-plus-sequence, and element-wise addition for sequence-plus-sequence. Use ++ when the intent is list/array concatenation:

[1, 2] + [3, 4]            // [4, 6]
[1, 2] ++ [3, 4]           // [1, 2, 3, 4]

Comparison Expressions

Equality

The == operator performs structural deep value equality for all types — scalars and containers alike.

// Scalar equality
5 == 5             // true
5 != 3             // true
1 == 1.0           // true (numeric promotion: int, float, decimal)

Container Equality

Containers (arrays, maps, elements) are compared by structure, not by reference. Two containers are equal if they have the same shape and all corresponding elements are equal:

// Array equality
[1, 2, 3] == [1, 2, 3]           // true
[1, 2, 3] == [1, 2, 4]           // false (element mismatch)
[1, 2, 3] == [1, 2]              // false (different length)
[] == []                          // true

// Numeric promotion composes into containers
[1] == [1.0]                      // true (element-wise: 1 == 1.0)
{a: 1} == {a: 1.0}                // true

// Map equality is order-independent
{a: 1, b: 2} == {b: 2, a: 1}     // true
{a: 1, b: 2} == {a: 1}           // false (different key count)

// Nested structural equality
[[1, 2], [3, 4]] == [[1, 2], [3, 4]]   // true
{a: {x: 1}} == {a: {x: 1}}             // true
{a: [1, 2]} == {a: [1, 2]}             // true

Cross-Type Sequence Equality

Ranges and arrays are sequences — they compare equal across types if they contain the same elements:

(1 to 3) == [1, 2, 3]            // true (range vs array)
[1, 2, 3] == (1 to 3)            // true (symmetric)
(1 to 3) == [1, 2, 4]            // false
(1 to 3) == [1, 2]               // false (different length)

Function Equality

Functions use reference equality — two function values are equal only if they are the same function object:

fn add1(x) => x + 1
let f = add1
f == f                            // true (same reference)

NaN Equality

Follows IEEE 754: NaN != NaN, including inside containers:

let x = 0.0 / 0.0
x == x                            // false
[x] == [x]                        // false

Relational

3 < 5              // Less than: true
5 > 3              // Greater than: true
3 <= 5             // Less than or equal: true
5 >= 3             // Greater than or equal: true

Null Comparisons

Null can be compared with any type:

null == null       // true
null == 42         // false (not an error)
"hello" != null    // true

// Idiomatic null check
if (x == null) "missing" else x

Type Comparisons

// Type checking
42 is int              // true
"hello" is string      // true
!(42 is string)        // true (negated type check)

// NaN detection (IEEE 754: nan == nan is false, use 'is nan' instead)
nan is nan             // true
(0/0) is nan           // true
1.0 is nan             // false

// Value comparison (when RHS is a value, not a type)
42 is 42               // true (equivalent to 42 == 42)
"hello" is "hello"     // true
[1,2,3] is [1,2,3]    // true (structural equality)
true is true           // true
inf is inf             // true

// Type equality
type(42) == int        // true
type([1,2]) == array   // true
type(42) != string     // true

Logical Expressions

Boolean Operators

true and false     // Logical AND: false
true or false      // Logical OR: true
not true           // Logical NOT: false

Short-Circuit Evaluation

// Right side only evaluated if needed
(x > 0) and (y / x > 2)    // Safe: y/x not evaluated if x <= 0
value or "default"          // Returns value if truthy, else "default"

Truthy and Falsy Values

Lambda has simple truthiness rules:

Falsy ValuesNote
null
false
errorerror is falsy, which allows idiom like: err or fallback
""Empty string is a real string value with length 0
Truthy Values (Everything Else)
true, all numbers (including 0)
All non-empty strings and all symbol values
All collections (including [], {})
All functions
Important: Unlike many languages, 0 and empty collections are truthy in Lambda.
if (0) "yes" else "no"           // "yes" - 0 is truthy
if ([]) "yes" else "no"          // "yes" - empty array is truthy
if (null) "yes" else "no"        // "no" - null is falsy
if (false) "yes" else "no"       // "no" - false is falsy
if ("") "yes" else "no"          // "no" - empty string is falsy

Member Access and Null Safety

Dot Operator

The . operator accesses fields and has built-in null safety:

// Field access
user.name
config.settings.theme

// Automatic null propagation
let x = null
x.name           // null (not an error)
x.a.b.c          // null (null propagates through chain)

Safe Navigation

Lambda's . operator behaves like JavaScript's ?. by default:

// Given potentially null objects at any level
let result = company.department.manager.name

// Equivalent to verbose null-checking:
// if (company == null) null
// else if (company.department == null) null
// else if (company.department.manager == null) null
// else company.department.manager.name

Index Access

arr[0]             // First element
arr[last]          // Last element
arr[-1]            // null
map["key"]         // Map value by key

// Null-safe index access
let data = null
data[0]            // null
data["key"]        // null

let arr = [1, 2, 3]
arr[10]            // null (out of bounds returns null)

Method Calls

// Method-style function calls
arr.len()          // Same as len(arr)
"hello".upper()    // Same as upper("hello")
items.sort()       // Same as sort(items)

// Null receiver returns null
let items = null
items.len()        // 0 (len of null is 0)
items.reverse()    // null

Pipe Expressions

The pipe operator (|>) enables fluent, left-to-right data transformation.

Basic Pipe Syntax

<collection> |> <expression-with-~>
  • |> — Pipe operator
  • ~ — Current item reference

Auto-Mapping Over Collections

When the left side is a collection, ~ binds to each item:

// Double each number
[1, 2, 3] |> ~ * 2
// Result: [2, 4, 6]

// Extract field from each item
users |> ~.name
// Result: ["Alice", "Bob", "Charlie"]

// Transform each item
["hello", "world"] |> ~ ++ "!"
// Result: ["hello!", "world!"]

Scalar Pipe

When the left side is a scalar, ~ binds to the whole value:

42 |> ~ * 2
// Result: 84

"hello" |> ~ ++ " world"
// Result: "hello world"

Chained Transformations

[1, 2, 3, 4, 5]
    |> ~ ** 2          // square: [1, 4, 9, 16, 25]
    |> ~ + 1           // add 1: [2, 5, 10, 17, 26]
// Result: [2, 5, 10, 17, 26]

Key/Index Access with ~#

// Arrays — ~# is index (0-based)
['a', 'b', 'c'] |> {index: ~#, value: ~}
// [{index: 0, value: 'a'}, {index: 1, value: 'b'}, {index: 2, value: 'c'}]

// Maps — ~ is value, ~# is key
{a: 1, b: 2} |> {key: ~#, val: ~}
// [{key: 'a', val: 1}, {key: 'b', val: 2}]

Aggregated Pipe (without ~)

When ~ is not used, the pipe passes the entire collection/data on left side to right side:

[3, 1, 4, 1, 5] |> sum        // 14
[3, 1, 4, 1, 5] |> sort       // [1, 1, 3, 4, 5]
[1, 2, 3, 4, 5] |> take(3)    // [1, 2, 3]

That Clause (Filtering)

// Basic filtering
[1, 2, 3, 4, 5] that (~ > 3)
// Result: [4, 5]

// Filter objects
users that (age >= 18)
// Keep only adult users

// == and != work without parens
[1, 2, 3, 4, 5] that ~ == 3
// Result: [3]

// Combined with pipe
data | ~.name that (len(~) > 3) | ~.upper()

Note: The relational operators <, >, <=, >= conflict with element-tag syntax in the parser. When a that (or |) condition uses any of these operators, wrap the condition in parentheses: items that (~ > 0). The operators ==, !=, and, or, +, -, *, / work without parens.

Implicit Field Access in that Clause

Inside a that clause, bare identifiers that are not in scope automatically resolve to ~.nameimplicit field access:

// Explicit: ~.field
users that (~.age >= 18 and ~.name != "admin")

// Implicit: bare field names
users that (age >= 18 and name != "admin")

// Both forms produce identical results

Name resolution order inside a that clause:

  1. Names in scope (let, var, fn, pn, type definitions)
  2. Stored field on the current item ~ (map/object/element)
  3. System properties of the current item ~
let min_age = 18
// 'min_age' resolves to the let binding; 'age' resolves to ~.age
users that (age >= min_age)

Pipe Behavior Summary

Left Side~ Binds To~# Binds ToResult
[a, b, c] (array)Each elementIndex (0, 1, 2)Array of results
(a, b, c) (tuple)Each elementIndex (0, 1, 2)Array of results
1 to 10 (range)Each numberPosition (0-9)Array of results
{a: 1, b: 2} (map)Each valueKey ('a', 'b')Collection of results
42 (scalar)The value itselfN/ASingle result

Spreading in Array Literals

Pipe (|) and filter (that) expressions inside array literals produce spreadable results — their array output is automatically flattened into the enclosing array, just like for-expressions and the spread operator:

// Pipe spreads into enclosing array
[1, [2, 3] | ~, 4, 5]                    // [1, 2, 3, 4, 5]
[0, [1, 2, 3] | ~ * 10, 99]              // [0, 10, 20, 30, 99]

// That/filter spreads into enclosing array
[1, [1, 5, 7, 10, 15] that (~ > 5), 99]  // [1, 7, 10, 15, 99]

// Non-array pipe results are pushed normally
fn double(x: int) { x * 2 }
[1, 5 | double, 4]                        // [1, 10, 4]

// Mixed: for-expr + pipe + that
[for (x in [1, 2]) x, [3, 4] | ~ * 10, [5, 6, 7] that (~ > 5)]  // [1, 2, 30, 40, 6, 7]

Rationale: This is consistent with for-expression and spread behavior — collection-producing sub-expressions flatten into the enclosing array literal, giving a uniform "inline expansion" semantics.


Query Expressions

Lambda provides type-based query operators for searching nested data structures — elements, maps, and arrays.

Recursive Query: ? and .?

The ? operator performs a recursive descendant search, returning all values at any depth that match the given type:

html?<img>                  // all <img> elements at any depth
data?int                    // all int values in the tree
data?(int | string)         // all int or string values
html?<div class: string>    // <div> elements with a class attribute
data?{name: string}         // maps with a string 'name' field
data?{status: "ok"}         // maps where status == "ok"

The .? variant is self-inclusive — it also tests the root value itself:

div.?<div>                  // includes div itself if it matches
42.?int                     // (42) — trivial self-match
el.?int                     // self + all int values in subtree

Both operators traverse attributes, children, map values, and array items in document order (depth-first, pre-order). Results are returned as a spreadable array.

Child-Level Query: [T]

The [T] child-level query searches only direct attributes and children — one level deep, no recursion:

[1, "hello", 3, true][int]        // (1, 3) — direct int items
{name: "Alice", age: 30}[string]  // ("Alice") — map values matching type
el[element]                       // direct child elements only
el[string]                        // attribute values + text children

The [T] syntax reuses the index operator expr[x]. When x is a type value, a child-level query is performed instead of normal index access:

Index value xInterpretation
int valuePositional index access
string or symbol valueNamed field access
TypeChild-level query

On elements, [T] searches both attribute values and direct children. On maps, it searches values only. On arrays, it searches items.

Chaining

Child-level queries can be chained for multi-level traversal, and mixed with ? for combined specific/recursive search:

type body = <body>
type div  = <div>

html[body][div]                // direct <div> children of <body>
html[body][div]?<a>            // then recursive search for <a>
html?<table>[tr][td]           // all tables → direct rows → direct cells

Comparison

Featureexpr?Texpr[T]
ScopeAll descendants (recursive)Direct attributes + children only
DepthUnlimitedOne level
Self-inclusive.?TN/A
AnalogyXPath //, CSS descendantXPath /, CSS > child
Return typeSpreadable arraySpreadable array

Control Flow Expressions

If Expressions

Lambda has two if forms that share the same else syntax and produce the same AST node.

Expression Form: if (cond) expr else ...

The parenthesized condition form requires else and returns a value:

// Simple if expression
let result = if (x > 0) "positive" else "non-positive"

// Nested if expressions
let grade = if (score >= 90) "A"
            else if (score >= 80) "B"
            else if (score >= 70) "C"
            else "F"

// If in let bindings
(let x = 5, if (x > 3) "big" else "small")

// Block else (NEW) — else branch can be a { stam } block
let msg = if (x > 0) "ok" else {
    let reason = diagnose(x);
    "error: " ++ reason
}

Block Form: if cond { stam } [else ...]

The block form uses { stam } for the then-branch. else is optional:

// Block if, no else
if x > 0 { print("positive") }

// Block if with block else
if temperature > 30 {
    print("hot")
} else {
    print("comfortable")
}

// Expression else (NEW) — else branch can be an expression
if x > 0 { compute(x) } else default_value

// Chaining
if x > 0 {
    "positive"
} else if x < 0 {
    "negative"
} else {
    "zero"
}

Unified Else Branch

Both forms accept the same else options:

Else formExample
else exprelse "fallback"
else { stam }else { let x = calc(); x }
else if ...else if (y > 0) ... or else if y > 0 { ... }

When else is omitted (block form only), the result is null.

Map/block ambiguity: else { ... } is always parsed as a block, not a map literal. To return a map from an else branch, use parentheses: else ({a: 1, b: 2}).

For Expressions

For expressions produce spreadable arrays that automatically flatten when nested in collections (pipe and filter expressions also spread — see Pipe § Spreading in Array Literals):

// Basic iteration - produces spreadable array
for (x in [1, 2, 3]) x * 2    // [2, 4, 6]

// Range iteration
for (i in 1 to 5) i * i       // [1, 4, 9, 16, 25]

// Conditional in body
for (num in [1, 2, 3, 4, 5])
    if (num % 2 == 0) num else null

Map Iteration with at

Use the at keyword (instead of in) to iterate over map keys or key-value pairs. This works with both static maps (literal {...}) and dynamic maps (created with map([...])). Map keys are returned as symbols.

Keys onlyfor (k at map):

for (k at {a: 1, b: 2, c: 3}) k
// ['a', 'b', 'c']

let m = map(["x", 10, "y", 20])
for (k at m) k
// ['x', 'y']

Key-value pairsfor (k, v at map):

for (k, v at {a: 1, b: 2, c: 3}) k ++ "=" ++ string(v)
// ['a=1', 'b=2', 'c=3']

let scores = map(["alice", 95, "bob", 87])
for (name, score at scores) {name: name, score: score}
// [{name: "alice", score: 95}, {name: "bob", score: 87}]

With where clause:

for (k, v at {a: 1, b: 5, c: 2} where v > 2) k
// ['b']

Note: Use in to iterate over arrays; use at to iterate over maps.

Spreadable Array Behavior

For expressions produce spreadable arrays that flatten when nested in other collections:

// Nested for-expressions flatten automatically
[for (i in 1 to 3) for (j in 1 to 3) i * j]
// [1, 2, 3, 2, 4, 6, 3, 6, 9] — flat array, not nested

// Spreading into array literals
[0, for (x in [1, 2, 3]) x * 10, 99]
// [0, 10, 20, 30, 99] — for-expr items spread into the array

// Spreading into tuples
(0, for (x in [1, 2]) x * 5, 99)
// (0, 5, 10, 99)

// Multiple for-expressions spread independently
[for (x in [1, 2]) x, for (y in [3, 4]) y * 10]
// [1, 2, 30, 40]

Empty For Results

When a for-expression iterates over an empty collection or filters all elements, it produces a spreadable null that is skipped when spreading:

// Empty iteration produces spreadable null (evaluates to null)
let v = for (i in []) i
v == null              // true

// Spreadable null is skipped in collections
[for (i in []) i]      // [] — empty array, not [null]
[1, for (i in []) i, 2]  // [1, 2] — null skipped

// Where clause filters all elements
[for (x in [1, 2, 3] where x > 100) x]  // []

Extended For-Expression Clauses

For expressions support SQL/XQuery-inspired clauses for filtering, sorting, pagination, and intermediate bindings. The full syntax is:

for (<bindings> [, let <name> = <expr>, ...] [where <cond>] [order by <spec>] [limit <n>] [offset <n>]) <body>

Clauses are processed in logical order: bindings → let → where → order by → offset → limit → body.

where — Filter

The where clause filters items by a boolean condition. Only items where the condition is truthy proceed to the body. Use and / or to combine conditions.

for (x in [1, 2, 3, 4, 5] where x > 2) x
// (3, 4, 5)

for (user in users where user.active and user.age >= 18)
  user.name

// where eliminates nulls cleanly — compare with if in body:
for (x in data where x > 0) x * 2     // [2, 4, 6]
for (x in data) if (x > 0) x * 2      // [null, 2, null, 4, ...] — nulls remain

let — Intermediate Bindings

The let clause introduces named values computed per iteration. The name is available in subsequent clauses (where, order by) and in the body. Multiple let clauses are comma-separated after the bindings, and each can reference earlier let names.

// Compute once, reuse in where and body
for (x in [1, 2, 3], let squared = x * x) squared + 1
// (2, 5, 10)

// let feeds into where
for (x in [1, 2, 3, 4, 5], let doubled = x * 2 where doubled > 4) doubled
// (6, 8, 10)

// Chained lets — each sees the previous
for (x in [2, 3, 4], let sq = x * x, let cube = sq * x where cube > 10) [x, sq, cube]
// [[3, 9, 27], [4, 16, 64]]

// let avoids redundant computation
for (order in orders,
     let subtotal = sum(order.items | ~.price * ~.qty),
     let tax = subtotal * 0.08,
     let total = subtotal + tax
     where total > 100)
  {id: order.id, total: total}

order by — Sort

The order by clause sorts the result set. The default direction is ascending. Append desc for descending, or asc for explicit ascending. Multiple sort keys are comma-separated for tie-breaking.

// Ascending (default)
for (x in [3, 1, 4, 1, 5] order by x) x
// (1, 1, 3, 4, 5)

// Descending
for (x in [3, 1, 4, 1, 5] order by x desc) x
// (5, 4, 3, 1, 1)

// Sort by field
let people = [{name: "Alice", age: 30}, {name: "Bob", age: 25}, {name: "Carol", age: 35}]
for (p in people order by p.age) p.name
// ("Bob", "Alice", "Carol")

// Sort by computed expression
for (s in ["banana", "fig", "apple"] order by len(s)) s
// ("fig", "apple", "banana")

// Multiple sort keys (secondary for tie-breaking)
for (p in employees order by p.department asc, p.salary desc)
  {name: p.name, dept: p.department}

limit and offset — Pagination

limit N returns at most N results. offset M skips the first M results. Both are applied after filtering and sorting.

// First 3 items
for (x in [1, 2, 3, 4, 5] limit 3) x
// (1, 2, 3)

// Skip first 2
for (x in [1, 2, 3, 4, 5] offset 2) x
// (3, 4, 5)

// Combined: skip 2, then take 3
for (x in [1, 2, 3, 4, 5, 6, 7] limit 3 offset 2) x
// (3, 4, 5)

// Pagination pattern: page 3, 20 items per page
for (item in items order by item.id limit 20 offset 40) item

// Offset past end returns empty
for (x in [1, 2, 3] offset 10) x
// (empty)

// Limit larger than collection returns all
for (x in [1, 2, 3] limit 10) x
// (1, 2, 3)

group by — Grouping

group by partitions the (post-where) rows by one or more key expressions and binds each group to a name via into:

group by <key-expr> [as <alias>] [, <key-expr> [as <alias>] ...] into <group-name>

The group binding is an element (tag group): the grouping keys become its attributes and the group's members become its children. One value carries the whole group, reusing Lambda's element duality — so g.region reads a key, len(g) counts members, g[0] indexes them, and g |> ~["amount"] projects a field across members.

// single key — attribute name inferred from the trailing field access (g.region)
for (x in sales group by x.region into g)
  {region: g.region, total: sum(g |> ~["amount"])}

// multiple keys — each becomes a named attribute (no positional g.key[i])
for (o in orders group by o.year, o.month into g)
  {year: g.year, month: g.month, n: len(g)}

// computed key — an alias is required (only trailing field access is inferable)
for (w in words group by len(w) as wlen into g)
  {length: g.wlen, n: len(g)}

// grouping by the loop item itself also requires an alias
for (w in doc.words
     where len(w) > 3
     group by w as word into g
     order by len(g) desc
     limit 10)
  {word: g.word, freq: len(g)}

Semantics:

  • Key equality is value equality with numeric-tower coherence, so 1 and 1.0 land in the same group. Multi-key grouping compares the key tuple element-wise. Null keys form one group.
  • The loop variable (and per-tuple let bindings) go out of scope after group by — only the into element (plus enclosing scope) is visible in order by/limit/offset and the body.
  • Groups emit in first-appearance order of their key; order by then reorders groups.
  • where filters rows before grouping; order by/limit/offset apply to groups.
  • Because a group is an element, it also formats/queries like any markup node — group by literally turns flat data into a <group ...>...</group> tree.

Join on — Relating Multiple Sources

A comma-separated source can carry an on condition to join it against the tuple stream built from the prior sources (a hash join under the hood). Without on, comma sources remain a cross product (unchanged):

// equi-join
for (o in orders, c in customers on o.cust_id == c.id)
  {id: o.id, name: c.name, total: o.total}

// left join: mark the source with `?`; unmatched prior rows appear once with c = null
for (o in orders, c? in customers on o.cust_id == c.id)
  {id: o.id, name: if (c != null) c.name else "unknown"}

// multi-key equi-join — a conjunction of equalities
for (a in xs, b in ys on a.k1 == b.k1 and a.k2 == b.k2) {...}

// chained joins — each `on` joins its source to the tuple stream so far
for (o in orders, c in customers on o.cust_id == c.id,
     r in regions on c.region_id == r.id) {...}

// index / key bindings survive the join
for (i, o in orders, c in customers on o.cust_id == c.id) {pos: i, name: c.name}

// mixed join + cross-product in one comprehension
for (o in orders, c in customers on o.cust_id == c.id, tag in tags)
  {order: o.id, cust: c.name, tag: tag}

Semantics:

  • on is a conjunction of equality tests only; each == must reference the new source on exactly one side. Non-equi conditions belong in a following where — a non-equi on is a compile error (it would silently become an O(n·m) nested loop).
  • ? marks the null-padded (optional) sidec? in customers on ... keeps every prior tuple, padding c with null on no match (left join). v1 ships inner + left joins; full/right outer are deferred. Null join keys never match (the deliberate asymmetry with group by, where null keys form a group).
  • Output preserves prior (probe-side) order, stable; multiple matches from the new source emit in that source's order.

Combined Clauses

All clauses can be used together. The recommended order matches the logical processing order:

for (x in items,
     let score = x.value * x.weight
     where x.active
     order by score desc
     limit 10)
  {name: x.name, score: score}

// Filter, compute, sort, paginate in one expression
for (x in 1 to 20,
     let sq = x * x
     where x > 3 and x < 15
     order by sq desc
     limit 3 offset 2)
  sq
// (144, 121, 100)

Match Expressions

The match expression provides multi-way branching based on type or value patterns. It is an expression that produces a value, and works in both functional and procedural contexts.

Syntax

match <expr> {
    case <type_expr>: <expr>            // expression arm
    case <type_expr> { <statements> }   // statement arm
    default: <expr>                     // default expression arm
    default { <statements> }            // default statement arm
}
  • Braces are required around the arm block.
  • Parentheses around the scrutinee are optional: match (expr) { ... } and match expr { ... } are both valid.
  • Expression and statement arms can be freely mixed within one match.
  • ~ refers to the matched value inside arm bodies, like in pipe expressions.
  • Arms are tested top-to-bottom; the first matching arm is selected.
  • default is the catch-all arm (matches anything not matched by previous arms).

Type Patterns

Match on the runtime type using type expressions:

fn describe(value: int | string | bool) => match value {
    case int: "integer"
    case string: "text"
    case bool: "boolean"
}

Literal Patterns

Literal values (integers, floats, booleans, strings, symbols, null) work as case patterns. The case checks equality against the literal value:

fn status_text(code: int) => match code {
    case 200: "OK"
    case 404: "Not Found"
    case 500: "Server Error"
    default: "Unknown"
}

Symbol Patterns

fn color_of(level) => match level {
    case 'info': "blue"
    case 'warn': "yellow"
    case 'error': "red"
    default: "white"
}

Or-Patterns

Combine multiple patterns into a single arm using |:

fn day_type(day) => match day {
    case 'mon' | 'tue' | 'wed' | 'thu' | 'fri': "weekday"
    case 'sat' | 'sun': "weekend"
}

Current Item Reference (~)

Inside match arms, ~ refers to the matched value:

fn check_range(n: int) => match n {
    case 0: "zero"
    case int: if (~ > 0) "positive" else "negative"
}

Mixed Expression and Statement Arms

Expression arms (case T: expr) and statement arms (case T { stmts }) can be freely combined:

fn describe(shape) => match shape.tag {
    case 'circle' {
        let area = 3.14159 * shape.r ** 2;
        "circle with area " ++ string(area)
    }
    case 'rect' {
        let area = shape.w * shape.h;
        "rectangle with area " ++ string(area)
    }
    default: "unknown shape"
}

Nested Match

fn classify(value) => match value {
    case int: match value {
        case 0: "zero"
        default: "nonzero int"
    }
    case string: "string"
    default: "other"
}

Match in Procedural Context

Match works in procedural functions with statement arms supporting var, while, break, continue, return:

pn handle(event) {
    match event.kind {
        case 'click' {
            var count = state.clicks;
            count = count + 1;
            update_state({clicks: count})
        }
        case 'keypress' {
            if (event.key == "Escape") return null;
            process_key(event.key)
        }
        default: null
    }
}

Match in Let Bindings

let label = match status {
    case 'ok': "success"
    case 'warn': "warning"
    case 'error': "failure"
}

Statements

Common Statements

let, if and for statements work in both functional and procedural context.

ConstructExpression FormStatement Form
Ifif (cond) a else bif (cond) { ... }
Forfor (x in col) exprfor x in col { ... }
Matchmatch x { case T: expr }match x { case T { ... } }
Let(let x = 1, x + 1)let x = 1;

Let Statements

// Variable declaration
let x = 42;
let name = "Alice", age = 30;

// With type annotation
let x: int = 42;
let items: string[] = ["a", "b", "c"];

If Statements

If statements use the block form described in If Expressions. Both forms produce the same AST node and can appear in statement position:

// Block form (else optional)
if x > 0 {
    print("positive")
}

if temperature > 30 {
    print("hot")
} else {
    print("comfortable")
}

// Expression else in statement position
if x > 0 { print("ok") } else print("fail")

For Statements

For statements (with curly braces) also produce spreadable arrays:

for item in [1, 2, 3] {
    print(item)
}

for i in 1 to 10 {
    if (i % 2 == 0) {
        print(i, "is even")
    }
}

// Nested for-statements flatten like for-expressions
let matrix = [[1, 2], [3, 4]]
for row in matrix {
    for col in row {
        col * 2
    }
}
// Produces: 2, 4, 6, 8 (flattened)

// Multiple loop variables
for x in [1, 2], y in [3, 4] {
    print(x, y)
}

Procedural Statements

var, while, break, continue, return, and assignment (=) are only available in pn (procedural) functions. See Lambda Procedural Programming for full documentation.


Operators

Operator Precedence

From highest to lowest:

PrecedenceOperatorsDescription
1(), [], [T], ., ?, .?Primary, query
2-, +, not, !, *Unary (!: type negation)
3**Exponentiation
4*, /, div, %Multiplicative
5+, -Additive
6<, <=, >, >=Relational
7==, !=Equality
8andLogical AND
9orLogical OR
10toRange
11is, inType operations
12|, thatPipe, Filter

Arithmetic Operators

OperatorDescriptionExampleResult
+Addition5 + 38
-Subtraction5 - 32
*Multiplication5 * 315
/Division10 / 33.333...
divInteger division10 div 33
%Modulo17 % 52
**Exponentiation2 ** 38

Comparison Operators

OperatorDescriptionExampleResult
==Equal5 == 5true
!=Not equal5 != 3true
<Less than3 < 5true
<=Less or equal5 <= 5true
>Greater than5 > 3true
>=Greater or equal5 >= 3true

Logical Operators

OperatorDescriptionExampleResult
andLogical ANDtrue and falsefalse
orLogical ORtrue or falsetrue
notLogical NOTnot truefalse

Set Operators

OperatorDescriptionExample
&Intersectionset1 & set2
|Unionset1 | set2
!Exclusionset1 ! set2

Type Operators

OperatorDescriptionExampleResult
isType check42 is inttrue
isValue comparison42 is 42true
is nanNaN checknan is nantrue
inMembership2 in [1, 2, 3]true
toRange1 to 5[1, 2, 3, 4, 5]

Pipe and Filter Operators

OperatorDescriptionExampleResult
|Union[1, 2] | [2, 3][1, 2, 3]
|>Pipe (transform)[1, 2, 3] |> ~ * 2[2, 4, 6]
thatFilter[1, 2, 3, 4] that (~ > 2)[3, 4]

File Output

File writes use the procedural output(...) function. See Lambda Procedural Programming.

String/Collection Operators

OperatorDescriptionExampleResult
++String concat"a" ++ "b""ab"
++Scalar concat42 ++ 10"4210"
++Array concat[1] ++ [2][1, 2]
+Element-wise array/list add[1] + [2][3]

This document covers Lambda's expression and statement system. For function definitions, see Lambda Functions. For type details, see Lambda Type System. For procedural programming (var, while, assignment), see Lambda Procedural.