Mesh

Language Basics ​

This guide covers the core Mesh language. After reading it, you can write modules that use literals, immutable bindings, functions and closures, pattern matching, control flow, collections, pipes, and typed error propagation.

Variables ​

Variables in Mesh are created with let bindings and are immutable by default:

mesh
fn main() do
  let name = "Mesh"
  let age = 30
  let pi = 3.14
  let active = true
  println("Hello, ${name}!")
end

You can add type annotations with :: to be explicit about a variable's type:

mesh
fn main() do
  let x :: Int = 42
  let greeting :: String = "hello"
  println("${x}: ${greeting}")
end

A let binds inside a function; there are no module-level bindings, and a let outside a function is error E0080. Make a shared constant a function instead: fn max_retries() -> Int do 3 end.

Type annotations are optional -- the compiler infers types from context. Use annotations when you want to be explicit or when the compiler needs a hint.

Since variables are immutable, you cannot reassign them. Instead, you create a new binding with the same name (shadowing):

mesh
fn main() do
  let x = 1
  let x = x + 1
  println("${x}")
end

Tuple destructuring is supported in bindings:

mesh
fn main() do
  let (name, age) = ("Ada", 36)
  println("#{name} is #{age}")
end

A call on a line of its own runs for its effects and its result is dropped, so ignoring a value needs no binding. In a pattern, _ skips the part you do not need:

mesh
fn main() do
  let (_, age) = ("Ada", 36)
  println("#{age}")
end

Identifiers ​

An identifier starts with _ or a Unicode alphabetic code point. Each remaining character may be _ or a Unicode alphanumeric code point, so names are not limited to ASCII:

mesh
let κόσμος = "world"
let 四季 = 4
let _private = true

Reserved keywords are exact ASCII words. For example, let begins a binding, while letπ is an ordinary identifier.

Comments and Statement Boundaries ​

Mesh supports line comments, documentation comments, module documentation, and nested block comments:

mesh
# A regular line comment
## Documentation for the declaration that follows
##! Documentation for this module

#=
  A block comment.
  #= Block comments can nest. =#
=#

A newline ends a statement. To put two statements on one line, separate them with a semicolon; without one, the second statement is a parse error:

mesh
let x = 1; let y = 2

Newlines are not statement boundaries inside parentheses, brackets, or braces. A line that ends in = or an infix operator continues on the next one (let total = price +), and so does a line followed by one that starts with an infix operator other than - and % (and ready, |> f()), which both start statements of their own; see also Multi-Line Pipes.

Basic Types ​

Mesh has the following core types:

TypeDescriptionExample
IntSigned machine integer42, -5, 0xff
FloatFloating-point number3.14, 1.0e6
StringUTF-8 text"hello"
BoolBoolean valuetrue, false
BytesOpaque binary dataBytes.from_utf8("hello")
U64, U128, I128Checked wide protocol integersU64.parse("18446744073709551615")
JsonA typed JSON valuejson { status: "ok" }
AtomA symbolic value:ok, :not_found
RegexA compiled regular expression~r/[a-z]+/i
()Unit, the absence of a useful value(), nil
(A, B)Tuple(1, "one")
List<T>Immutable sequence[1, 2, 3]
Map<K, V>Immutable key-value collection%{"a" => 1}
Set<T>Immutable collection of unique valuesSet.new()
RangeInteger range valueRange.new(0, 10)
Queue<T>Immutable first-in, first-out queueQueue.new()
Pid<M>Actor identity accepting messages of type Mreturned by spawn(...)
Option<T>Optional valueSome(42), None; shorthand Int?
Result<T, E>Success or failureOk(42), Err("failed"); shorthand Int!String
Fun(A) -> BFunction valuefn x -> x + 1 end

Int, Float, String, and Bool have literal syntax. U64, U128, and I128 are opaque checked values intended for full-width protocol fields; construct and operate on them with their modules rather than Int literals or arithmetic operators.

Numeric Literals ​

Integer literals can use decimal, hexadecimal, binary, or octal notation. Underscores are ignored:

mesh
let decimal = 1_000_000
let hex = 0xff_ff
let binary = 0b1111_0000
let octal = 0o777
let scientific = 1.25e3

The radix prefixes may also be written 0X, 0B, and 0O. A literal with an exponent, such as 1e3, is a Float.

The compiler checks every numeric literal. A malformed one (0x, 1e) is an error, and so is a digit the radix does not have: the error for 0b102 names the digit 2. An integer literal must fit an Int: 9223372036854775807 is the largest, and -9223372036854775808 may be written directly. A hexadecimal, binary, or octal literal is limited to the same maximum, so 0xffffffffffffffff is rejected. A float literal that overflows, such as 1e999, is an error too.

Unit and nil ​

() is the Unit value. nil is an equivalent spelling and is useful when a value is required syntactically but carries no information:

mesh
fn log_done() do
  println("done")
  nil
end

Atoms and Regular Expressions ​

Atoms are lightweight symbolic values. An atom begins with : followed by a lowercase ASCII letter or underscore; the rest of the name may be _ or any Unicode letter or digit, uppercase included:

