Language Guide
Cure is an indentation-structured, expression-oriented language that compiles to BEAM bytecode. Blocks are delimited by indentation level – no do/end, no braces. The last expression in a block is its value.
Modules
Every Cure source file contains one module. The module name follows Elixir/Erlang dot-separated conventions:
mod MyApp.Math
fn add(a: Int, b: Int) -> Int = a + b
fn sub(a: Int, b: Int) -> Int = a - b
All functions inside a module are public by default. Use local fn for private functions:
mod MyApp.Internal
fn public_api(x: Int) -> Int = helper(x) + 1
local fn helper(x: Int) -> Int = x * 2
Functions
Single-expression body
When the body is a single expression, write it after = on the same line:
fn add(a: Int, b: Int) -> Int = a + b
fn greet(name: String) -> String = "Hello, " <> name <> "!"
fn identity(x: T) -> T = x
Multi-expression body
For multiple expressions, put = at the end of the signature line, then indent the body:
fn compute(x: Int) -> Int =
let y = x * 2
let z = y + 1
z
The last expression (z) is the return value.
Multi-clause functions
Pattern match on arguments using | clauses:
fn factorial(n: Int) -> Int
| 0 -> 1
| n -> n * factorial(n - 1)
fn describe(x: Int) -> String
| 0 -> "zero"
| 1 -> "one"
| _ -> "other"
fn fibonacci(n: Int) -> Int
| 0 -> 0
| 1 -> 1
| n -> fibonacci(n - 1) + fibonacci(n - 2)
Guards
Guards restrict when a function clause or pattern applies. Use when after the parameter list or after the pattern:
fn abs(x: Int) -> Int when x >= 0 = x
fn classify(x: Int) -> String
| x when x > 0 -> "positive"
| x when x < 0 -> "negative"
| _ -> "zero"
Guards can use comparison operators (>, <, >=, <=, ==, !=), boolean operators (and, or, not), and arithmetic.
Effect annotations
Functions can declare their side effects after the return type using !:
fn read_file(path: String) -> String ! Io
fn risky(x: Int) -> Int ! Exception
fn complex(x: Int) -> Int ! Io, Exception
Effect kinds: Io, State, Exception, Spawn, Extern. When no ! annotation is present, effects are inferred from the body.
Type annotations
Every parameter must have a type annotation. Return types are declared after ->:
fn process(name: String, count: Int) -> String = name <> "!"
Polymorphic functions use type variables (bare capitalized identifiers):
fn identity(x: T) -> T = x
fn apply(f: A -> B, x: A) -> B = f(x)
Keywords
Reserved words in Cure:
fn, mod, rec, fsm, proto, impl, type, let, if, then, else, elif, match, when, where, local, use, return, throw, try, catch, finally, for, in, true, false, nil, and, or, not
Comments
Single-line comments start with #:
# This is a comment
fn add(a: Int, b: Int) -> Int = a + b # inline comment
Doc comments start with ## and are attached to the following definition. They are extracted by cure doc to generate HTML documentation:
## Returns the absolute value of an integer.
##
## Examples:
## abs(-5) # => 5
fn abs(x: Int) -> Int
| x when x >= 0 -> x
| x -> -x
Operators
Ordered from lowest to highest precedence:
| Precedence | Operator(s) | Associativity | Description |
|---|---|---|---|
| 1 |
|> |
left | pipe |
| 2 |
or |
left | boolean or |
| 3 |
and |
left | boolean and |
| 4 |
== != < > <= >= |
non-assoc | comparison |
| 5 |
.. ..= |
non-assoc | range (exclusive, inclusive) |
| 6 |
<> |
right | string concatenation |
| 7 |
+ - |
left | additive |
| 8 |
* / % |
left | multiplicative |
| 9 |
- not |
prefix | unary negation, boolean not |
| 10 |
. |
left | field access |
Examples:
# Pipe chains
5 |> double |> add(1)
# desugars to: add(double(5), 1)
# Boolean
x > 0 and x < 100 or x == -1
# String concat
"hello" <> " " <> "world"
# Range
1..10
0..=255
# Field access
point.x + point.y
Literals
Integers
42
0xFF
0b1010
1_000_000
Floats
3.14
0.001
Strings
Double-quoted, with interpolation via #{}:
"hello"
"hello #{name}"
"result: #{compute(42)}"
Booleans
true
false
Atoms
Prefixed with ::
:ok
:error
:my_atom
Nil
nil
Chars
Single-quoted:
'a'
'Z'
Lists
[1, 2, 3]
["a", "b", "c"]
[]
Cons syntax for head/tail decomposition:
[h | t]
[1, 2 | rest]
Tuples
Prefixed with %:
%[1, "hello"]
%[x, y, z]
Maps
Prefixed with %:
%{name: "Alice", age: 30}
%{key: value}
Let bindings
Introduce local variables with let:
fn compute(x: Int) -> Int =
let doubled = x * 2
let offset = 10
doubled + offset
let bindings are immutable. Each let introduces a new binding; there is no reassignment.
If / then / else
if is an expression and always produces a value:
fn abs(x: Int) -> Int = if x > 0 then x else 0 - x
fn sign(x: Int) -> String =
if x > 0 then "positive"
elif x < 0 then "negative"
else "zero"
Both branches must be present when the result is used. elif chains multiple conditions.
Match expressions
Pattern match on values with match:
fn unwrap(opt: Option(Int)) -> Int =
match opt
Some(v) -> v
None() -> 0
fn describe_list(xs: List(Int)) -> String =
match xs
[] -> "empty"
[h | t] -> "starts with " <> Std.String.from_int(h)
fn handle(r: Result(Int, String)) -> Int =
match r
Ok(v) -> v
Error(_) -> -1
The compiler checks pattern exhaustiveness – missing cases produce warnings.
Pipe operator
The pipe operator |> passes the result of the left expression as the first argument to the function on the right:
fn process(xs: List(Int)) -> Int =
xs
|> Std.List.filter(fn(x) -> x > 0)
|> Std.List.map(fn(x) -> x * 2)
|> Std.List.sum()
ADTs (algebraic data types)
Define sum types with type:
type Color = Red | Green | Blue
type Option(T) = Some(T) | None
type Result(T, E) = Ok(T) | Error(E)
type Shape = Circle(Float) | Rectangle(Float, Float) | Point
Use constructors as regular functions:
fn wrap(x: Int) -> Option(Int) = Some(x)
fn nothing() -> Option(Int) = None()
fn make_color() -> Color = Red()
fn safe_divide(a: Int, b: Int) -> Result(Int, String) =
if b == 0 then Error("division by zero") else Ok(a / b)
Destructure ADTs with match:
fn unwrap_or(opt: Option(Int), default: Int) -> Int =
match opt
Some(v) -> v
None() -> default
Records
Define records with rec:
rec Point
x: Float
y: Float
rec Person
name: String
age: Int
Records compile to maps. Access fields with .:
fn distance(p: Point) -> Float = p.x + p.y
Protocols
Protocols provide ad-hoc polymorphism (similar to type classes or interfaces). Define with proto, implement with impl:
proto Show(T)
fn show(x: T) -> String
impl Show for Int
fn show(x: Int) -> String = Std.String.from_int(x)
impl Show for Bool
fn show(x: Bool) -> String = if x then "true" else "false"
impl Show for String
fn show(x: String) -> String = x
Protocol dispatch compiles to guard-based multi-clause BEAM functions.
Imports
Import modules with use:
mod MyApp
use Std.List
use Std.Core
fn double_all(xs: List(Int)) -> List(Int) =
Std.List.map(xs, fn(x) -> x * 2)
Import multiple modules from the same namespace:
use Std.{List, Core, Math}
FFI (Foreign Function Interface)
Call Erlang/OTP functions with the @extern attribute:
@extern(:erlang, :abs, 1)
fn abs(x: Int) -> Int
@extern(:math, :sqrt, 1)
fn sqrt(x: Float) -> Float
@extern(:erlang, :integer_to_binary, 1)
fn int_to_string(n: Int) -> String
@extern(:io, :put_chars, 1)
fn print(s: String) -> Atom
The three arguments are the Erlang module atom, the function atom, and the arity. The compiler generates a wrapper that delegates to the Erlang function.
Lambdas
Anonymous functions use fn without a name:
fn double_all(xs: List(Int)) -> List(Int) =
Std.List.map(xs, fn(x) -> x * 2)
fn apply_twice(f: Int -> Int, x: Int) -> Int = f(f(x))
fn make_adder(n: Int) -> Int -> Int = fn(x) -> x + n
Lambdas with multiple arguments:
Std.List.foldl(xs, 0, fn(x) -> fn(acc) -> acc + x)
Note: curried style – each fn takes one argument and returns the next function.
String interpolation
Embed expressions inside strings with #{}:
fn greet(name: String, age: Int) -> String =
"Hello, #{name}! You are #{Std.String.from_int(age)} years old."
Any expression can appear inside #{}.
Refinement types
Constrain a base type with a logical predicate:
type NonZero = {x: Int | x != 0}
type Positive = {x: Int | x > 0}
type Percentage = {p: Int | p >= 0 and p <= 100}
Functions can use when guards that are verified at call sites via Z3:
fn safe_divide(a: Int, b: Int) -> Int when b != 0 = a / b
fn positive_double(x: Int) -> Int when x > 0 = x * 2
See the Type System page for details on how refinement types and dependent type verification work.
FSMs (Finite State Machines)
FSMs are first-class language constructs:
fsm TrafficLight
Red --timer--> Green
Green --timer--> Yellow
Yellow --timer--> Red
* --emergency--> Red
See the Finite State Machines page for the full guide.
Comments
Line comments start with #:
# This is a comment
fn add(a: Int, b: Int) -> Int = a + b # inline comment
Complete example
mod MyApp.Math
use Std.{Result, Option}
type Sign = Positive | Negative | Zero
fn factorial(n: Int) -> Int
| 0 -> 1
| n -> n * factorial(n - 1)
fn classify(x: Int) -> Sign
| x when x > 0 -> Positive
| x when x < 0 -> Negative
| _ -> Zero
fn safe_divide(a: Int, b: {x: Int | x != 0}) -> Int = a / b
fn sum(xs: List(Int)) -> Int =
Std.List.foldl(xs, 0, fn(x) -> fn(acc) -> acc + x)
fn main() -> Int = factorial(10)