Rex Language Guide
Rex is a small, strongly-typed functional DSL with:
- Hindley–Milner type inference (let-polymorphism)
- algebraic data types (ADTs), including record-carrying constructors
- Haskell-style type classes (including higher-kinded classes like
Functor)
This guide is meant for users and embedders. For locked/production-facing semantics and edge cases, see SPEC.md.
A Program
A Rex program consists of:
- zero or more declarations (
type,class,instance,fn,import) - followed by a single expression (the program result)
Example:
fn inc : i32 -> i32 = \x -> x + 1
let
xs = [1, 2, 3]
in
map inc xs
Modules and Imports
Rex modules are .rex files. Imports are top-level declarations.
Module files are declaration-only: they do not have a top-level expression result. To evaluate an
expression, use snippet/REPL/program entrypoints.
Supported forms:
import foo.bar as Bar
import foo.bar (*)
import foo.bar (x, y as z)
Semantics:
import foo.bar as Barimports a module alias; use qualified access (Bar.name).- Alias-qualified lookup is namespace-aware:
- expression/pattern positions use exported values and constructors (
Bar.value). - type positions use exported types (
Bar.Type). - class-constraint positions use exported classes (
Bar.Class).
- expression/pattern positions use exported values and constructors (
import foo.bar (*)imports all exported values into local unqualified scope.import foo.bar (x, y as z)imports selected exported values;yis bound locally asz.- Module alias imports and clause imports are mutually exclusive in one import declaration.
- Only
pubvalues are importable into unqualified local scope via(*)/ item clauses. - If two imports introduce the same unqualified name (including via
(*)), resolution fails with a module error. - Importing a name that conflicts with a local top-level declaration is a module error.
- Lexical bindings (
let, lambda params, pattern bindings) can shadow imported names. - For binder forms with annotations, the annotation is resolved before the new binder name enters expression scope.
Path resolution:
foo.barresolves tofoo/bar.rex.- Local module paths resolve relative to the importing file.
- Leading
superpath segments walk up directories (for examplesuper.core.calc).
Lexical Structure
Whitespace and Comments
- Whitespace (including newlines) is generally insignificant.
- Comments use
{- ... -}and are stripped before parsing. - Nested block comments are not supported in current Rex builds.
Identifiers and Operators
- Identifiers start with a letter or
_, followed by letters/digits/underscores. - Operators are non-alphanumeric symbol sequences (
+,*,==,<, …). - Operators can be used as values by parenthesizing:
(+),(==),(<).
Lambdas
The lambda syntax is \x -> expr. Some docs/examples may also use Unicode λ and →.
Expressions
Literals
true,false- integers and floats (integer literals are overloaded over
Integraland default toi32when ambiguous) - strings:
"hello" - UUID and datetime literals (if present in your lexer source)
Examples:
( (4 is u8)
, (4 is u64)
, (4 is i16)
, (-3 is i16)
)
Negative literals only specialize to signed types. For example, (-3 is u8) is a type error.
Function Application
Application is left-associative: f x y parses as (f x) y.
let add = \x y -> x + y in add 1 2
Let-In
Let binds one or more definitions and then evaluates a body:
let
x = 1 + 2,
y = 3
in
x * y
Let bindings are polymorphic (HM “let-generalization”):
let id = \x -> x in (id 1, id true, id "hi")
Integer-literal bindings are a special case: unannotated let x = 4 is kept monomorphic so use
sites can specialize it through context.
let
x = 4,
f: u8 -> u8 = \y -> y
in
f x
Recursive Let (let rec)
Use let rec for self-recursive and mutually-recursive bindings.
let rec
even = \n -> if n == 0 then true else odd (n - 1),
odd = \n -> if n == 0 then false else even (n - 1)
in
(even 10, odd 11)
Notes:
- Bindings in
let recare separated by commas.
If-Then-Else
if 1 < 2 then "ok" else "no"
Tuples, Lists, Dictionaries
(1, "hi", true)
[1, 2, 3]
{ a = 1, b = 2 }
Notes:
- Lists are implemented as a
List aADT (Empty/Cons) in the prelude. - Cons expressions use
::(for examplex::xs), equivalent toCons x xs. Consis used with normal constructor-call syntax (Cons head tail), while::is infix sugar.- Dictionary literals
{ k = v, ... }build record/dict values. They become records when used as the payload of an ADT record constructor, or when their type is inferred/annotated as a record.
:: is right-associative, so 1::2::[] means 1::(2::[]).
let
xs = 1::2::3::[]
in
xs
Pattern Matching
match performs structural matching with one or more when arms:
match xs
when Empty -> 0
when Cons h t -> h
Patterns include:
- wildcards:
_ - variables:
x - constructors:
Ok x,Cons h t,Pair a b - qualified constructors via module alias:
Sample.Right x - list patterns:
[],[x],[x, y] - cons patterns:
h::t(equivalent toCons h t) - dict key presence:
{foo, bar}(keys are identifiers) - record patterns on record-carrying constructors:
Bar {x, y}
match [1, 2, 3]
when h::t -> h
when [] -> 0
Rex checks ADT matches for exhaustiveness and reports missing constructors.
Types
Primitive Types
Common built-in types include:
booli32(default integer-literal fallback type)f32(float literal type)stringuuiddatetime
Function Types
Functions are right-associative: a -> b -> c means a -> (b -> c).
Tuples, Lists, Arrays, Dicts
- Tuple type:
(a, b, c) - List type:
List a(prelude) - Array type:
Array a(prelude) - Dict type:
Dict a(prelude; key type is a symbol/field label at runtime)
ADTs
Define an ADT with type:
type Maybe a = Just a | Nothing
Constructors are values (functions) in the prelude environment:
Just 1
Nothing
Record-Carrying Constructors
ADT variants can carry a record payload:
type User = User { name: string, age: i32 }
let u: User = User { name = "Ada", age = 36 } in u
Type Annotations
Annotate let bindings, lambda parameters, and function declarations:
let x: i32 = 1 in x
Annotations can mention ADTs and prelude types:
let xs: List i32 = [1, 2, 3] in xs
They can also use module-qualified type names:
import dep as D
fn id x: D.Boxed -> D.Boxed = x
Records: Projection and Update
Rex supports:
- projection:
x.field - record update:
{ base with { field = expr } }
Projection and update are valid when the field is definitely available on the base:
- on plain record types
{ field: Ty, ... } - on single-variant ADTs whose payload is a record
- on multi-variant ADTs only after the constructor has been proven (typically by
match)
Example (multi-variant refinement via match):
type Sum = A { x: i32 } | B { x: i32 }
let s: Sum = A { x = 1 } in
match s
when A {x} -> { s with { x = x + 1 } }
when B {x} -> { s with { x = x + 2 } }
Declarations
Functions (fn)
Top-level functions are declared with an explicit type signature and a value (typically a lambda):
fn add : i32 -> i32 -> i32 = \x y -> x + y
Top-level fn declarations are mutually recursive, so they can refer to each other in the same
module:
fn even : i32 -> bool = \n ->
if n == 0 then true else odd (n - 1)
fn odd : i32 -> bool = \n ->
if n == 0 then false else even (n - 1)
even 10
Type Classes (class)
Type classes declare overloaded operations. Method signatures live in the class:
class Size a
size : a -> i32
Methods can be operators (use parentheses to refer to them as values if needed):
class Eq a
== : a -> a -> bool
Superclasses use <= (read “requires”):
class Ord a <= Eq a
< : a -> a -> bool
Instances (instance)
Instances attach method implementations to a concrete head type, optionally with constraints:
class Size a
size : a -> i32
instance Size (List t)
size = \xs ->
match xs
when Empty -> 0
when Cons _ rest -> 1 + size rest
The class in an instance header may be module-qualified:
import dep as D
instance D.Pick i32 where
pick = 7
Instance contexts use <=:
class Show a
show : a -> string
instance Show i32
show = \_ -> "<i32>"
instance Show (List a) <= Show a
show = \xs ->
let
step = \out x ->
if out == "["
then out + show x
else out + ", " + show x,
out = foldl step "[" xs
in
out + "]"
Notes:
- Instance heads are non-overlapping per class (overlap is rejected).
- Inside instance method bodies, the instance context is the only source of “given” constraints.
Prelude Type Classes (Selected)
Rex ships a prelude with common abstractions and instances. Highlights:
- numeric hierarchy:
AdditiveMonoid,Semiring,Ring,Field, … Default(default) for common scalar and container typesEq/OrdFunctor/Applicative/MonadforList,Array,Option,ResultFoldable,Filterable,Sequence- multi-parameter
Indexable t awith instances for lists/arrays
Example: Functor across different container types:
( map ((*) 2) [1, 2, 3]
, map ((+) 1) (Some 41)
, map ((*) 2) (Ok 21)
)
Example: Indexable:
get 0 [10, 20, 30]
Defaulting (Ambiguous Types)
Rex supports defaulting for variables constrained by defaultable classes
(for example AdditiveMonoid).
This matters for expressions like zero where no concrete type is otherwise forced.
This defaulting pass is separate from the Default type class method default.
Example:
zero
With no other constraints, zero defaults to a concrete candidate type. See
SPEC.md for the exact algorithm and candidate order.