mesh
let status = :ready
let unit = :millisecond
let mixed = :notFound

Atoms compare with == and !=, match as patterns, hash as map keys, and print as their names ("#{:ready}" is ready):

mesh
fn label(status) -> String do
  case status do
    :ready -> "go"
    :waiting -> "hold"
    _ -> "unknown"
  end
end

Regex literals use ~r/.../ and accept i (case-insensitive), m (multiline), and s (dot matches newline) flags:

mesh
let digits = ~r/\d+/
let name = ~r/^[a-z]+$/im
let matched = Regex.is_match(digits, "item-42")

The pattern itself may span physical source lines until its closing unescaped /. Flags still follow that closing delimiter:

mesh
let two_lines = ~r/^first$
^second$/ms

Here, the physical newline is part of the pattern. The m flag separately changes how ^ and $ match within the input; it is not what permits the literal to span source lines.

String Interpolation ​

Strings support two interpolation syntaxes -- #{} (preferred) and ${} (also valid). Expressions inside the braces are evaluated and rendered through their Display implementations:

mesh
fn main() do
  let name = "Mesh"
  let val = 42
  println("Hello, #{name}!")
  println("The answer is #{val}")
  println("Double: #{val * 2}")
end

String Escapes ​

A backslash starts an escape:

EscapeMeaning
\n, \t, \r, \0Newline, tab, carriage return, NUL
\\, \"Backslash, double quote
\$, \#A literal $ or #, so "\#{x}" is the text #{x} rather than an interpolation
\u{1F389}The Unicode character with that hexadecimal code point (1 to 6 digits)

Any other escape, such as \q, is a compile error, as is a malformed \u{...}.

mesh
fn main() do
  let price = 5
  println("tab:\t| quote: \" | literal: \#{price} | emoji: \u{1F389}")
end

Heredoc Strings ​

Use triple-quote """...""" for multiline strings. Heredocs support interpolation, and ordinary quote characters and newlines can appear directly until the closing triple quote:

mesh
fn main() do
  let id = 42
  let name = "Alice"
  let body = """
    {"id": #{id}, "name": "#{name}"}
    """
  println(body)
end

