Skip to main content

Language guide

Primitive types

Number (f64), Integer (i64), Bool, String, Char, Timestamp, Size, Void.

Refined types

A refined type narrows a base type with a where constraint; value refers to the underlying value.

type Age = Number where value >= 0 and value <= 130
type Email = String where value matches /^[^@\s]+@[^@\s]+\.[^@\s]+$/
type NonEmpty = String where value.length > 0

Construction is gated. Whenever a value is placed into a refined-type field of a compound literal, its constraint is checked; a violation aborts the program with a clear message. Partially-valid instances cannot exist.

type Person = { name: NonEmpty, age: Age }
let ok = Person { name: "Ada", age: 30 }
let bad = Person { age: 999 } // xc: constraint violation: Age -> abort

Constraints may use comparisons/and/or, value.length (string length), and value matches /regex/.

Compound types

type Person = { name: NonEmpty, age: Age, email: Email }
type Team = { lead: Person, members: Person[] }

T? is an optional, T[] an array, T! a result.

Sum (algebraic) types

A sum type's value is exactly one of several variants, each optionally carrying its own fields. Declare the variants with |:

type Shape =
| Circle { radius: Number }
| Rect { w: Number, h: Number }
| Empty // a nullary variant

type Color = | Red | Green | Blue // no payloads == a plain enum

Construct a variant by name (Circle { radius: 2.0 }, or just Empty). Deconstruct with match — a variant pattern may bind the payload:

mapper area(s: Shape) -> Number {
match s {
Circle c -> { return 3.14159 * c.radius * c.radius } // c is the payload
Rect r -> { return r.w * r.h }
Empty -> { return 0.0 }
}
return 0.0
}

A sum type is an ordinary type: use it in fields (type Light = { color: Color }), arrays (Shape[]), parameters, and returns. Variant names must be unique across all sum types in the program.

Type aliases

A type can alias another type — handy for readable plural names for arrays:

type Person = { name: String, age: Number }
type People = Person[] // plural alias for an array
type Name = String // plain alias

mapper headcount(p: People) -> Integer { return p.len }
type Team = { lead: Person, members: People }

empty — zero values

empty T evaluates to the zero value of T: a struct with all fields zeroed, an empty array, 0/false, an empty string.

let nobody = empty Person // { name: "", age: 0 }
let blank = empty People // an empty array (len 0)
let zeroed = empty Team // nested: members is an empty array too

It is a concrete blank value, distinct from an optional's none. Note it bypasses refined-type checks (a zero may not satisfy a constraint), so treat it as a deliberate blank/null-object, not a validated value.

Function kinds

Intent is part of the syntax. Each kind documents purity and effect:

KindMeaning
mapperT -> U pure transformation
projectorstructural field extraction
predicateT -> Bool
consumerside-effecting; may mutate
producer() -> T (often I/O)
reducer(Acc, T) -> Acc
creatorconstructs instances
actionimpure; may mutate; not a pure function (e.g. a web handle)
mapper fullName(p: Person) -> String { return p.name }
predicate isAdult(p: Person) { return p.age >= 18 }
consumer log(msg: String) { system.stdout.writeln(msg) }

A one-expression body can be written inline with => — sugar for { return <expr> } (bounded to its line; use a { block } for multi-line). It works for any kind, including methods and where-overloads:

mapper fullName(p: Person) -> String => p.first + " " + p.last
predicate isAdult(p: Person) => p.age >= 18
mapper tier(n: Integer) -> String where n >= 100 => "high"
mapper tier(n: Integer) -> String => "low"

where-guarded overloading

A function may be declared several times under the same name, each with a where guard over its parameters. At a call site the compiler emits a dispatcher that evaluates the guards in declaration order and calls the first match. An unguarded overload is the default; if none matches and there is no default, the program panics.

type ApiResponse = { status: Number, body: String }

mapper mapResponse(res: ApiResponse) -> String where res.status == 200 {
return "OK: " + res.body
}
mapper mapResponse(res: ApiResponse) -> String where res.status == 404 {
return "Not Found"
}
mapper mapResponse(res: ApiResponse) -> String where res.status >= 500 {
return "Server Error (" + res.status + ")"
}
mapper mapResponse(res: ApiResponse) -> String { // default
return "Unhandled status: " + res.status
}