The heredoc's text is trimmed so the source can be indented naturally:

  • The newline right after the opening """ is dropped.
  • A final line holding only the indentation before the closing """ is dropped, so the text does not end with a newline.
  • Every line loses as much leading indentation as the closing """ has; deeper indentation is kept. Above, the body is {"id": 42, "name": "Alice"}.
  • Escapes are processed and checked as in ordinary strings.
  • A Windows line ending (\r\n) in the source becomes \n.
  • In a run of more than three quotes, the last three close the heredoc, so it can end with a quote: """say "hi"""" is say "hi".

A heredoc can also be a case pattern; it is trimmed the same way before matching.

Heredocs are useful for SQL queries and any multiline string content where backslash escaping would be cumbersome. For JSON objects, prefer json { } literals instead (see JSON Literals).

Type Inference ​

The Mesh compiler infers types from how values are used. You rarely need to write type annotations for local variables:

mesh
fn main() do
  let x = 42          # inferred as Int
  let name = "Mesh"   # inferred as String
  let flag = true     # inferred as Bool
  println("${x} ${name} ${flag}")
end

Boolean Logic ​

Boolean values support and, or, and not. Symbolic &&, ||, and ! spellings are also available:

mesh
fn main() do
  let t = true
  let f = false
  if t and not f do
    println("logic works")
  end
end

Operators and Precedence ​

From lowest to highest precedence, Mesh groups operators as follows:

GroupOperators
Pipes`
Boolean oror, `
Boolean andand, &&
Equality==, !=
Ordering<, >, <=, >=
Range..
Concatenation<>, ++
Addition+, -
Multiplication*, /, %
Prefix-, not, !
Postfixcalls, field access, ?

<> and ++ are interchangeable: each joins two strings or two lists, and both sides must have the same type. a..b builds a Range wherever it appears. Parentheses can make any grouping explicit.

Integer and Float Arithmetic ​

Int arithmetic follows these rules:

  • / truncates toward zero: -7 / 2 is -3.
  • % takes the sign of the dividend: -7 % 2 is -1 and 7 % -2 is 1.
  • Dividing by zero with / or % is a runtime error that panics.
  • The one overflowing division, -9223372036854775808 / -1, wraps to -9223372036854775808.
  • +, -, * and negation wrap on overflow in two's complement: 9223372036854775807 + 1 is -9223372036854775808. The Checked functions (Standard Library) return overflow as an error instead.

Float values follow IEEE 754: 0.0 / 0.0 is NaN, 1.0 / 0.0 is infinity, and NaN is unequal to everything, itself included, so nan != nan is true. Converting a float to an integer saturates: Float.to_int, Math.floor, Math.ceil, and Math.round return the largest or smallest Int for a value beyond the range, and 0 for NaN.

Functions ​

Functions are declared with the fn keyword, followed by the name, parameters, and a do...end body:

mesh
fn add(a :: Int, b :: Int) -> Int do
  a + b
end

fn greet(name :: String) -> String do
  "Hello, ${name}!"
end

fn main() do
  println("${add(10, 20)}")
  println(greet("Mesh"))
end

The last expression in a function body is the return value -- there is no need for an explicit return keyword (though return is available for early exits).

def is an exact synonym for fn on named functions:

mesh
def greet(name :: String) -> String do
  "Hello, #{name}!"
end

A function with no parameters may leave out its parentheses: fn version do 3 end defines version().

One-Line Functions ​

For simple functions, you can use the concise = syntax:

mesh
fn double(x) = x * 2
fn square(x :: Int) -> Int = x * x

fn main() do
  println("${double(21)}")
  println("${square(6)}")
end

Generic Functions and Bounds ​

Declare type parameters after the function name. Mesh generalizes inferred local bindings and functions, so reusable code can remain polymorphic:

mesh
fn identity<T>(value :: T) -> T do
  value
end

fn main() do
  println("#{identity(42)}")
  println(identity("mesh"))
end

Use a where clause when an operation requires a trait:

mesh
fn render<T>(value :: T) -> String where T: Display do
  value.to_string()
end

Multiple bounds are comma-separated: where T: Display, U: Eq. See Type System for inference and trait details.

Keyword Arguments ​

A run of name: value arguments at the end of a call is collected into one final Map argument:

mesh
fn request(path :: String, options :: Map<String, String>) -> String do
  path
end

let result = request("/events", method: "POST", content_type: "application/json")

Positional arguments must come before keyword arguments. This is syntax sugar for passing a map; it does not add default or reordered named parameters to a function declaration.

Multi-Clause Functions ​

Functions can have multiple clauses that pattern match on their arguments, similar to Elixir:

mesh
fn fib(0) = 0
fn fib(1) = 1
fn fib(n) = fib(n - 1) + fib(n - 2)

fn to_string(true) = "yes"
fn to_string(false) = "no"

fn main() do
  println("${fib(10)}")
  println(to_string(true))
  println(to_string(false))
end

The compiler tries each clause in order and uses the first one that matches. Clauses for the same function and arity must be consecutive, and a catch-all clause must be last.

A parameter can be any pattern: a constructor, a tuple, a list, a cons, or an or-pattern. A clause can also have a do ... end body:

mesh
type Shape do
  Circle(Int)
  Square(Int)
end

fn area(Circle(r)) = r * r * 3
fn area(Square(w)) = w * w

fn len([]) = 0
fn len(_ :: rest) = 1 + len(rest)

fn size_label(1 | 2) = "small"
fn size_label(_) = "large"

fn pick((a, _), true) = a
fn pick((_, b), false) = b

fn fact(0) do
  1
end
fn fact(n) do
  n * fact(n - 1)
end

In a parameter, :: Type after a name or a pattern is a type annotation: n :: Int, 0 :: Int, (a, b) :: (Int, String). When :: is followed by a lowercase name, _, or a list pattern, as in _ :: rest, it makes a cons pattern instead. (The ownership modifiers borrow and consume are the exception: r :: borrow Handle is an annotation.)

Functions can reuse a name at different arities. Each arity is its own function, and a call runs the one with as many parameters as it has arguments (a piped value counts as one):

mesh
fn area(r) = r * r * 3
fn area(w, h) = w * h

fn main() do
  println("${area(2)} ${area(3, 4)} ${3 |> area(4)}")
end

Because such a name does not identify one function, it cannot be used as a value on its own. Pass a closure instead, for example fn r -> area(r) end.

Guard Clauses ​

Multi-clause functions can include when guards for additional conditions. A guard may be any Bool expression, such as when n * 2 > limit (see Guards):

mesh
fn abs(n) when n < 0 = -n
fn abs(n) = n

fn classify(n) when n > 0 = "positive"
fn classify(n) when n < 0 = "negative"
fn classify(n) = "zero"

fn main() do
  println("${abs(-5)}")
  println(classify(10))
  println(classify(-3))
  println(classify(0))
end

Direct Tail Recursion ​

A direct call to the current function in tail position is lowered to a loop, so this accumulator-style recursion does not grow the call stack:

mesh
fn sum_to(n :: Int, total :: Int) -> Int do
  if n <= 0 do
    total
  else
    sum_to(n - 1, total + n)
  end
end

Tail positions include the final expression of blocks, if branches, case/match arms, let continuations, explicit return, and actor receive arms or timeouts. Mutual recursion and self-calls followed by more work are ordinary calls and are not eliminated.

Closures ​

Anonymous functions (closures) are created with fn...end:

mesh
fn main() do
  let factor = 3
  let triple = fn(x :: Int) -> x * factor end
  println("${triple(7)}")
  println("${triple(10)}")
end

Closures capture variables from their surrounding scope. A closure bound with let is as polymorphic as a named function: let id = fn x -> x end can be applied to an Int and then to a String, and each use gets its own compiled copy. The syntax forms are:

  • Arrow syntax for one-line closures: fn x -> x * 2 end
  • Do-end syntax for multi-line closures: fn x do ... end
  • Zero-argument syntax: fn -> 42 end or fn do ... end
  • Multi-clause syntax: fn 0 -> "zero" | n -> "non-zero" end
  • A tuple taken apart: fn ((key, value)) -> "#{key}=#{value}" end, as Map.to_list pairs need; fn (key, value) -> ... end is a closure of two parameters
mesh
fn main() do
  let list = [1, 2, 3, 4, 5]

  # Arrow syntax
  let doubled = list |> map(fn x -> x * 2 end)

  # Do-end syntax for multi-line bodies
  let processed = map(list, fn x do
    let doubled = x * 2
    let incremented = doubled + 1
    incremented
  end)

  println("${doubled}")
  println("${processed}")
end

A call can also take a trailing closure:

mesh
fn with_value(value :: Int, block :: Fun(Int) -> Int) -> Int do
  block(value)
end

let result = with_value(10) do |value|
  value * 2
end

A trailing do |params| ... end closure follows a call's argument list and becomes the call's last argument. It works the same after a method call or at the end of a pipe:

mesh
let doubled = [1, 2, 3].map() do |x|
  x * 2
end

let labels = [1, 2] |> List.map() do |n|
  "item #{n}"
end

The heads of if, while, case, and for never take a trailing closure: in if ready(x) do, the do opens the if body.

A parameter typed to return () runs its function only for its effects, so it accepts a function that returns anything and drops the result:

mesh
fn each_twice(f :: Fun(Int) -> ()) do
  f(1)
  f(2)
end

fn main() do
  each_twice(fn n -> println("#{n}") end)
  each_twice(fn n -> n * 2 end)
end

Pattern Matching ​

The case expression matches a value against patterns and executes the first matching branch. match is an equivalent spelling:

mesh
fn describe(x :: Int) -> String do
  case x do
    0 -> "zero"
    1 -> "one"
    _ -> "other"
  end
end

fn main() do
  println(describe(0))
  println(describe(1))
  println(describe(42))
end

The _ pattern is a wildcard that matches anything. Arms go one per line, or on one line separated by ;, as statements do: case x do 0 -> "zero"; _ -> "other" end.

Every unguarded case or match must cover all possible values. The compiler reports a non-exhaustive match as an error and warns about redundant arms. An arm with a when guard does not count as exhaustive because the guard may be false.

Arm Bodies ​

An arm's body is one expression after ->. For several statements, write -> do ... end, or start the body on the next line, indented. The last expression is the arm's value. return is an expression too, so an arm can leave the function early:

mesh
fn score(o :: Option<Int>) -> Int do
  let points = case o do
    Some(n) when n > 100 -> do
      let capped = 100
      capped
    end
    Some(n) ->
      let doubled = n * 2
      doubled + 1
    None -> return 0
  end
  points * 10
end

Pattern Forms ​

Patterns can bind names and decompose tuples, structs, and constructors:

PatternMeaning
_Match anything without binding it
nameMatch anything and bind it (a lowercase name)
42, -1, "ok", :ok, true, nilLiteral pattern
(left, right)Tuple pattern
Point { x: 0, y }, Geo.Point { x }Struct pattern: field: pattern matches a field, a field alone binds it, and fields left out match anything
Some(value), Result.Ok(value), NoneConstructor pattern; an uppercase name is always a constructor, and an unknown one is an error
head :: tailMatch a non-empty list as its head and tail
[], [first, second]Match a list of exactly that length, element by element
`leftright`
pattern as wholeMatch a pattern and also bind the complete value
mesh
fn describe_pair(value) -> String do
  case value do
    (0, y) -> "on y axis at #{y}"
    (x, 0) -> "on x axis at #{x}"
    (x, y) as _point when x == y -> "diagonal at #{x}"
    _ -> "other"
  end
end

List patterns combine with head :: tail for the usual recursion shape, and the exhaustiveness checker knows that [] and head :: tail together cover every list:

mesh
fn describe(xs :: List<Int>) -> String do
  case xs do
    [] -> "empty"
    [only] -> "one: #{only}"
    first :: rest -> "starts with #{first}, #{List.length(rest)} more"
  end
end

A struct pattern names the fields it cares about. field: pattern matches the field's value, a field on its own binds a variable of that name, and the fields it leaves out match anything. Struct patterns nest inside constructors, tuples, and other struct patterns, and a case over a struct must still cover every value:

mesh
struct Point do
  x :: Int
  y :: Int
end

fn quadrant(p :: Point) -> String do
  case p do
    Point { x: 0, y: 0 } -> "origin"
    Point { x: 0 } | Point { y: 0 } -> "on an axis"
    Point { x, y } when x > 0 and y > 0 -> "first quadrant"
    _ -> "elsewhere"
  end
end

fn manhattan(Point { x, y }) = x + y

A struct pattern that matches every value, like Point { x, y }, can also take a value apart in let and for: let Point { x, y } = p.

Matching on Constructors ​

You can match on sum type constructors and destructure their contents:

mesh
type Color do
  Red
  Green
  Blue
end

fn color_name(c :: Color) -> String do
  case c do
    Red -> "red"
    Green -> "green"
    Blue -> "blue"
  end
end

fn main() do
  let c = Red
  println(color_name(c))
end

Variants can be qualified with their type or module when that makes the source clearer:

mesh
case result do
  Result.Ok(value) -> value
  Result.Err(_) -> 0
end

Guards ​

case, match, receive, function clauses, and multi-clause closures can use a when guard. A guard is any expression of type Bool, and it can use the names its pattern binds: n when n * 2 > limit, n when n > -1, and s when String.length(s) > 3 are all guards.

mesh
case score do
  n when n >= 90 -> "excellent"
  n when n >= 60 -> "passing"
  _ -> "retry"
end

Matching on Results ​

Pattern matching works naturally with Ok and Err result types:

mesh
fn safe_divide(a :: Int, b :: Int) -> Int!String do
  if b == 0 do
    return Err("division by zero")
  end
  Ok(a / b)
end

fn main() do
  let r = safe_divide(10, 2)
  case r do
    Ok(val) -> println("Result: ${val}")
    Err(msg) -> println("Error: ${msg}")
  end
end

See the Error Handling section below for more on result types.

Control Flow ​

If/Else ​

The if/else expression evaluates a condition and runs the corresponding branch:

mesh
fn max(a :: Int, b :: Int) -> Int do
  if a > b do
    a
  else
    b
  end
end

fn main() do
  println("${max(10, 20)}")
end

if is an expression in Mesh, so it returns a value. The else branch is optional; an if without one has type (), so use that form only for its effects.

Chain conditions with else if. The whole chain closes with a single end:

mesh
fn sign(n :: Int) -> String do
  if n < 0 do
    "negative"
  else if n == 0 do
    "zero"
  else
    "positive"
  end
end

For Loops ​

The for...in expression iterates over ranges and collections:

mesh
fn main() do
  # Iterate over a range (0 through 4)
  for i in 0..5 do
    println("${i}")
  end
end

start..end is end-exclusive, so 0..5 yields 0, 1, 2, 3, and 4. A range is also a value on its own: let r = 1..5 builds the same Range as Range.new(1, 5).

For loops can also iterate over lists:

mesh
fn main() do
  let names = ["Alice", "Bob", "Charlie"]
  for name in names do
    println("Hello, ${name}!")
  end
end

Filter Clauses ​

Add a when clause to filter elements during iteration:

mesh
fn main() do
  let evens = for i in 0..10 when i % 2 == 0 do
    i
  end
  for e in evens do
    println("${e}")
  end
end

Every for expression returns a list containing one body result per accepted element, making it a list comprehension even when the body is used primarily for side effects.

Destructuring Loop Variables ​

A tuple or struct pattern in the loop header takes each element apart, exactly as let (a, b) = ... does. Over a map the pattern receives a (key, value) pair:

mesh
fn main() do
  let pairs = [(1, "one"), (2, "two")]
  for (n, name) in pairs when n > 1 do
    println("#{n} is #{name}")
  end
  for (word, count) in %{"a" => 1} do
    println("#{word}: #{count}")
  end
  for (i, item) in List.enumerate(["x", "y"]) do
    println("#{i}: #{item}")
  end
end

Map Iteration ​

Iterate over map entries with destructuring; a single name, as in for fruit in stock, takes the keys alone. Like any for, it returns a list of the body's results:

mesh
fn main() do
  let stock = %{"apples" => 10, "pears" => 20}
  let labels = for {fruit, count} in stock do
    "#{count} #{fruit}"
  end
  println("#{labels}")   # [10 apples, 20 pears]
end

While Loops ​

The while loop repeats its body as long as the condition is true:

mesh
fn main() do
  while true do
    println("loop ran")
    break
  end
  println("after loop")
end

Break and Continue ​

Use break to exit a loop early and continue to skip to the next iteration:

mesh
fn main() do
  # break exits the loop
  while true do
    println("before break")
    break
  end
  println("after loop")

  # continue skips the rest of the current iteration
  let result = for x in [1, 2, 3, 4, 5] when x > 1 do
    if x == 3 do
      continue
    end
    x
  end
  for r in result do
    println("${r}")
  end
end

Pipe Operator ​

The pipe operator |> passes the result of the left-hand expression as the first argument to the right-hand function. It turns nested calls into readable left-to-right chains:

mesh
fn double(x :: Int) -> Int do
  x * 2
end

fn add_one(x :: Int) -> Int do
  x + 1
end

fn main() do
  # Without pipes (nested, reads inside-out)
  let a = add_one(double(5))

  # With pipes (chained, reads left-to-right)
  let b = 5 |> double |> add_one

  println("${a}")
  println("${b}")
end

Both a and b equal 11. The pipe version reads naturally: "take 5, double it, add one."

Pipes with Closures ​

Pipes work well with higher-order functions like map, filter, and reduce:

mesh
fn main() do
  let list = [1, 2, 3, 4, 5]

  let doubled = list |> map(fn x -> x * 2 end)
  let filtered = doubled |> filter(fn x -> x > 4 end)
  let sum = reduce(filtered, 0, fn acc, x -> acc + x end)

  println("${sum}")
end

Slot Pipe Operator ​

The slot pipe |N> routes the left-hand value to a specific argument position (N) instead of the first position:

mesh
fn add(a :: Int, b :: Int) -> Int do
  a + b
end

fn main() do
  # Slot pipe: 10 |2> add(1) = add(1, 10) = 11
  let result = 10 |2> add(1)
  println("#{result}")

  # Chain slot pipe and regular pipe
  let chained = 5 |2> add(10) |> add(1)
  println("#{chained}")
end

Use |2> to insert the piped value as the second argument, |3> for the third, and so on. Slot pipes can be chained with regular pipes.

Multi-Line Pipes ​

Long pipe chains can be split across lines using either the trailing form (|> at the end of a line) or the leading form (|> at the start of the next line):

mesh
fn double(x :: Int) -> Int do
  x * 2
end

fn add_one(x :: Int) -> Int do
  x + 1
end

fn negate(x :: Int) -> Int do
  -x
end

fn main() do
  # Trailing form: |> at the end of each line
  let result = 5 |>
    double |>
    add_one |>
    negate

  # Leading form: |> at the start of continuation lines
  let result2 = 5
    |> double
    |> add_one
    |> negate

  println("#{result}")
  println("#{result2}")
end

Both forms produce identical compiled output to their single-line equivalents -- only formatting differs. Choose whichever reads more clearly for your use case.

Multi-line pipes are especially useful for long chains where all steps would not fit on a single line, such as building an HTTP router:

mesh
fn main() do
  let router = HTTP.router()
    |> HTTP.on_post("/api/events", handle_event)
    |> HTTP.on_get("/api/issues", handle_issues)
    |> HTTP.on_get("/api/dashboard", handle_dashboard)
end

Error Handling ​

Mesh uses result types for error handling. A function that can fail returns T!E, where T is the success type and E is the error type:

mesh
fn safe_divide(a :: Int, b :: Int) -> Int!String do
  if b == 0 do
    return Err("division by zero")
  end
  Ok(a / b)
end
  • Ok(value) wraps a successful result
  • Err(error) wraps an error
  • The return type Int!String means "returns an Int on success or a String error on failure"

The Try Operator ​

The postfix ? operator works with both Result and Option. It unwraps Ok(value) or Some(value); Err(error) or None returns immediately from the enclosing function:

mesh
fn step1(x :: Int) -> Int!String do
  if x < 0 do
    return Err("negative input")
  end
  Ok(x * 2)
end

fn step2(x :: Int) -> Int!String do
  if x > 100 do
    return Err("too large")
  end
  Ok(x + 1)
end

fn pipeline(x :: Int) -> Int!String do
  let a = step1(x)?
  let b = step2(a)?
  Ok(b)
end

fn main() do
  let r = pipeline(10)
  case r do
    Ok(val) -> println("${val}")
    Err(msg) -> println(msg)
  end
end

The ? after step1(x) means: if step1 returns Ok(value), bind value to a and continue; if it returns Err(e), immediately return Err(e) from the current function. This keeps error handling concise without deeply nested pattern matches.

After a pipe, ? applies to the call the value goes into: x |> step1()? is (x |> step1())?, so a chain reads in order, x |> step1()? |> step2()?.

The enclosing function must be able to return that early value: it returns a Result (for an Option operand, an Option). main returns nothing, so ? cannot be used there; handle the Result with case instead. A function or closure without a declared return type returns a Result when it uses ? on one, so its other results must be Ok(...) or Err(...) too.

When the enclosing Result uses a different error type, Mesh looks for a matching From<SourceError> implementation and converts the error during propagation. See From/Into Conversion.

For Option, the enclosing function must return Option:

mesh
fn first_positive(values :: List<Int>) -> Int? do
  let value = List.find(values, fn n -> n > 0 end)?
  Some(value)
end

Handling Results with Pattern Matching ​

Use case to handle both success and error cases:

mesh
fn safe_divide(a :: Int, b :: Int) -> Int!String do
  if b == 0 do
    return Err("division by zero")
  end
  Ok(a / b)
end

fn main() do
  let r = safe_divide(10, 0)
  case r do
    Ok(val) -> println("Result: ${val}")
    Err(msg) -> println("Error: ${msg}")
  end
end

An arm that is only a pattern passes the value it matched through: Ok(value) on its own means Ok(value) -> Ok(value). Use it when another arm changes the rest of the type, as mapping the error does here, so the matched value cannot be returned as it is:

mesh
fn error_length(r :: Int!String) -> Int!Int do
  case r do
    Ok(value)
    Err(message) -> Err(String.length(message))
  end
end

The pattern may bind names, nest constructors (Some(Ok(value))), contain literals (Ok(true)), be a nullary constructor such as None, and take a when guard. A pattern that does not name a whole value, such as _ or a bare Ok, needs an explicit ->.

Panics ​

For a failure the program cannot handle, call panic(message). It has type String -> Never, so it fits in any branch: None -> panic("not a port: #{text}"). A panic prints Mesh panic: message to standard error and ends the current actor, which a supervisor can restart; in main, it ends the program with exit status 101. See Standard Library for an example.

Runtime errors are panics too and behave the same way: List.get past the end of a list, Map.get of a missing key, a call that no function clause matches, and integer division by zero. Recursion too deep for the stack is different: it ends the whole program with error: stack overflow. A direct self-call in tail position runs as a loop and never overflows (see Direct Tail Recursion).

Modules ​

Mesh organizes code into modules. The standard library provides built-in modules like String, List, and Map. They need no import; call their functions with dot notation:

mesh
fn main() do
  let n = String.length("test")
  println("${n}")
end

Every other source file of a project is a module too. Its name comes from its path: each directory and the file name are converted to PascalCase (linear_algebra becomes LinearAlgebra) and joined with dots. The entry file, main.mpl, has no module name.

text
main.mpl                  entry point
geo/shapes.mpl            Geo.Shapes
lib/linear_algebra.mpl    Lib.LinearAlgebra

The import statement makes a module available under the last segment of its name. Qualified names work in type annotations, struct literals, constructors and their patterns, and impl headers such as impl Shapes.Describe for Shapes.Point:

mesh
import Geo.Shapes

fn area(s :: Shapes.Shape) -> Int do
  case s do
    Shapes.Circle(r) -> r * r * 3
    Shapes.Square(w) -> w * w
  end
end

fn main() do
  let p :: Shapes.Point = Shapes.Point { x: 1, y: 2 }
  println("#{p.x} #{area(Shapes.Circle(2))}")
end

You can also import specific public names directly:

mesh
from String import length

fn main() do
  let n = length("test")
  println("${n}")
end

Selective imports can be comma-separated or parenthesized across lines:

mesh
from Geometry import (
  Point,
  distance,
  translate,
)

Glob imports are not supported. Private names cannot be imported.

A few kinds of names are shared by every module of a project: public functions, and structs, sum types, interfaces, actors, services and supervisors, private ones included. Two modules cannot define the same one; the build names both definitions. A struct or sum type defined identically in several modules (a private helper copied into each) is fine.

You can define a module explicitly with module ... do ... end and export declarations with pub:

mesh
pub module Geometry do
  pub struct Point do
    x :: Float
    y :: Float
  end

  pub fn origin() -> Point do
    Point { x: 0.0, y: 0.0 }
  end

  fn internal_helper() -> Int do
    0
  end
end

A module block is a module like a file is: import it to use it, in the file that holds it too. Its name is exactly the one it declares, wherever the file is: module Billing in lib/helpers.mpl is Billing, not Lib.Helpers.Billing. A block named like a file's module, such as Billing beside billing.mpl, is an error. Using a module of the project without importing it is error E0081, which names the import to add. Without pub, only the block's own file may import it:

mesh
import Billing

module Billing do
  pub fn total(items :: List<Int>) -> Int do
    List.reduce(items, 0, fn acc, item -> acc + item end)
  end
end

fn main() do
  println("${Billing.total([1, 2, 3])}")
end

pub is available on functions, modules, structs, interfaces, supervisors, sum types, type aliases, and resources (pub resource; see Resource Types). Actors, services, impl blocks, imports, and local bindings are not declared pub.

Standard Library Modules ​

Mesh includes several built-in modules. None of them needs an import:

ModulePurposeExample
ListList operationsList.length(xs), List.get(xs, 0)
MapKey-value mapsMap.new(), Map.put(m, k, v)
SetUnique value setsSet.new(), Set.add(s, v)
StringString manipulationString.length(s)

Working with Lists ​

Lists are a core data structure. You can create them with literal syntax or the List module:

mesh
fn main() do
  # List literal
  let xs = [1, 2, 3]
  let len = List.length(xs)
  println("${len}")

  # Access by index
  let first = List.get(xs, 0)
  println("${first}")
end

Working with Maps ​

Maps are key-value collections:

mesh
fn main() do
  let m = Map.new()
  let m = Map.put(m, 1, 10)
  let m = Map.put(m, 2, 20)
  let m = Map.put(m, 3, 30)

  for {k, v} in m do
    println("${k}: ${v}")
  end
end

Note that Map.put returns a new map -- all collections in Mesh are immutable.

Map literals use %{key => value}:

mesh
fn main() do
  let scores = %{"Ada" => 10, "Lin" => 9}
  println("#{Map.size(scores)}")
end

Method-Call Syntax ​

A function of the String, List, Map, Set, or Range module can also be called as a method on a value of that type. value.fn(args) means Module.fn(value, args):

mesh
fn main() do
  let xs = [1, 2, 3]
  let m = %{"a" => 1}.put("b", 2)
  println("#{xs.contains(2)} #{m.get("b")} #{m.size()} #{"mesh".length()}")
end

An interface method of the same name takes precedence over the module function.

Function Decorators ​

Mesh has three source decorators for function boundaries: @cluster, @native, and @export. They are declarations with compiler-defined behavior, not general-purpose annotations.

@cluster ​

@cluster marks a public function as runtime-owned clustered work. The uncounted form uses the manifest's [cluster].default_replicas, or a total copy count of two without one; @cluster(N) requests an explicit total copy count:

mesh
@cluster
pub fn refresh_cache() -> Int do
  1
end

@cluster(3)
pub fn rebuild_index() -> Int do
  3
end

The decorated target must resolve to one public, non-overloaded function. The removed clustered(work) spelling is not supported. See Autonomous Clusters for deployment and runtime policy.

@native ​

@native("symbol") declares a Mesh signature implemented by a symbol in a checksum-verified static library:

mesh
@native("mesh_math_add")
pub fn add(left :: Int, right :: Int) -> Int

@native("mesh_decode")
pub fn decode(input :: Bytes) -> Bytes!String

A native declaration:

  • must be pub and have no Mesh body;
  • must give every parameter and the return value an explicit type;
  • cannot have generic parameters, a where clause, or a guard;
  • can pass Int, Float, Bool, String, Bytes, U64, U128, and I128;
  • can additionally return Option or Result containing supported ABI values.

The package's [native] manifest entry selects ABI version 1 bindings and a SHA-256-pinned archive for the exact target. The package manager never executes a native build script.

@export ​

@export("c_symbol") makes a Mesh function callable from a host program when the project is built as a library with meshc build --artifact staticlib or --artifact cdylib:

mesh
@export("mesh_mobile_echo")
pub fn echo(request :: Bytes) -> Bytes!String do
  Ok(request)
end

An exported function must be pub, its symbol must be a C identifier, and its signature must be exactly (Bytes) -> Bytes!String, with no generic parameters, where clause, or guard. Any other declaration is an error (E0055). See Library Builds for building and calling the library.

JSON Literals ​

Use json { } to construct JSON objects without manual string escaping or interpolation:

mesh
# Simple object literal
let response = json { status: "ok", count: 42 }
# response has type Json and encodes as {"status":"ok","count":42}

# Multi-line (same result)
let event = json {
  issue_id: issue_id,
  severity: "high"
}

Keys are bare identifiers (no quotes needed). Values are any Mesh expression — the type determines how they are serialized:

Mesh typeJSON output
String"quoted string"
Int42 (unquoted number)
Float3.14 (unquoted)
Booltrue / false
nilnull
Option<T>null (None) or the value (Some)
List<T>JSON array
Struct with deriving(Json)nested JSON object

Nested json { } values embed raw — no double-encoding:

mesh
let inner = json { code: 200 }
let outer = json { result: inner, ok: true }
# outer is: {"result":{"code":200},"ok":true}

The result of json { } has type Json. A Json passed as an argument where a String is expected is its JSON text, so it goes directly to APIs such as HTTP.response or Ws.broadcast without manual encoding:

mesh
HTTP.response(200, json { status: "ok", affected: n })
HTTP.response(401, json { error: "unauthorized" })
Ws.broadcast(room, json { id: record_id })

A json { } value is a Json like one from Json.parse: Json.encode, Json.object_get and the other Json functions read it, either kind nests in a literal, and either kind is its JSON text when passed as a String argument or interpolated ("#{value}"). Anywhere else a String is expected, such as a function's return value or a List<String>, encode it with Json.encode(value).

This replaces heredoc JSON templates ("""{"key":"#{val}"}""") and manual string concatenation ("{\"key\":\"" <> val <> "\"}") with readable, type-safe object literals.

Note: Keys must be bare identifiers. Reserved keywords (type, fn, let, etc.) cannot be used as keys directly — use heredoc strings for JSON objects with keyword-named fields.

Type Aliases ​

A type alias creates a new name for an existing type. The alias is transparent -- the compiler treats the alias and the original type as identical, so no conversion is needed:

mesh
type Url = String
type Count = Int

fn fetch(url :: Url) -> String do
  # url is transparently a String -- no conversion needed
  url
end

fn main() do
  let u :: Url = "https://example.com"
  println(fetch(u))
end

Type aliases improve code readability by giving domain-meaningful names to primitive types without introducing any runtime overhead.

Exported Type Aliases ​

Use pub type to export a type alias so other modules can import and use it:

mesh
# types/user.mpl
pub type UserId = Int
pub type Email = String
mesh
# main.mpl
from Types.User import UserId, Email

fn create_user(id :: UserId, email :: Email) -> String do
  "user-#{id}: #{email}"
end

fn main() do
  println(create_user(1, "alice@example.com"))
end

Because aliases are transparent, a UserId value satisfies any Int constraint and an Email value satisfies any String constraint.

Aliases can be generic:

mesh
type Pair<A, B> = (A, B)
type StringResult<T> = Result<T, String>

let pair :: Pair<Int, String> = (1, "one")
let result :: StringResult<Int> = Ok(42)

Type arguments are substituted into the aliased type, and the result remains transparent at runtime.

See Type System for full trait and type documentation.

What's Next? ​

You now have a solid foundation in the Mesh language. Continue with:

  • Type System -- structs, sum types, traits, and advanced type features
  • Iterators -- lazy iterator pipelines, combinators, and collection materialization
  • Concurrency -- actors, message passing, supervision trees, and services
  • Syntax Cheatsheet -- quick reference for all Mesh syntax
Edit this page on GitHub
v0.1.8 Last updated: September 28, 2026