The same applies to methods: a class may declare a method several times with where guards (each can use self/deps), and the first matching guard wins. std/web routing is exactly this — several action handle(req, res) where req.path == "…" overloads plus a default.

Classes, deps, and dependency injection

Classes never extend classes; reuse is via interfaces and injected deps. Dependency injection is automatic: the compiler discovers which class implements each interface and wires it in — no registration required.

interface Greeter { mapper greet(u: User) -> String }
interface Formatter { mapper format(name: String) -> String }

class TieredGreeter implements Greeter {
deps { logger: Logger, formatter: Formatter } // auto-wired
mapper greet(u: User) -> String { return formatter.format(u.name) }
}

async entry main(args: String[]) -> Integer {
let greeter = App.resolve(Greeter) // resolved automatically
...
}

module App {} // empty — present only so `App.resolve` has a name to call

An interface method may carry a default implementation — a { … } body that implementors inherit unless they override it. The default runs over the method's parameters (it can't touch instance fields, since the concrete type is unknown):

interface WebRequestHandler {
action handle(req: HttpRequest, res: HttpResponse)
mapper getBaseUrl() -> String { return "/" } // default; override to mount elsewhere
}

Steering with bind (optional)

A module may bind an interface to a specific class, or mark a class as a singleton (one shared instance) instead of the default transient (constructed per dependent). Binds are overrides — only needed to pick among candidates or change scope.

module App {
bind Logger -> ConsoleLogger as singleton // shared
bind Greeter -> TieredGreeter as transient // override the auto choice
}

Disambiguating multiple implementations

When an interface has more than one implementation, a dependency says which one it wants:

class TaxEngine implements Engine {
deps {
logger: Logger // exactly one impl -> auto
calc: Calculator where calc.precise() // pick the impl whose guard holds
rules: TaxRule[] // inject ALL implementations
repo: Repository or EmptyRepository // use a real Repository, else the fallback
}
}
FormMeaning
d: Ithe single implementation of I (or the bound one)
d: I where <cond>among I's implementations, the first whose <cond> (over d) holds
d: I[]an array of all implementations of I
d: I or Jthe resolved I, or class J if none qualifies
d: I?the implementation if one exists, else none

Function-, method-, and entry-level dependencies

A function, method, or entry can declare its own deps between the kind and the name; they are auto-resolved before the body and visible by name. Use (…) for plain deps and {…} when you need disambiguation (where / or / I[] / I?):

mapper (logger: Logger) mapPerson(p: Person) -> ResponseDTO { // simple: ( )
logger.print("mapping " + p.name)
return ResponseDTO { greeting: "Hello, " + p.name }
}

mapper { db: Repo where env == "prod" } load(id: String) -> Row { ... } // guarded: { }

async entry (logger: Logger) main(args: String[]) -> Integer { // entry too
logger.print("Hello, world!")
return 0
}

Interface calls dispatch through a vtable; the compiler devirtualizes when the concrete type is known. See the dedicated Dependency injection page, plus examples/di_auto.xi and examples/logger_demo.xi.

Control flow

if isAdult(u) { ... } else { ... }

if let row = maybeRow { use(row) } // optional unwrap

for item in items { ... } // arrays, List<T>, Set<T>

for i in 1..5 { ... } // ranges: 1 2 3 4 5 (inclusive)
for i in 0 until 5 { ... } // 0 1 2 3 4 (exclusive end)
for i in 10 downTo 1 { ... } // counts down
for i in 0..100 step 10 { ... } // custom stride

while cond { ... }

match status {
200 -> { return "ok" }
404 -> { return "missing" }
n -> { return "other " + n } // binds n
}

A match arm can be a { block } or — as sugar — a single-line inline expression, which is returned. Patterns may be a single key, a parenthesised list of keys (matches any), or else / _ for the default:

match code {
"x" -> { return 345 } // block arm
"A" -> 101 // inline: same as -> { return 101 }
("BA", "BD", "BR") -> 200 // multi-key: any of these
else -> 300 // default (alias for `_`)
}

(An inline arm is bounded to its line; use a { block } for multi-line bodies.)

See Error handling for T!, ?, ok/err, and Multi-file projects for import/namespace.